hatchkit 0.1.40 → 0.1.42
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/dist/adopt.d.ts.map +1 -1
- package/dist/adopt.js +663 -82
- package/dist/adopt.js.map +1 -1
- package/dist/config.d.ts +32 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +91 -38
- package/dist/config.js.map +1 -1
- package/dist/deploy/coolify-app.d.ts.map +1 -1
- package/dist/deploy/coolify-app.js +0 -7
- package/dist/deploy/coolify-app.js.map +1 -1
- package/dist/deploy/coolify.d.ts.map +1 -1
- package/dist/deploy/coolify.js +20 -1
- package/dist/deploy/coolify.js.map +1 -1
- package/dist/deploy/ghcr.d.ts +4 -2
- package/dist/deploy/ghcr.d.ts.map +1 -1
- package/dist/deploy/ghcr.js +1 -1
- package/dist/deploy/ghcr.js.map +1 -1
- package/dist/deploy/github.d.ts +4 -3
- package/dist/deploy/github.d.ts.map +1 -1
- package/dist/deploy/github.js +5 -2
- package/dist/deploy/github.js.map +1 -1
- package/dist/deploy/pages.d.ts +41 -0
- package/dist/deploy/pages.d.ts.map +1 -1
- package/dist/deploy/pages.js +363 -22
- package/dist/deploy/pages.js.map +1 -1
- package/dist/deploy/regen-infra.d.ts.map +1 -1
- package/dist/deploy/regen-infra.js +5 -11
- package/dist/deploy/regen-infra.js.map +1 -1
- package/dist/deploy/rollback.d.ts.map +1 -1
- package/dist/deploy/rollback.js +44 -6
- package/dist/deploy/rollback.js.map +1 -1
- package/dist/deploy/terraform.d.ts.map +1 -1
- package/dist/deploy/terraform.js +20 -37
- package/dist/deploy/terraform.js.map +1 -1
- package/dist/dns.d.ts.map +1 -1
- package/dist/dns.js +4 -5
- package/dist/dns.js.map +1 -1
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +110 -36
- package/dist/doctor.js.map +1 -1
- package/dist/email/index.d.ts +31 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +251 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/presets.d.ts +14 -0
- package/dist/email/presets.d.ts.map +1 -0
- package/dist/email/presets.js +33 -0
- package/dist/email/presets.js.map +1 -0
- package/dist/email/setup.d.ts +93 -0
- package/dist/email/setup.d.ts.map +1 -0
- package/dist/email/setup.js +263 -0
- package/dist/email/setup.js.map +1 -0
- package/dist/email/spf.d.ts +56 -0
- package/dist/email/spf.d.ts.map +1 -0
- package/dist/email/spf.js +102 -0
- package/dist/email/spf.js.map +1 -0
- package/dist/index.js +306 -22
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +37 -0
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +536 -55
- package/dist/inventory.js.map +1 -1
- package/dist/overview.d.ts +101 -0
- package/dist/overview.d.ts.map +1 -0
- package/dist/overview.js +880 -0
- package/dist/overview.js.map +1 -0
- package/dist/prompts.d.ts +27 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +262 -34
- package/dist/prompts.js.map +1 -1
- package/dist/provision/index.d.ts +20 -1
- package/dist/provision/index.d.ts.map +1 -1
- package/dist/provision/index.js +115 -0
- package/dist/provision/index.js.map +1 -1
- package/dist/provision/s3-buckets.js +1 -1
- package/dist/provision/s3-buckets.js.map +1 -1
- package/dist/scaffold/app.d.ts.map +1 -1
- package/dist/scaffold/app.js +15 -7
- package/dist/scaffold/app.js.map +1 -1
- package/dist/scaffold/build-pipeline.d.ts +16 -0
- package/dist/scaffold/build-pipeline.d.ts.map +1 -1
- package/dist/scaffold/build-pipeline.js +47 -4
- package/dist/scaffold/build-pipeline.js.map +1 -1
- package/dist/scaffold/infra.d.ts +4 -5
- package/dist/scaffold/infra.d.ts.map +1 -1
- package/dist/scaffold/infra.js +18 -57
- package/dist/scaffold/infra.js.map +1 -1
- package/dist/scaffold/manifest.d.ts +6 -0
- package/dist/scaffold/manifest.d.ts.map +1 -1
- package/dist/scaffold/manifest.js +2 -0
- package/dist/scaffold/manifest.js.map +1 -1
- package/dist/scaffold/pages-heuristics.d.ts +17 -0
- package/dist/scaffold/pages-heuristics.d.ts.map +1 -0
- package/dist/scaffold/pages-heuristics.js +344 -0
- package/dist/scaffold/pages-heuristics.js.map +1 -0
- package/dist/scaffold/pages-mode.d.ts +10 -0
- package/dist/scaffold/pages-mode.d.ts.map +1 -0
- package/dist/scaffold/pages-mode.js +107 -0
- package/dist/scaffold/pages-mode.js.map +1 -0
- package/dist/scaffold/pkg-json.d.ts +4 -0
- package/dist/scaffold/pkg-json.d.ts.map +1 -1
- package/dist/scaffold/pkg-json.js +17 -0
- package/dist/scaffold/pkg-json.js.map +1 -1
- package/dist/scaffold/surfaces.d.ts.map +1 -1
- package/dist/scaffold/surfaces.js +12 -1
- package/dist/scaffold/surfaces.js.map +1 -1
- package/dist/scaffold/update.js +1 -1
- package/dist/scaffold/update.js.map +1 -1
- package/dist/templates/build-pipeline/Dockerfile.nextjs.hbs +103 -0
- package/dist/templates/build-pipeline/docker-compose.yml.hbs +23 -6
- package/dist/utils/cloudflare-api.d.ts +158 -13
- package/dist/utils/cloudflare-api.d.ts.map +1 -1
- package/dist/utils/cloudflare-api.js +219 -11
- package/dist/utils/cloudflare-api.js.map +1 -1
- package/dist/utils/coolify-api.d.ts +9 -0
- package/dist/utils/coolify-api.d.ts.map +1 -1
- package/dist/utils/coolify-api.js +26 -0
- package/dist/utils/coolify-api.js.map +1 -1
- package/dist/utils/run-ledger.d.ts +42 -1
- package/dist/utils/run-ledger.d.ts.map +1 -1
- package/dist/utils/run-ledger.js.map +1 -1
- package/dist/utils/s3-admin.d.ts +9 -0
- package/dist/utils/s3-admin.d.ts.map +1 -0
- package/dist/utils/s3-admin.js +46 -0
- package/dist/utils/s3-admin.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface EmailAddressPreset {
|
|
2
|
+
/** Local part. Joined with `@<domain>` at apply time. */
|
|
3
|
+
localPart: string;
|
|
4
|
+
/** Human-readable description shown in the multi-select prompt. */
|
|
5
|
+
description: string;
|
|
6
|
+
/** Whether this preset is ticked by default in the picker. */
|
|
7
|
+
defaultChecked: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const DEFAULT_FORWARD_PRESETS: EmailAddressPreset[];
|
|
10
|
+
/** Whether to enable a catch-all rule (`*@domain` → destination) by
|
|
11
|
+
* default. Catch-all is a safety net for anything not matched by an
|
|
12
|
+
* explicit rule — recommended for personal/operator domains. */
|
|
13
|
+
export declare const DEFAULT_CATCH_ALL = true;
|
|
14
|
+
//# sourceMappingURL=presets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../../src/email/presets.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,eAAO,MAAM,uBAAuB,EAAE,kBAAkB,EAMvD,CAAC;AAEF;;iEAEiE;AACjE,eAAO,MAAM,iBAAiB,OAAO,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Email forwarding presets.
|
|
3
|
+
*
|
|
4
|
+
* Default set of local-parts to offer when configuring Cloudflare Email
|
|
5
|
+
* Routing on a new project's zone. The picker is multi-select so the
|
|
6
|
+
* user can untick anything that doesn't apply, or add custom entries.
|
|
7
|
+
*
|
|
8
|
+
* Curated to cover the common public-facing aliases without bloating
|
|
9
|
+
* the rule list (each rule is a distinct Email Routing entry):
|
|
10
|
+
* · hello@ — generic first-touch contact
|
|
11
|
+
* · rico@ — personal alias (the operator's first name)
|
|
12
|
+
* · admin@ — system / infrastructure correspondence (TLS notices,
|
|
13
|
+
* registrar alerts, dotenvx/Github billing receipts)
|
|
14
|
+
* · support@— customer-facing support inbox
|
|
15
|
+
* · hi@ — short personal alternative to hello@
|
|
16
|
+
*
|
|
17
|
+
* A catch-all rule (`*@domain`) is offered separately because
|
|
18
|
+
* Cloudflare's API treats it differently — it's exactly one rule per
|
|
19
|
+
* zone (PUT semantics), not a list. The default is "enable catch-all"
|
|
20
|
+
* so stray addresses (`careers@`, `dmarc@`, …) still reach the user.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_FORWARD_PRESETS = [
|
|
23
|
+
{ localPart: "hello", description: "general first-touch contact", defaultChecked: true },
|
|
24
|
+
{ localPart: "rico", description: "personal alias", defaultChecked: true },
|
|
25
|
+
{ localPart: "admin", description: "infrastructure / system alerts", defaultChecked: true },
|
|
26
|
+
{ localPart: "support", description: "customer-facing support", defaultChecked: true },
|
|
27
|
+
{ localPart: "hi", description: "short personal alias", defaultChecked: false },
|
|
28
|
+
];
|
|
29
|
+
/** Whether to enable a catch-all rule (`*@domain` → destination) by
|
|
30
|
+
* default. Catch-all is a safety net for anything not matched by an
|
|
31
|
+
* explicit rule — recommended for personal/operator domains. */
|
|
32
|
+
export const DEFAULT_CATCH_ALL = true;
|
|
33
|
+
//# sourceMappingURL=presets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presets.js","sourceRoot":"","sources":["../../src/email/presets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAWH,MAAM,CAAC,MAAM,uBAAuB,GAAyB;IAC3D,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,6BAA6B,EAAE,cAAc,EAAE,IAAI,EAAE;IACxF,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,EAAE,IAAI,EAAE;IAC1E,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,gCAAgC,EAAE,cAAc,EAAE,IAAI,EAAE;IAC3F,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,yBAAyB,EAAE,cAAc,EAAE,IAAI,EAAE;IACtF,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,sBAAsB,EAAE,cAAc,EAAE,KAAK,EAAE;CAChF,CAAC;AAEF;;iEAEiE;AACjE,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type CfEmailDestination } from "../utils/cloudflare-api.js";
|
|
2
|
+
/** Cloudflare-published MX hosts for Email Routing. Verified against
|
|
3
|
+
* what `GET /zones/{id}/email/routing/dns` returns — kept hardcoded
|
|
4
|
+
* so a hatchkit run can describe the records up-front without an
|
|
5
|
+
* extra API call, and because the values have been stable since the
|
|
6
|
+
* Email Routing product launched. The API stays the source of truth
|
|
7
|
+
* in practice: the email module calls `getEmailRoutingDnsRecords()`
|
|
8
|
+
* and uses *those* values, falling back to this list only if CF is
|
|
9
|
+
* unreachable. */
|
|
10
|
+
export declare const CLOUDFLARE_EMAIL_ROUTING_MX: {
|
|
11
|
+
host: string;
|
|
12
|
+
priority: number;
|
|
13
|
+
}[];
|
|
14
|
+
export interface EmailSetupOptions {
|
|
15
|
+
/** Bearer token with Zone:DNS:Edit + Zone:Email Routing Rules:Edit +
|
|
16
|
+
* Account:Email Routing Addresses:Edit. */
|
|
17
|
+
token: string;
|
|
18
|
+
/** Optional Cloudflare account id. Discovered from the zone when
|
|
19
|
+
* absent (since the destinations API is account-scoped, we always
|
|
20
|
+
* need one before the call). */
|
|
21
|
+
accountId?: string;
|
|
22
|
+
/** Apex domain — must match a zone the token can access. */
|
|
23
|
+
domain: string;
|
|
24
|
+
/** Email address that will receive forwarded mail. CF sends a
|
|
25
|
+
* verification email here on first add. */
|
|
26
|
+
destination: string;
|
|
27
|
+
/** Local parts to create forwarding rules for. Each becomes a
|
|
28
|
+
* `<localPart>@<domain>` literal-match rule forwarding to
|
|
29
|
+
* `destination`. Pass an empty list to skip individual rules
|
|
30
|
+
* (still useful with `catchAll: true`). */
|
|
31
|
+
addresses: string[];
|
|
32
|
+
/** Whether to set a catch-all rule (`*@domain` → destination). */
|
|
33
|
+
catchAll: boolean;
|
|
34
|
+
/** SPF includes to merge with Cloudflare's. Use this when the zone
|
|
35
|
+
* already sends via Resend / SES / etc. Pre-existing on-zone
|
|
36
|
+
* records are also auto-merged. */
|
|
37
|
+
extraSpfIncludes?: string[];
|
|
38
|
+
/** DMARC policy. Defaults to "quarantine". */
|
|
39
|
+
dmarcPolicy?: "none" | "quarantine" | "reject";
|
|
40
|
+
/** Aggregate-report destination for DMARC. Defaults to
|
|
41
|
+
* `dmarc@<domain>` (will be caught by the catch-all if enabled).
|
|
42
|
+
* Override when you want CF Email Routing's reports forwarded to
|
|
43
|
+
* a known inbox without relying on catch-all. */
|
|
44
|
+
dmarcRua?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Per-resource report returned by {@link runEmailSetup}. The caller
|
|
47
|
+
* feeds the ledger entries into `RunLedger.record()` for rollback. */
|
|
48
|
+
export interface EmailSetupResult {
|
|
49
|
+
domain: string;
|
|
50
|
+
zoneId: string;
|
|
51
|
+
accountId: string;
|
|
52
|
+
/** True when this run was the one that enabled routing on the zone. */
|
|
53
|
+
routingEnabledThisRun: boolean;
|
|
54
|
+
destination: {
|
|
55
|
+
record: CfEmailDestination;
|
|
56
|
+
/** True when this run created the destination (verification email
|
|
57
|
+
* was just sent). False when it already existed. */
|
|
58
|
+
createdThisRun: boolean;
|
|
59
|
+
/** "active" / "pending" — the user must click the verify link
|
|
60
|
+
* before forwards land. */
|
|
61
|
+
verified: string | null;
|
|
62
|
+
};
|
|
63
|
+
/** DNS records the run created/updated. Each entry's `created` is
|
|
64
|
+
* true when this run added it (use for the rollback ledger). */
|
|
65
|
+
dnsRecords: Array<{
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
type: "MX" | "TXT";
|
|
69
|
+
content: string;
|
|
70
|
+
created: boolean;
|
|
71
|
+
updated: boolean;
|
|
72
|
+
}>;
|
|
73
|
+
/** Per-address forwarding rules. */
|
|
74
|
+
rules: Array<{
|
|
75
|
+
address: string;
|
|
76
|
+
id: string;
|
|
77
|
+
created: boolean;
|
|
78
|
+
updated: boolean;
|
|
79
|
+
}>;
|
|
80
|
+
/** Catch-all is a zone-level singleton — toggled, not "created" in
|
|
81
|
+
* the usual sense. `changed` is true when this run flipped its
|
|
82
|
+
* enabled state or forward destination. */
|
|
83
|
+
catchAll?: {
|
|
84
|
+
enabled: boolean;
|
|
85
|
+
changed: boolean;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export declare function runEmailSetup(opts: EmailSetupOptions): Promise<EmailSetupResult>;
|
|
89
|
+
/** Pretty-print the result for the CLI. Centralised here so both the
|
|
90
|
+
* standalone command and the create/adopt-flow hook print the same
|
|
91
|
+
* status block. */
|
|
92
|
+
export declare function printEmailSetupSummary(result: EmailSetupResult): void;
|
|
93
|
+
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/email/setup.ts"],"names":[],"mappings":"AA2BA,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,4BAA4B,CAAC;AAGpC;;;;;;;mBAOmB;AACnB,eAAO,MAAM,2BAA2B;;;GAIvC,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC;gDAC4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd;;qCAEiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf;gDAC4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB;;;gDAG4C;IAC5C,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,kEAAkE;IAClE,QAAQ,EAAE,OAAO,CAAC;IAClB;;wCAEoC;IACpC,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,QAAQ,CAAC;IAC/C;;;sDAGkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;uEACuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,qBAAqB,EAAE,OAAO,CAAC;IAC/B,WAAW,EAAE;QACX,MAAM,EAAE,kBAAkB,CAAC;QAC3B;6DACqD;QACrD,cAAc,EAAE,OAAO,CAAC;QACxB;oCAC4B;QAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;KACzB,CAAC;IACF;qEACiE;IACjE,UAAU,EAAE,KAAK,CAAC;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,IAAI,GAAG,KAAK,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC,CAAC;IACH,oCAAoC;IACpC,KAAK,EAAE,KAAK,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC,CAAC;IACH;;gDAE4C;IAC5C,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CACnD;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAiKtF;AAgCD;;oBAEoB;AACpB,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAqDrE"}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Email setup orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Configures Cloudflare Email Routing end-to-end for one zone:
|
|
5
|
+
* 1. Enable Email Routing on the zone (idempotent).
|
|
6
|
+
* 2. Verify (or add) the destination address — the verification email
|
|
7
|
+
* lands in the user's inbox; they click the link before routing
|
|
8
|
+
* starts working. Skipping verify isn't an option — Cloudflare
|
|
9
|
+
* rejects forwards to unverified destinations.
|
|
10
|
+
* 3. Upsert the receiving MX records (3 Cloudflare hosts).
|
|
11
|
+
* 4. Upsert a single SPF TXT, merging Cloudflare's include with any
|
|
12
|
+
* pre-existing senders (e.g. Resend). One SPF record per zone is
|
|
13
|
+
* the RFC 7208 rule — multiple records cause PermError.
|
|
14
|
+
* 5. Upsert a DMARC TXT at `_dmarc.<zone>`. Default `p=quarantine`
|
|
15
|
+
* with `sp=none` to avoid breaking subdomain senders.
|
|
16
|
+
* 6. Create the forwarding rules — one per `<localPart>@<zone>` →
|
|
17
|
+
* destination. Idempotent: existing rules with matching matchers
|
|
18
|
+
* get their forward updated; otherwise we POST a new rule.
|
|
19
|
+
* 7. Optionally set the catch-all (a single rule per zone, separate
|
|
20
|
+
* endpoint from the rule list).
|
|
21
|
+
*
|
|
22
|
+
* Caller surfaces results via the {@link EmailSetupResult} so the
|
|
23
|
+
* create/adopt run-ledger can record each newly created resource for
|
|
24
|
+
* later rollback.
|
|
25
|
+
*/
|
|
26
|
+
import chalk from "chalk";
|
|
27
|
+
import { CloudflareApi, } from "../utils/cloudflare-api.js";
|
|
28
|
+
import { buildDmarcRecord, buildSpfRecord, parseSpfIncludes } from "./spf.js";
|
|
29
|
+
/** Cloudflare-published MX hosts for Email Routing. Verified against
|
|
30
|
+
* what `GET /zones/{id}/email/routing/dns` returns — kept hardcoded
|
|
31
|
+
* so a hatchkit run can describe the records up-front without an
|
|
32
|
+
* extra API call, and because the values have been stable since the
|
|
33
|
+
* Email Routing product launched. The API stays the source of truth
|
|
34
|
+
* in practice: the email module calls `getEmailRoutingDnsRecords()`
|
|
35
|
+
* and uses *those* values, falling back to this list only if CF is
|
|
36
|
+
* unreachable. */
|
|
37
|
+
export const CLOUDFLARE_EMAIL_ROUTING_MX = [
|
|
38
|
+
{ host: "route1.mx.cloudflare.net", priority: 1 },
|
|
39
|
+
{ host: "route2.mx.cloudflare.net", priority: 2 },
|
|
40
|
+
{ host: "route3.mx.cloudflare.net", priority: 3 },
|
|
41
|
+
];
|
|
42
|
+
export async function runEmailSetup(opts) {
|
|
43
|
+
const cf = new CloudflareApi({ token: opts.token, accountId: opts.accountId });
|
|
44
|
+
const zone = await cf.getZoneByName(opts.domain);
|
|
45
|
+
if (!zone) {
|
|
46
|
+
throw new Error(`No Cloudflare zone for "${opts.domain}". Add the zone to your CF account and re-run.`);
|
|
47
|
+
}
|
|
48
|
+
const accountId = opts.accountId ?? zone.account?.id;
|
|
49
|
+
if (!accountId) {
|
|
50
|
+
throw new Error(`Cloudflare account id could not be resolved for zone ${opts.domain}. Set it via \`hatchkit config add dns\` (advanced — leave blank only when the token spans one account).`);
|
|
51
|
+
}
|
|
52
|
+
// Step 1 — enable routing on the zone. Idempotent; CF returns the
|
|
53
|
+
// current settings on re-enable. We still record whether *this* call
|
|
54
|
+
// flipped enabled=false → true so the rollback ledger doesn't yank
|
|
55
|
+
// a routing setup the user enabled by hand earlier.
|
|
56
|
+
const beforeRouting = await cf.getEmailRouting(zone.id);
|
|
57
|
+
if (!beforeRouting?.enabled) {
|
|
58
|
+
await cf.enableEmailRouting(zone.id);
|
|
59
|
+
}
|
|
60
|
+
const routingEnabledThisRun = !beforeRouting?.enabled;
|
|
61
|
+
// Step 2 — destination address. CF sends a verification email if it
|
|
62
|
+
// didn't already exist.
|
|
63
|
+
const dest = await cf.addEmailDestination(accountId, opts.destination);
|
|
64
|
+
// Step 3 — MX records. Fetch CF's recommended list first (in case the
|
|
65
|
+
// hosts change), fall back to the constant if the call fails.
|
|
66
|
+
let mxRecords = CLOUDFLARE_EMAIL_ROUTING_MX.map((m) => ({
|
|
67
|
+
name: opts.domain,
|
|
68
|
+
content: m.host,
|
|
69
|
+
priority: m.priority,
|
|
70
|
+
}));
|
|
71
|
+
try {
|
|
72
|
+
const recommended = await cf.getEmailRoutingDnsRecords(zone.id);
|
|
73
|
+
const mx = recommended.filter((r) => r.type === "MX");
|
|
74
|
+
if (mx.length > 0) {
|
|
75
|
+
mxRecords = mx.map((r) => ({
|
|
76
|
+
name: r.name,
|
|
77
|
+
content: r.content,
|
|
78
|
+
priority: r.priority ?? 10,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Fall back to constants — see the comment on
|
|
84
|
+
// CLOUDFLARE_EMAIL_ROUTING_MX. CF's list is authoritative; the
|
|
85
|
+
// fallback keeps setup working if that one endpoint flakes.
|
|
86
|
+
}
|
|
87
|
+
const dnsRecords = [];
|
|
88
|
+
for (const mx of mxRecords) {
|
|
89
|
+
const res = await cf.upsertRecord(zone.id, {
|
|
90
|
+
type: "MX",
|
|
91
|
+
name: mx.name,
|
|
92
|
+
content: mx.content,
|
|
93
|
+
priority: mx.priority,
|
|
94
|
+
});
|
|
95
|
+
dnsRecords.push({
|
|
96
|
+
id: res.id,
|
|
97
|
+
name: mx.name,
|
|
98
|
+
type: "MX",
|
|
99
|
+
content: mx.content,
|
|
100
|
+
created: res.created,
|
|
101
|
+
updated: res.updated,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Step 4 — SPF. Merge Cloudflare's include with whatever's already on
|
|
105
|
+
// the zone (Resend, SES, etc.) and any caller-supplied includes.
|
|
106
|
+
const existingSpf = await findApexSpf(cf, zone.id, opts.domain);
|
|
107
|
+
const mergedIncludes = new Set(["_spf.mx.cloudflare.net"]);
|
|
108
|
+
for (const inc of opts.extraSpfIncludes ?? [])
|
|
109
|
+
mergedIncludes.add(inc);
|
|
110
|
+
if (existingSpf) {
|
|
111
|
+
for (const inc of parseSpfIncludes(existingSpf.content))
|
|
112
|
+
mergedIncludes.add(inc);
|
|
113
|
+
}
|
|
114
|
+
const spfContent = buildSpfRecord({ includes: [...mergedIncludes] });
|
|
115
|
+
// Delete any *other* SPF TXT records first — exactly one SPF record
|
|
116
|
+
// per zone per RFC 7208. The upsert below patches `existingSpf` in
|
|
117
|
+
// place; stale duplicates (zone moved between providers, etc.) get
|
|
118
|
+
// removed here.
|
|
119
|
+
await deleteStaleSpfRecords(cf, zone.id, opts.domain, existingSpf?.id);
|
|
120
|
+
const spfRes = await cf.upsertRecord(zone.id, {
|
|
121
|
+
type: "TXT",
|
|
122
|
+
name: opts.domain,
|
|
123
|
+
content: spfContent,
|
|
124
|
+
});
|
|
125
|
+
dnsRecords.push({
|
|
126
|
+
id: spfRes.id,
|
|
127
|
+
name: opts.domain,
|
|
128
|
+
type: "TXT",
|
|
129
|
+
content: spfContent,
|
|
130
|
+
created: spfRes.created,
|
|
131
|
+
updated: spfRes.updated,
|
|
132
|
+
});
|
|
133
|
+
// Step 5 — DMARC.
|
|
134
|
+
const dmarcName = `_dmarc.${opts.domain}`;
|
|
135
|
+
const dmarcContent = buildDmarcRecord({
|
|
136
|
+
rua: opts.dmarcRua ?? `dmarc@${opts.domain}`,
|
|
137
|
+
policy: opts.dmarcPolicy ?? "quarantine",
|
|
138
|
+
});
|
|
139
|
+
const dmarcRes = await cf.upsertRecord(zone.id, {
|
|
140
|
+
type: "TXT",
|
|
141
|
+
name: dmarcName,
|
|
142
|
+
content: dmarcContent,
|
|
143
|
+
});
|
|
144
|
+
dnsRecords.push({
|
|
145
|
+
id: dmarcRes.id,
|
|
146
|
+
name: dmarcName,
|
|
147
|
+
type: "TXT",
|
|
148
|
+
content: dmarcContent,
|
|
149
|
+
created: dmarcRes.created,
|
|
150
|
+
updated: dmarcRes.updated,
|
|
151
|
+
});
|
|
152
|
+
// Step 6 — per-address forwarding rules.
|
|
153
|
+
const rules = [];
|
|
154
|
+
for (const localPart of opts.addresses) {
|
|
155
|
+
const address = `${localPart}@${opts.domain}`;
|
|
156
|
+
const res = await cf.upsertEmailRoutingRule(zone.id, {
|
|
157
|
+
address,
|
|
158
|
+
forwardTo: [opts.destination],
|
|
159
|
+
name: `Forward ${address}`,
|
|
160
|
+
});
|
|
161
|
+
rules.push({ address, id: res.id, created: res.created, updated: res.updated });
|
|
162
|
+
}
|
|
163
|
+
// Step 7 — catch-all (zone-level singleton). `changed` reflects the
|
|
164
|
+
// catch-all rule itself — its `enabled` flag and forward destination —
|
|
165
|
+
// not the zone-level Email Routing toggle (that's `routingEnabledThisRun`).
|
|
166
|
+
let catchAll;
|
|
167
|
+
if (opts.catchAll) {
|
|
168
|
+
const beforeRule = await cf.getEmailCatchAll(zone.id);
|
|
169
|
+
const beforeEnabled = beforeRule?.enabled ?? false;
|
|
170
|
+
const beforeForward = (beforeRule?.actions?.[0]?.value ?? []).join(",");
|
|
171
|
+
const wantForward = [opts.destination].join(",");
|
|
172
|
+
await cf.setEmailCatchAll(zone.id, {
|
|
173
|
+
forwardTo: [opts.destination],
|
|
174
|
+
enabled: true,
|
|
175
|
+
name: "Catch-all",
|
|
176
|
+
});
|
|
177
|
+
catchAll = { enabled: true, changed: !beforeEnabled || beforeForward !== wantForward };
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
domain: opts.domain,
|
|
181
|
+
zoneId: zone.id,
|
|
182
|
+
accountId,
|
|
183
|
+
routingEnabledThisRun,
|
|
184
|
+
destination: {
|
|
185
|
+
record: dest,
|
|
186
|
+
createdThisRun: !dest.existed,
|
|
187
|
+
verified: dest.verified ?? null,
|
|
188
|
+
},
|
|
189
|
+
dnsRecords,
|
|
190
|
+
rules,
|
|
191
|
+
catchAll,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** Find the apex SPF TXT record (one starting with `v=spf1`). Returns
|
|
195
|
+
* null when no SPF record exists yet. There MAY be other TXT records
|
|
196
|
+
* at the apex (verification tokens, etc.) — they're left alone. */
|
|
197
|
+
async function findApexSpf(cf, zoneId, domain) {
|
|
198
|
+
const all = await cf.findRecordsByName(zoneId, domain, "TXT");
|
|
199
|
+
return all.find((r) => /^"?v=spf1\b/i.test(r.content)) ?? null;
|
|
200
|
+
}
|
|
201
|
+
/** Delete every *other* SPF TXT at the apex except the one we're about
|
|
202
|
+
* to upsert. Multiple SPF records cause receivers to PermError per
|
|
203
|
+
* RFC 7208, so a clean zone has exactly one. Skips the record we're
|
|
204
|
+
* keeping (identified by id). */
|
|
205
|
+
async function deleteStaleSpfRecords(cf, zoneId, domain, keepId) {
|
|
206
|
+
const all = await cf.findRecordsByName(zoneId, domain, "TXT");
|
|
207
|
+
for (const rec of all) {
|
|
208
|
+
if (!/^"?v=spf1\b/i.test(rec.content))
|
|
209
|
+
continue;
|
|
210
|
+
if (rec.id === keepId)
|
|
211
|
+
continue;
|
|
212
|
+
await cf.deleteRecord(zoneId, rec.id);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Pretty-print the result for the CLI. Centralised here so both the
|
|
216
|
+
* standalone command and the create/adopt-flow hook print the same
|
|
217
|
+
* status block. */
|
|
218
|
+
export function printEmailSetupSummary(result) {
|
|
219
|
+
console.log(chalk.bold(`\n ── Email setup: ${result.domain} ──────────────────────────\n`));
|
|
220
|
+
if (result.routingEnabledThisRun) {
|
|
221
|
+
console.log(chalk.green(" ✓ Enabled Cloudflare Email Routing on zone"));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
console.log(chalk.dim(" · Email Routing already enabled"));
|
|
225
|
+
}
|
|
226
|
+
const verifySuffix = result.destination.verified === "active"
|
|
227
|
+
? chalk.green(" (verified)")
|
|
228
|
+
: chalk.yellow(" (pending — check inbox + click verify link)");
|
|
229
|
+
if (result.destination.createdThisRun) {
|
|
230
|
+
console.log(chalk.green(` ✓ Added destination ${result.destination.record.email}`) + verifySuffix);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.log(chalk.dim(` · Destination ${result.destination.record.email} already on account`) +
|
|
234
|
+
verifySuffix);
|
|
235
|
+
}
|
|
236
|
+
for (const rec of result.dnsRecords) {
|
|
237
|
+
const tag = rec.created
|
|
238
|
+
? chalk.green("✓ created")
|
|
239
|
+
: rec.updated
|
|
240
|
+
? chalk.yellow("· updated")
|
|
241
|
+
: chalk.dim("· unchanged");
|
|
242
|
+
console.log(` ${tag} ${rec.type.padEnd(4)} ${rec.name} → ${truncate(rec.content, 60)}`);
|
|
243
|
+
}
|
|
244
|
+
for (const r of result.rules) {
|
|
245
|
+
const tag = r.created
|
|
246
|
+
? chalk.green("✓ created")
|
|
247
|
+
: r.updated
|
|
248
|
+
? chalk.yellow("· updated")
|
|
249
|
+
: chalk.dim("· unchanged");
|
|
250
|
+
console.log(` ${tag} rule ${r.address}`);
|
|
251
|
+
}
|
|
252
|
+
if (result.catchAll) {
|
|
253
|
+
const tag = result.catchAll.changed ? chalk.green("✓ enabled") : chalk.dim("· already enabled");
|
|
254
|
+
console.log(` ${tag} catch-all *@${result.domain}`);
|
|
255
|
+
}
|
|
256
|
+
if (result.destination.verified !== "active") {
|
|
257
|
+
console.log(chalk.yellow(`\n ! Forwards won't deliver until ${result.destination.record.email} clicks the Cloudflare verification email.`));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function truncate(s, n) {
|
|
261
|
+
return s.length > n ? `${s.slice(0, n - 1)}…` : s;
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/email/setup.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Common SPF includes for popular senders. The Resend value is the
|
|
2
|
+
* one their dashboard tells you to add; AWS SES users would substitute
|
|
3
|
+
* `amazonses.com` etc. Hatchkit auto-includes Cloudflare's host; other
|
|
4
|
+
* senders come from configuration. */
|
|
5
|
+
export declare const SPF_INCLUDES: {
|
|
6
|
+
readonly cloudflareEmailRouting: "_spf.mx.cloudflare.net";
|
|
7
|
+
readonly resend: "_spf.resend.com";
|
|
8
|
+
readonly amazonSes: "amazonses.com";
|
|
9
|
+
readonly google: "_spf.google.com";
|
|
10
|
+
};
|
|
11
|
+
export type SpfQualifier = "~all" | "-all" | "?all" | "+all";
|
|
12
|
+
/** Build a single SPF TXT record content string. Sorts includes for
|
|
13
|
+
* deterministic output (matters for idempotent upserts: same input =
|
|
14
|
+
* same record content = no PATCH on re-run). */
|
|
15
|
+
export declare function buildSpfRecord(opts: {
|
|
16
|
+
/** Hostnames to wrap as `include:<host>`. Order-insensitive. */
|
|
17
|
+
includes: string[];
|
|
18
|
+
/** Optional `ip4:` / `ip6:` mechanisms. */
|
|
19
|
+
ip4?: string[];
|
|
20
|
+
ip6?: string[];
|
|
21
|
+
/** Default `~all` — see file header. */
|
|
22
|
+
qualifier?: SpfQualifier;
|
|
23
|
+
}): string;
|
|
24
|
+
/** Parse the includes out of an existing SPF record so we can union
|
|
25
|
+
* them with new ones (e.g. when adding Cloudflare to a zone that
|
|
26
|
+
* already has Resend). Lenient: returns an empty list for malformed
|
|
27
|
+
* input rather than throwing — the caller can decide whether to
|
|
28
|
+
* overwrite or surface the parse failure. */
|
|
29
|
+
export declare function parseSpfIncludes(record: string): string[];
|
|
30
|
+
/** Build a DMARC TXT record content string.
|
|
31
|
+
*
|
|
32
|
+
* Default policy is `p=quarantine` — failed-DMARC mail lands in spam
|
|
33
|
+
* rather than the inbox. `sp=none` deliberately exempts subdomains
|
|
34
|
+
* (e.g. `staging.<domain>`) because they often send via third-party
|
|
35
|
+
* services not aligned with the apex. `rua` is the aggregate-report
|
|
36
|
+
* destination — same address as the forwarding inbox by default, so
|
|
37
|
+
* the operator gets weekly delivery summaries.
|
|
38
|
+
*
|
|
39
|
+
* Tweaks (call sites can override):
|
|
40
|
+
* · `policy: "none"` — observe-only, no enforcement. Use during
|
|
41
|
+
* a soft rollout when you're not sure every legit sender is
|
|
42
|
+
* aligned yet.
|
|
43
|
+
* · `subdomainPolicy: "quarantine"` — extend enforcement to
|
|
44
|
+
* subdomains. Only safe once those subdomains' senders are known. */
|
|
45
|
+
export declare function buildDmarcRecord(opts: {
|
|
46
|
+
rua: string;
|
|
47
|
+
policy?: "none" | "quarantine" | "reject";
|
|
48
|
+
subdomainPolicy?: "none" | "quarantine" | "reject";
|
|
49
|
+
/** Percentage of mail subject to policy (0-100). Default 100. Drop
|
|
50
|
+
* to e.g. 20 during a soft rollout. */
|
|
51
|
+
percent?: number;
|
|
52
|
+
/** SPF/DKIM alignment mode — strict ("s") or relaxed ("r"). */
|
|
53
|
+
alignmentSpf?: "s" | "r";
|
|
54
|
+
alignmentDkim?: "s" | "r";
|
|
55
|
+
}): string;
|
|
56
|
+
//# sourceMappingURL=spf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spf.d.ts","sourceRoot":"","sources":["../../src/email/spf.ts"],"names":[],"mappings":"AAgCA;;;uCAGuC;AACvC,eAAO,MAAM,YAAY;;;;;CAKf,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE7D;;iDAEiD;AACjD,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,gEAAgE;IAChE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,2CAA2C;IAC3C,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,wCAAwC;IACxC,SAAS,CAAC,EAAE,YAAY,CAAC;CAC1B,GAAG,MAAM,CAUT;AAED;;;;8CAI8C;AAC9C,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CASzD;AAED;;;;;;;;;;;;;;yEAcyE;AACzE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,QAAQ,CAAC;IAC1C,eAAe,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,QAAQ,CAAC;IACnD;4CACwC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;IACzB,aAAa,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;CAC3B,GAAG,MAAM,CAWT"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPF + DMARC TXT-record helpers.
|
|
3
|
+
*
|
|
4
|
+
* SPF rules-of-the-road that make this small but important:
|
|
5
|
+
*
|
|
6
|
+
* 1. **One SPF TXT per domain.** RFC 7208 §3.2 — multiple records
|
|
7
|
+
* cause receivers to PermError. So if Resend's records already
|
|
8
|
+
* include `v=spf1 …` and Cloudflare Email Routing wants its own
|
|
9
|
+
* `v=spf1 …`, they MUST be merged into ONE record, not added as
|
|
10
|
+
* siblings.
|
|
11
|
+
*
|
|
12
|
+
* 2. **Maximum 10 DNS lookups inside the SPF chain.** Each `include:`
|
|
13
|
+
* counts as one lookup. Cloudflare Email Routing's
|
|
14
|
+
* `_spf.mx.cloudflare.net` and (e.g.) Resend's
|
|
15
|
+
* `_spf.resend.com` each cost one — well under the cap, but worth
|
|
16
|
+
* knowing if a user later piles on more senders.
|
|
17
|
+
*
|
|
18
|
+
* 3. **Order of `include:` mechanisms does NOT matter for the verdict,
|
|
19
|
+
* but a stable order makes hatchkit's idempotent upserts a no-op
|
|
20
|
+
* on re-run.** We sort the includes alphabetically before joining.
|
|
21
|
+
*
|
|
22
|
+
* 4. **Qualifier (`~all` vs `-all`):** we default to `~all` (softfail)
|
|
23
|
+
* — recipients see a soft signal that mail from unauthorised IPs
|
|
24
|
+
* is suspicious but they still accept it. This pairs well with
|
|
25
|
+
* DMARC at `p=quarantine`, which makes the final disposition call
|
|
26
|
+
* via alignment rather than SPF-fail-alone. Upgrade to `-all`
|
|
27
|
+
* (hardfail) once you're confident no legitimate sender is missing
|
|
28
|
+
* from the include list.
|
|
29
|
+
*/
|
|
30
|
+
const CLOUDFLARE_EMAIL_ROUTING_SPF_INCLUDE = "_spf.mx.cloudflare.net";
|
|
31
|
+
/** Common SPF includes for popular senders. The Resend value is the
|
|
32
|
+
* one their dashboard tells you to add; AWS SES users would substitute
|
|
33
|
+
* `amazonses.com` etc. Hatchkit auto-includes Cloudflare's host; other
|
|
34
|
+
* senders come from configuration. */
|
|
35
|
+
export const SPF_INCLUDES = {
|
|
36
|
+
cloudflareEmailRouting: CLOUDFLARE_EMAIL_ROUTING_SPF_INCLUDE,
|
|
37
|
+
resend: "_spf.resend.com",
|
|
38
|
+
amazonSes: "amazonses.com",
|
|
39
|
+
google: "_spf.google.com",
|
|
40
|
+
};
|
|
41
|
+
/** Build a single SPF TXT record content string. Sorts includes for
|
|
42
|
+
* deterministic output (matters for idempotent upserts: same input =
|
|
43
|
+
* same record content = no PATCH on re-run). */
|
|
44
|
+
export function buildSpfRecord(opts) {
|
|
45
|
+
const includes = [...new Set(opts.includes)].sort();
|
|
46
|
+
const ip4 = opts.ip4 ? [...new Set(opts.ip4)].sort() : [];
|
|
47
|
+
const ip6 = opts.ip6 ? [...new Set(opts.ip6)].sort() : [];
|
|
48
|
+
const parts = ["v=spf1"];
|
|
49
|
+
for (const i of ip4)
|
|
50
|
+
parts.push(`ip4:${i}`);
|
|
51
|
+
for (const i of ip6)
|
|
52
|
+
parts.push(`ip6:${i}`);
|
|
53
|
+
for (const inc of includes)
|
|
54
|
+
parts.push(`include:${inc}`);
|
|
55
|
+
parts.push(opts.qualifier ?? "~all");
|
|
56
|
+
return parts.join(" ");
|
|
57
|
+
}
|
|
58
|
+
/** Parse the includes out of an existing SPF record so we can union
|
|
59
|
+
* them with new ones (e.g. when adding Cloudflare to a zone that
|
|
60
|
+
* already has Resend). Lenient: returns an empty list for malformed
|
|
61
|
+
* input rather than throwing — the caller can decide whether to
|
|
62
|
+
* overwrite or surface the parse failure. */
|
|
63
|
+
export function parseSpfIncludes(record) {
|
|
64
|
+
const trimmed = record.trim().replace(/^"|"$/g, "").trim();
|
|
65
|
+
if (!/^v=spf1\b/i.test(trimmed))
|
|
66
|
+
return [];
|
|
67
|
+
const includes = [];
|
|
68
|
+
for (const part of trimmed.split(/\s+/)) {
|
|
69
|
+
const m = part.match(/^include:(.+)$/i);
|
|
70
|
+
if (m)
|
|
71
|
+
includes.push(m[1].toLowerCase());
|
|
72
|
+
}
|
|
73
|
+
return includes;
|
|
74
|
+
}
|
|
75
|
+
/** Build a DMARC TXT record content string.
|
|
76
|
+
*
|
|
77
|
+
* Default policy is `p=quarantine` — failed-DMARC mail lands in spam
|
|
78
|
+
* rather than the inbox. `sp=none` deliberately exempts subdomains
|
|
79
|
+
* (e.g. `staging.<domain>`) because they often send via third-party
|
|
80
|
+
* services not aligned with the apex. `rua` is the aggregate-report
|
|
81
|
+
* destination — same address as the forwarding inbox by default, so
|
|
82
|
+
* the operator gets weekly delivery summaries.
|
|
83
|
+
*
|
|
84
|
+
* Tweaks (call sites can override):
|
|
85
|
+
* · `policy: "none"` — observe-only, no enforcement. Use during
|
|
86
|
+
* a soft rollout when you're not sure every legit sender is
|
|
87
|
+
* aligned yet.
|
|
88
|
+
* · `subdomainPolicy: "quarantine"` — extend enforcement to
|
|
89
|
+
* subdomains. Only safe once those subdomains' senders are known. */
|
|
90
|
+
export function buildDmarcRecord(opts) {
|
|
91
|
+
const parts = [
|
|
92
|
+
"v=DMARC1",
|
|
93
|
+
`p=${opts.policy ?? "quarantine"}`,
|
|
94
|
+
`sp=${opts.subdomainPolicy ?? "none"}`,
|
|
95
|
+
`rua=mailto:${opts.rua}`,
|
|
96
|
+
`pct=${opts.percent ?? 100}`,
|
|
97
|
+
`adkim=${opts.alignmentDkim ?? "r"}`,
|
|
98
|
+
`aspf=${opts.alignmentSpf ?? "r"}`,
|
|
99
|
+
];
|
|
100
|
+
return parts.join("; ");
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=spf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spf.js","sourceRoot":"","sources":["../../src/email/spf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,oCAAoC,GAAG,wBAAwB,CAAC;AAEtE;;;uCAGuC;AACvC,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,sBAAsB,EAAE,oCAAoC;IAC5D,MAAM,EAAE,iBAAiB;IACzB,SAAS,EAAE,eAAe;IAC1B,MAAM,EAAE,iBAAiB;CACjB,CAAC;AAIX;;iDAEiD;AACjD,MAAM,UAAU,cAAc,CAAC,IAQ9B;IACC,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACpD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1D,MAAM,KAAK,GAAG,CAAC,QAAQ,CAAC,CAAC;IACzB,KAAK,MAAM,CAAC,IAAI,GAAG;QAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC5C,KAAK,MAAM,CAAC,IAAI,GAAG;QAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC5C,KAAK,MAAM,GAAG,IAAI,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC,CAAC;IACzD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC,CAAC;IACrC,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED;;;;8CAI8C;AAC9C,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3D,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IAC3C,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACxC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;QACxC,IAAI,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;;;;yEAcyE;AACzE,MAAM,UAAU,gBAAgB,CAAC,IAUhC;IACC,MAAM,KAAK,GAAa;QACtB,UAAU;QACV,KAAK,IAAI,CAAC,MAAM,IAAI,YAAY,EAAE;QAClC,MAAM,IAAI,CAAC,eAAe,IAAI,MAAM,EAAE;QACtC,cAAc,IAAI,CAAC,GAAG,EAAE;QACxB,OAAO,IAAI,CAAC,OAAO,IAAI,GAAG,EAAE;QAC5B,SAAS,IAAI,CAAC,aAAa,IAAI,GAAG,EAAE;QACpC,QAAQ,IAAI,CAAC,YAAY,IAAI,GAAG,EAAE;KACnC,CAAC;IACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
|