@oxygen-agent/cli 1.1014.1138 → 1.1014.1219
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/README.md +1 -1
- package/dist/command-manifest.js +2 -0
- package/dist/command-support.d.ts +8 -0
- package/dist/command-support.js +11 -0
- package/dist/commands/api-keys.d.ts +9 -0
- package/dist/commands/api-keys.js +4 -1
- package/dist/commands/capabilities.js +4 -2
- package/dist/commands/crm.js +28 -2
- package/dist/commands/dashboards.js +2 -2
- package/dist/commands/db.js +34 -0
- package/dist/commands/find.js +1 -1
- package/dist/commands/knowledge.js +2 -0
- package/dist/commands/mailboxes.js +3 -2
- package/dist/commands/messages.js +6 -1
- package/dist/commands/senders.js +2 -0
- package/dist/commands/sequences.js +1 -1
- package/dist/commands/signals.js +11 -2
- package/dist/deliverability-evidence-commands.d.ts +4 -0
- package/dist/deliverability-evidence-commands.js +104 -0
- package/dist/diagnostic-context.d.ts +21 -0
- package/dist/diagnostic-context.js +25 -0
- package/dist/diagnostics.d.ts +1 -3
- package/dist/diagnostics.js +12 -30
- package/dist/http-client.js +1 -1
- package/dist/network-mining-host.d.ts +6 -0
- package/dist/network-mining-host.js +10 -0
- package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +1 -1
- package/node_modules/@oxygen/formula/dist/value-normalizers.js +5 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +169 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +17 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +114 -5
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/operation-request.d.ts +92 -0
- package/node_modules/@oxygen/shared/dist/contracts/operation-request.js +36 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +3 -90
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +5 -39
- package/node_modules/@oxygen/shared/dist/contracts/reply-oxygent.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/reply-oxygent.js +123 -0
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +1 -0
- package/node_modules/@oxygen/shared/dist/crm-activity-events.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/crm-activity-events.js +1 -0
- package/node_modules/@oxygen/shared/dist/crm-record-brief.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/crm-record-brief.js +22 -0
- package/node_modules/@oxygen/shared/dist/deliverability-blacklist.d.ts +74 -0
- package/node_modules/@oxygen/shared/dist/deliverability-blacklist.js +118 -0
- package/node_modules/@oxygen/shared/dist/deliverability-checks.d.ts +138 -0
- package/node_modules/@oxygen/shared/dist/deliverability-checks.js +124 -0
- package/node_modules/@oxygen/shared/dist/deliverability-evidence.d.ts +366 -0
- package/node_modules/@oxygen/shared/dist/deliverability-evidence.js +169 -0
- package/node_modules/@oxygen/shared/dist/deliverability-loop.d.ts +334 -0
- package/node_modules/@oxygen/shared/dist/deliverability-loop.js +312 -0
- package/node_modules/@oxygen/shared/dist/deliverability-surface.d.ts +42 -0
- package/node_modules/@oxygen/shared/dist/deliverability-surface.js +66 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +34 -4
- package/node_modules/@oxygen/shared/dist/domain-authentication.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/domain-authentication.js +20 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +11 -3
- package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.js +30 -2
- package/node_modules/@oxygen/shared/dist/network-mining.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/network-mining.js +9 -0
- package/node_modules/@oxygen/shared/dist/received-authentication.d.ts +74 -0
- package/node_modules/@oxygen/shared/dist/received-authentication.js +452 -0
- package/node_modules/@oxygen/shared/dist/recipient-provider.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/recipient-provider.js +33 -0
- package/node_modules/@oxygen/shared/dist/reply-oxygent.d.ts +73 -0
- package/node_modules/@oxygen/shared/dist/reply-oxygent.js +31 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +10 -1
- package/node_modules/@oxygen/shared/dist/repricing.js +16 -4
- package/node_modules/@oxygen/shared/dist/research-findings.d.ts +80 -0
- package/node_modules/@oxygen/shared/dist/research-findings.js +199 -0
- package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.js +4 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +8 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +20 -3
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.d.ts +8 -2
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +63 -0
- package/package.json +1 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `refused` (the operator answered with an error code) and `error` (no usable answer) are
|
|
3
|
+
* unknown evidence. Only NXDOMAIN from the operator's zone proves `not_listed`.
|
|
4
|
+
*/
|
|
5
|
+
export const DNSBL_VERDICTS = ["listed", "not_listed", "refused", "error"];
|
|
6
|
+
export const BLACKLIST_CHECK_PREFIX = "blacklist.";
|
|
7
|
+
const FINDING_STATUS = { listed: "fail", not_listed: "pass", refused: "unknown", error: "unknown" };
|
|
8
|
+
/** One typed finding per queried zone. */
|
|
9
|
+
export function blacklistFindings(results) {
|
|
10
|
+
return results.map((result) => ({
|
|
11
|
+
check: `${BLACKLIST_CHECK_PREFIX}${result.label}`,
|
|
12
|
+
status: FINDING_STATUS[result.verdict],
|
|
13
|
+
severity: "high",
|
|
14
|
+
summary: result.verdict === "listed" ? `Listed on ${result.zone}.`
|
|
15
|
+
: result.verdict === "not_listed" ? `Not listed on ${result.zone}.`
|
|
16
|
+
: `${result.zone} gave no usable answer; listing state unknown.`,
|
|
17
|
+
value: { zone: result.zone, verdict: result.verdict, codes: result.codes, error: result.error },
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Why a blacklist run queried no zone at all. Each is recorded as an `unknown` run, so
|
|
22
|
+
* "checked but nothing could answer" is never confused with "never checked" or "clean".
|
|
23
|
+
*/
|
|
24
|
+
export const BLACKLIST_UNAVAILABLE_REASONS = [
|
|
25
|
+
"dnsbl_no_queryable_zone",
|
|
26
|
+
"dnsbl_resolver_unconfigured",
|
|
27
|
+
"dnsbl_resolver_invalid",
|
|
28
|
+
"dnsbl_public_resolver_refused",
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* succeeded when every zone answered, partial when some did, failed when none did, and
|
|
32
|
+
* unknown when no zone was queried. A failure shared by every unanswered zone (for
|
|
33
|
+
* example dnsbl_resolver_unavailable) is reported as itself.
|
|
34
|
+
*/
|
|
35
|
+
export function blacklistRunOutcome(results, unavailable = "dnsbl_no_queryable_zone") {
|
|
36
|
+
if (!results.length)
|
|
37
|
+
return { status: "unknown", errorCode: unavailable };
|
|
38
|
+
const unanswered = results.filter((r) => r.verdict !== "listed" && r.verdict !== "not_listed");
|
|
39
|
+
if (!unanswered.length)
|
|
40
|
+
return { status: "succeeded", errorCode: null };
|
|
41
|
+
const causes = new Set(unanswered.map((r) => r.error));
|
|
42
|
+
const [cause] = causes;
|
|
43
|
+
return {
|
|
44
|
+
status: unanswered.length < results.length ? "partial" : "failed",
|
|
45
|
+
errorCode: causes.size === 1 && cause ? cause : "dnsbl_unanswered",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Listing starts and ends against the last KNOWN state per zone. An unknown answer
|
|
50
|
+
* changes nothing, so a refused query between two listed answers never re-fires.
|
|
51
|
+
* A listing seen on the first answered check of a subject is a start.
|
|
52
|
+
*/
|
|
53
|
+
export function blacklistListingTransitions(lastKnown, current) {
|
|
54
|
+
const transitions = [];
|
|
55
|
+
for (const finding of current) {
|
|
56
|
+
if (!finding.check.startsWith(BLACKLIST_CHECK_PREFIX))
|
|
57
|
+
continue;
|
|
58
|
+
const previous = lastKnown.get(finding.check);
|
|
59
|
+
const value = (finding.value ?? {});
|
|
60
|
+
const base = {
|
|
61
|
+
check: finding.check,
|
|
62
|
+
zone: typeof value.zone === "string" ? value.zone : finding.check.slice(BLACKLIST_CHECK_PREFIX.length),
|
|
63
|
+
codes: Array.isArray(value.codes) ? value.codes.filter((c) => typeof c === "string") : [],
|
|
64
|
+
};
|
|
65
|
+
if (finding.status === "fail" && previous !== "fail")
|
|
66
|
+
transitions.push({ ...base, transition: "listed" });
|
|
67
|
+
if (finding.status === "pass" && previous === "fail")
|
|
68
|
+
transitions.push({ ...base, transition: "delisted" });
|
|
69
|
+
}
|
|
70
|
+
return transitions;
|
|
71
|
+
}
|
|
72
|
+
/** Workflow-subscribable event for listing starts and ends (workspace-event-catalog). */
|
|
73
|
+
export const DELIVERABILITY_WORKFLOW_EVENT_SOURCE = "deliverability";
|
|
74
|
+
export const BLACKLIST_LISTING_CHANGED_EVENT = "blacklist_listing_changed";
|
|
75
|
+
export const BLACKLIST_LISTING_CHANGED_PAYLOAD_FIELDS = [
|
|
76
|
+
"transition",
|
|
77
|
+
"subject_type",
|
|
78
|
+
"subject",
|
|
79
|
+
"zone",
|
|
80
|
+
"check",
|
|
81
|
+
"codes",
|
|
82
|
+
"run_id",
|
|
83
|
+
"occurred_at",
|
|
84
|
+
];
|
|
85
|
+
/**
|
|
86
|
+
* Link hosts recorded by a content-score finding (`content.links`). Accepts
|
|
87
|
+
* `{ domains: string[] }` or `{ links: Array<string | { url?, domain? }> }`; anything
|
|
88
|
+
* else yields nothing.
|
|
89
|
+
*/
|
|
90
|
+
export function linkDomainsFromFindingValue(value) {
|
|
91
|
+
if (!value || typeof value !== "object")
|
|
92
|
+
return [];
|
|
93
|
+
const record = value;
|
|
94
|
+
const raw = [
|
|
95
|
+
...(Array.isArray(record.domains) ? record.domains : []),
|
|
96
|
+
...(Array.isArray(record.links) ? record.links : []),
|
|
97
|
+
];
|
|
98
|
+
const hosts = new Set();
|
|
99
|
+
for (const entry of raw) {
|
|
100
|
+
const candidate = typeof entry === "string" ? entry
|
|
101
|
+
: entry && typeof entry === "object" ? (entry.domain ?? entry.url) : null;
|
|
102
|
+
if (typeof candidate !== "string" || !candidate.trim())
|
|
103
|
+
continue;
|
|
104
|
+
let host = candidate.trim().toLowerCase();
|
|
105
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//.test(host)) {
|
|
106
|
+
try {
|
|
107
|
+
host = new URL(host).hostname;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
host = host.replace(/\.$/, "");
|
|
114
|
+
if (/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(host) && !/^[0-9.]+$/.test(host))
|
|
115
|
+
hosts.add(host);
|
|
116
|
+
}
|
|
117
|
+
return [...hosts];
|
|
118
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { DomainAuthenticationObservation, DomainBimiObservation } from "./domain-authentication.js";
|
|
2
|
+
/** Append-only deliverability evidence: one run per check execution, typed findings, one observation per probe. */
|
|
3
|
+
export declare const DELIVERABILITY_CHECK_KINDS: readonly ["dns_auth", "bimi", "tracking_cname", "blacklist", "content_score", "header_proof", "placement"];
|
|
4
|
+
export type DeliverabilityCheckKind = (typeof DELIVERABILITY_CHECK_KINDS)[number];
|
|
5
|
+
export declare const DELIVERABILITY_SUBJECT_TYPES: readonly ["domain", "mailbox", "copy", "placement_run"];
|
|
6
|
+
export type DeliverabilitySubjectType = (typeof DELIVERABILITY_SUBJECT_TYPES)[number];
|
|
7
|
+
export declare const DELIVERABILITY_TRIGGERS: readonly ["schedule", "preview", "manual", "workflow", "agent", "backfill"];
|
|
8
|
+
export type DeliverabilityTrigger = (typeof DELIVERABILITY_TRIGGERS)[number];
|
|
9
|
+
export declare const DELIVERABILITY_RUN_STATUSES: readonly ["succeeded", "partial", "failed", "unknown"];
|
|
10
|
+
export type DeliverabilityRunStatus = (typeof DELIVERABILITY_RUN_STATUSES)[number];
|
|
11
|
+
export declare const DELIVERABILITY_FINDING_STATUSES: readonly ["pass", "warn", "fail", "unknown"];
|
|
12
|
+
export type DeliverabilityFindingStatus = (typeof DELIVERABILITY_FINDING_STATUSES)[number];
|
|
13
|
+
export type DeliverabilityFindingSeverity = "info" | "low" | "medium" | "high";
|
|
14
|
+
export declare const PLACEMENT_OBSERVATION_SOURCES: readonly ["workspace", "network", "emailguard", "zapmail", "sendkit"];
|
|
15
|
+
export type PlacementObservationSource = (typeof PLACEMENT_OBSERVATION_SOURCES)[number];
|
|
16
|
+
export declare const PLACEMENT_FOLDERS: readonly ["inbox", "spam", "other", "not_observed", "unknown"];
|
|
17
|
+
export type PlacementFolder = (typeof PLACEMENT_FOLDERS)[number];
|
|
18
|
+
export declare const PLACEMENT_TABS: readonly ["primary", "promotions", "social", "updates", "forums", "focused", "other", "unknown"];
|
|
19
|
+
export type PlacementTab = (typeof PLACEMENT_TABS)[number];
|
|
20
|
+
export type PlacementReceiverProvider = "google" | "microsoft" | "yahoo" | "other" | "unknown";
|
|
21
|
+
/** Stable check codes: lowercase dotted segments; the last segment may name a zone, selector or rule. */
|
|
22
|
+
export declare const DELIVERABILITY_CHECK_CODE: RegExp;
|
|
23
|
+
export declare const DELIVERABILITY_RAW_MAX_BYTES = 65536;
|
|
24
|
+
/** History older than this is pruned; the current-state projections are unaffected. */
|
|
25
|
+
export declare const DELIVERABILITY_RETENTION_DAYS = 400;
|
|
26
|
+
export type DeliverabilityFinding = {
|
|
27
|
+
check: string;
|
|
28
|
+
status: DeliverabilityFindingStatus;
|
|
29
|
+
severity: DeliverabilityFindingSeverity;
|
|
30
|
+
summary?: string | null;
|
|
31
|
+
value?: unknown;
|
|
32
|
+
};
|
|
33
|
+
export type DeliverabilityCheckRunInput = {
|
|
34
|
+
/** Retries of the same execution converge on one run. */
|
|
35
|
+
idempotencyKey: string;
|
|
36
|
+
kind: DeliverabilityCheckKind;
|
|
37
|
+
subjectType: DeliverabilitySubjectType;
|
|
38
|
+
subject: string;
|
|
39
|
+
trigger: DeliverabilityTrigger;
|
|
40
|
+
sourceRef?: string | null;
|
|
41
|
+
engine: string;
|
|
42
|
+
engineVersion?: string | null;
|
|
43
|
+
status: DeliverabilityRunStatus;
|
|
44
|
+
errorCode?: string | null;
|
|
45
|
+
credits?: number;
|
|
46
|
+
actorId?: string | null;
|
|
47
|
+
raw?: unknown;
|
|
48
|
+
startedAt: Date;
|
|
49
|
+
finishedAt?: Date | null;
|
|
50
|
+
findings: DeliverabilityFinding[];
|
|
51
|
+
};
|
|
52
|
+
export type DeliverabilityCheckRun = {
|
|
53
|
+
id: string;
|
|
54
|
+
kind: DeliverabilityCheckKind;
|
|
55
|
+
subjectType: DeliverabilitySubjectType;
|
|
56
|
+
subject: string;
|
|
57
|
+
trigger: DeliverabilityTrigger;
|
|
58
|
+
sourceRef: string | null;
|
|
59
|
+
engine: string;
|
|
60
|
+
engineVersion: string | null;
|
|
61
|
+
status: DeliverabilityRunStatus;
|
|
62
|
+
errorCode: string | null;
|
|
63
|
+
credits: number;
|
|
64
|
+
startedAt: string;
|
|
65
|
+
finishedAt: string | null;
|
|
66
|
+
findingCounts: Record<DeliverabilityFindingStatus, number>;
|
|
67
|
+
findings?: Array<DeliverabilityFinding & {
|
|
68
|
+
observedAt: string;
|
|
69
|
+
}>;
|
|
70
|
+
raw?: unknown;
|
|
71
|
+
};
|
|
72
|
+
export type PlacementObservation = {
|
|
73
|
+
id: string;
|
|
74
|
+
source: PlacementObservationSource;
|
|
75
|
+
runRef: string;
|
|
76
|
+
probeRef: string;
|
|
77
|
+
senderMailboxId: string | null;
|
|
78
|
+
senderAddress: string | null;
|
|
79
|
+
senderDomain: string | null;
|
|
80
|
+
receiverLabel: string;
|
|
81
|
+
receiverProvider: PlacementReceiverProvider;
|
|
82
|
+
receiverGateway: string | null;
|
|
83
|
+
outcome: "observed" | "not_observed" | "send_failed" | "send_unknown" | "read_blocked";
|
|
84
|
+
folder: PlacementFolder;
|
|
85
|
+
tab: PlacementTab;
|
|
86
|
+
spf: string | null;
|
|
87
|
+
dkim: string | null;
|
|
88
|
+
dmarc: string | null;
|
|
89
|
+
dkimDomain: string | null;
|
|
90
|
+
dkimSelector: string | null;
|
|
91
|
+
aligned: boolean | null;
|
|
92
|
+
sendingIp: string | null;
|
|
93
|
+
variantKey: string | null;
|
|
94
|
+
contentHash: string | null;
|
|
95
|
+
contentScore: number | null;
|
|
96
|
+
sentAt: string | null;
|
|
97
|
+
observedAt: string | null;
|
|
98
|
+
recordedAt: string;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* One content identity for copy across tests and sources. The tenant migration backfill computes the same
|
|
102
|
+
* digest in SQL (`sha256(subject || E'\n\n' || body_text || E'\n\n' || coalesce(body_html,''))`).
|
|
103
|
+
*/
|
|
104
|
+
export declare function deliverabilityContentHash(copy: {
|
|
105
|
+
subject: string;
|
|
106
|
+
body_text: string;
|
|
107
|
+
body_html?: string | null;
|
|
108
|
+
}): string;
|
|
109
|
+
/** Typed findings from one public-DNS observation; a failed lookup yields one unknown finding. */
|
|
110
|
+
export declare function domainAuthenticationFindings(observation: DomainAuthenticationObservation | null): DeliverabilityFinding[];
|
|
111
|
+
/** BIMI is optional: absence warns, never fails; a published record is held to what receivers require. */
|
|
112
|
+
export declare function bimiFindings(bimi: DomainBimiObservation): DeliverabilityFinding[];
|
|
113
|
+
export declare const TRACKING_DOMAIN_STATUSES: readonly ["pending", "verified", "failed"];
|
|
114
|
+
export type TrackingDomainVerificationStatus = (typeof TRACKING_DOMAIN_STATUSES)[number];
|
|
115
|
+
/**
|
|
116
|
+
* One verification of a tracking host: its public CNAME, and whether the open-pixel endpoint answers
|
|
117
|
+
* through it. Link rewriting starts once a host is verified, so a host that resolves but does not
|
|
118
|
+
* serve the endpoint must never verify: every tracked link would break.
|
|
119
|
+
*/
|
|
120
|
+
export type TrackingCnameObservation = {
|
|
121
|
+
host: string;
|
|
122
|
+
expectedTarget: string;
|
|
123
|
+
/** The CNAME public DNS returned for the host; null when it has none. */
|
|
124
|
+
actualTarget: string | null;
|
|
125
|
+
cname: "match" | "mismatch" | "absent" | "unknown";
|
|
126
|
+
/** Probed only when the CNAME matches. */
|
|
127
|
+
https: {
|
|
128
|
+
state: "pass" | "fail" | "unknown" | "skipped";
|
|
129
|
+
httpStatus: number | null;
|
|
130
|
+
contentType: string | null;
|
|
131
|
+
};
|
|
132
|
+
checkedAt: string;
|
|
133
|
+
};
|
|
134
|
+
/** Null keeps the current status: a failed lookup or an unanswered probe is not evidence. */
|
|
135
|
+
export declare function nextTrackingDomainStatus(current: TrackingDomainVerificationStatus, observation: TrackingCnameObservation): TrackingDomainVerificationStatus | null;
|
|
136
|
+
export declare function trackingCnameFindings(observation: TrackingCnameObservation): DeliverabilityFinding[];
|
|
137
|
+
/** Bounded raw evidence: oversized payloads keep a marker instead of failing the write. */
|
|
138
|
+
export declare function boundedDeliverabilityRaw(raw: unknown): unknown;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** Append-only deliverability evidence: one run per check execution, typed findings, one observation per probe. */
|
|
3
|
+
export const DELIVERABILITY_CHECK_KINDS = ["dns_auth", "bimi", "tracking_cname", "blacklist", "content_score", "header_proof", "placement"];
|
|
4
|
+
export const DELIVERABILITY_SUBJECT_TYPES = ["domain", "mailbox", "copy", "placement_run"];
|
|
5
|
+
export const DELIVERABILITY_TRIGGERS = ["schedule", "preview", "manual", "workflow", "agent", "backfill"];
|
|
6
|
+
export const DELIVERABILITY_RUN_STATUSES = ["succeeded", "partial", "failed", "unknown"];
|
|
7
|
+
export const DELIVERABILITY_FINDING_STATUSES = ["pass", "warn", "fail", "unknown"];
|
|
8
|
+
export const PLACEMENT_OBSERVATION_SOURCES = ["workspace", "network", "emailguard", "zapmail", "sendkit"];
|
|
9
|
+
export const PLACEMENT_FOLDERS = ["inbox", "spam", "other", "not_observed", "unknown"];
|
|
10
|
+
export const PLACEMENT_TABS = ["primary", "promotions", "social", "updates", "forums", "focused", "other", "unknown"];
|
|
11
|
+
/** Stable check codes: lowercase dotted segments; the last segment may name a zone, selector or rule. */
|
|
12
|
+
export const DELIVERABILITY_CHECK_CODE = /^[a-z0-9_]+(\.[A-Za-z0-9_-]+)*$/;
|
|
13
|
+
export const DELIVERABILITY_RAW_MAX_BYTES = 65_536;
|
|
14
|
+
/** History older than this is pruned; the current-state projections are unaffected. */
|
|
15
|
+
export const DELIVERABILITY_RETENTION_DAYS = 400;
|
|
16
|
+
/**
|
|
17
|
+
* One content identity for copy across tests and sources. The tenant migration backfill computes the same
|
|
18
|
+
* digest in SQL (`sha256(subject || E'\n\n' || body_text || E'\n\n' || coalesce(body_html,''))`).
|
|
19
|
+
*/
|
|
20
|
+
export function deliverabilityContentHash(copy) {
|
|
21
|
+
return createHash("sha256").update(`${copy.subject}\n\n${copy.body_text}\n\n${copy.body_html ?? ""}`, "utf8").digest("hex");
|
|
22
|
+
}
|
|
23
|
+
const DNS_STATUS = { configured: "pass", issues: "fail", unknown: "unknown" };
|
|
24
|
+
const DNS_SEVERITY = { spf: "high", dkim: "high", dmarc: "high", mx: "medium", nameservers: "low" };
|
|
25
|
+
/** Typed findings from one public-DNS observation; a failed lookup yields one unknown finding. */
|
|
26
|
+
export function domainAuthenticationFindings(observation) {
|
|
27
|
+
if (!observation)
|
|
28
|
+
return [{ check: "dns_auth.lookup", status: "unknown", severity: "medium", summary: "Public DNS could not be read." }];
|
|
29
|
+
const findings = Object.entries(observation.checks ?? {}).map(([name, check]) => ({
|
|
30
|
+
check: `dns_auth.${name}`,
|
|
31
|
+
status: DNS_STATUS[check.state] ?? "unknown",
|
|
32
|
+
severity: DNS_SEVERITY[name] ?? "low",
|
|
33
|
+
summary: check.summary,
|
|
34
|
+
value: { records: check.records, next_action: check.nextAction },
|
|
35
|
+
}));
|
|
36
|
+
findings.push({ check: "dns_auth.dmarc_policy", status: observation.dmarcPolicy == null ? "unknown" : observation.dmarcPolicy === "none" ? "warn" : "pass",
|
|
37
|
+
severity: "medium", value: { policy: observation.dmarcPolicy ?? null, policy_domain: observation.dmarcPolicyDomain ?? null } });
|
|
38
|
+
findings.push({ check: "dns_auth.spf_lookup_budget",
|
|
39
|
+
status: !observation.spfLookupComplete || observation.spfLookupTerms == null ? "unknown" : observation.spfLookupTerms > 10 ? "fail" : observation.spfLookupTerms > 8 ? "warn" : "pass",
|
|
40
|
+
severity: "high", value: { lookup_terms: observation.spfLookupTerms ?? null, complete: observation.spfLookupComplete ?? false } });
|
|
41
|
+
for (const selector of observation.dkimSelectors ?? [])
|
|
42
|
+
findings.push({ check: `dns_auth.dkim_selector.${selector.replace(/[^A-Za-z0-9_-]/g, "_")}`, status: "pass", severity: "info", value: { selector } });
|
|
43
|
+
return findings;
|
|
44
|
+
}
|
|
45
|
+
const isHttps = (url) => {
|
|
46
|
+
try {
|
|
47
|
+
return new URL(url).protocol === "https:";
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
/** BIMI is optional: absence warns, never fails; a published record is held to what receivers require. */
|
|
54
|
+
export function bimiFindings(bimi) {
|
|
55
|
+
const value = { domain: bimi.domain ?? null, record: bimi.record, logo_url: bimi.logoUrl, authority_url: bimi.authorityUrl, declined: bimi.declined };
|
|
56
|
+
if (bimi.state === "unknown")
|
|
57
|
+
return [{ check: "bimi.record", status: "unknown", severity: "info", summary: "The BIMI record could not be read.", value }];
|
|
58
|
+
if (bimi.state === "absent") {
|
|
59
|
+
return [{ check: "bimi.record", status: "warn", severity: "info", summary: "No BIMI record is published for this domain or its organizational domain.", value }];
|
|
60
|
+
}
|
|
61
|
+
if (bimi.state === "invalid") {
|
|
62
|
+
return [{ check: "bimi.record", status: "fail", severity: "low", summary: "More than one BIMI record is published; receivers ignore them all.", value }];
|
|
63
|
+
}
|
|
64
|
+
if (bimi.declined)
|
|
65
|
+
return [{ check: "bimi.record", status: "pass", severity: "info", summary: "The BIMI record declines a logo.", value }];
|
|
66
|
+
const logo = bimi.logoUrl;
|
|
67
|
+
const authority = bimi.authorityUrl;
|
|
68
|
+
return [
|
|
69
|
+
{ check: "bimi.record", status: "pass", severity: "info", summary: "A BIMI record is published.", value },
|
|
70
|
+
!logo
|
|
71
|
+
? { check: "bimi.logo_url", status: "warn", severity: "low", summary: "The BIMI record names no logo.", value: { logo_url: null } }
|
|
72
|
+
: !isHttps(logo)
|
|
73
|
+
? { check: "bimi.logo_url", status: "fail", severity: "low", summary: "The BIMI logo must be served over HTTPS.", value: { logo_url: logo } }
|
|
74
|
+
: !/\.svg$/i.test(new URL(logo).pathname)
|
|
75
|
+
? { check: "bimi.logo_url", status: "warn", severity: "low", summary: "The BIMI logo should be an SVG Tiny PS file.", value: { logo_url: logo } }
|
|
76
|
+
: { check: "bimi.logo_url", status: "pass", severity: "low", summary: "The BIMI logo is an HTTPS SVG.", value: { logo_url: logo } },
|
|
77
|
+
!authority
|
|
78
|
+
? { check: "bimi.vmc", status: "warn", severity: "low", summary: "No mark certificate is published; Gmail and Apple Mail show no logo without one.", value: { authority_url: null } }
|
|
79
|
+
: !isHttps(authority)
|
|
80
|
+
? { check: "bimi.vmc", status: "fail", severity: "low", summary: "The mark certificate must be served over HTTPS.", value: { authority_url: authority } }
|
|
81
|
+
: { check: "bimi.vmc", status: "pass", severity: "low", summary: "A mark certificate is published; its validity is not checked.", value: { authority_url: authority } },
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
export const TRACKING_DOMAIN_STATUSES = ["pending", "verified", "failed"];
|
|
85
|
+
/** Null keeps the current status: a failed lookup or an unanswered probe is not evidence. */
|
|
86
|
+
export function nextTrackingDomainStatus(current, observation) {
|
|
87
|
+
if (observation.cname === "unknown")
|
|
88
|
+
return null;
|
|
89
|
+
if (observation.cname !== "match")
|
|
90
|
+
return "failed";
|
|
91
|
+
if (observation.https.state === "pass")
|
|
92
|
+
return "verified";
|
|
93
|
+
if (observation.https.state === "fail")
|
|
94
|
+
return current === "verified" ? "failed" : "pending";
|
|
95
|
+
return current === "failed" ? "pending" : null;
|
|
96
|
+
}
|
|
97
|
+
export function trackingCnameFindings(observation) {
|
|
98
|
+
const target = { expected: observation.expectedTarget, actual: observation.actualTarget };
|
|
99
|
+
const findings = [
|
|
100
|
+
observation.cname === "unknown"
|
|
101
|
+
? { check: "tracking_cname.target", status: "unknown", severity: "high", summary: "The tracking CNAME could not be read.", value: target }
|
|
102
|
+
: observation.cname === "match"
|
|
103
|
+
? { check: "tracking_cname.target", status: "pass", severity: "high", summary: "The tracking CNAME points at the tracking service.", value: target }
|
|
104
|
+
: { check: "tracking_cname.target", status: "fail", severity: "high",
|
|
105
|
+
summary: observation.cname === "absent" ? "The tracking host has no CNAME record." : "The tracking CNAME points somewhere else.", value: target },
|
|
106
|
+
];
|
|
107
|
+
const https = observation.https;
|
|
108
|
+
if (https.state !== "skipped") {
|
|
109
|
+
const value = { http_status: https.httpStatus, content_type: https.contentType };
|
|
110
|
+
findings.push(https.state === "pass"
|
|
111
|
+
? { check: "tracking_cname.https", status: "pass", severity: "high", summary: "The tracking endpoint answers over HTTPS on this host.", value }
|
|
112
|
+
: https.state === "fail"
|
|
113
|
+
? { check: "tracking_cname.https", status: "fail", severity: "high", summary: "The tracking endpoint does not answer over HTTPS on this host yet.", value }
|
|
114
|
+
: { check: "tracking_cname.https", status: "unknown", severity: "high", summary: "The tracking endpoint gave no conclusive answer (no response, or a server error).", value });
|
|
115
|
+
}
|
|
116
|
+
return findings;
|
|
117
|
+
}
|
|
118
|
+
/** Bounded raw evidence: oversized payloads keep a marker instead of failing the write. */
|
|
119
|
+
export function boundedDeliverabilityRaw(raw) {
|
|
120
|
+
if (raw === undefined || raw === null)
|
|
121
|
+
return null;
|
|
122
|
+
const text = JSON.stringify(raw);
|
|
123
|
+
return Buffer.byteLength(text, "utf8") <= DELIVERABILITY_RAW_MAX_BYTES ? raw : { truncated: true, bytes: Buffer.byteLength(text, "utf8") };
|
|
124
|
+
}
|