@konfeature/ap-email-guessr 0.1.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.
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /**
3
+ * Minimal HTTPS client built on Node's `node:https` only (no dependencies).
4
+ * Used by the provider signal collectors (O365, Gravatar). Designed to fail
5
+ * soft: callers treat any network/timeout error as a neutral signal rather
6
+ * than a hard verdict.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.httpRequest = httpRequest;
10
+ const node_https_1 = require("node:https");
11
+ const DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; ap-email-guessr/0.2; +https://www.activepieces.com)";
12
+ /**
13
+ * Perform a single HTTPS request. Resolves with the status code and (for
14
+ * non-HEAD requests) the response body. Rejects only on transport failure or
15
+ * timeout so callers can map those to a neutral signal.
16
+ */
17
+ function httpRequest(options) {
18
+ const method = options.method ?? "GET";
19
+ const url = new URL(options.url);
20
+ return new Promise((resolve, reject) => {
21
+ const req = (0, node_https_1.request)({
22
+ protocol: url.protocol,
23
+ hostname: url.hostname,
24
+ port: url.port || 443,
25
+ path: `${url.pathname}${url.search}`,
26
+ method,
27
+ headers: {
28
+ "User-Agent": DEFAULT_USER_AGENT,
29
+ Accept: "*/*",
30
+ ...options.headers,
31
+ },
32
+ }, (res) => {
33
+ const status = res.statusCode ?? 0;
34
+ // HEAD has no body; drain and resolve immediately.
35
+ if (method === "HEAD") {
36
+ res.resume();
37
+ res.on("end", () => resolve({ status, body: "" }));
38
+ return;
39
+ }
40
+ const chunks = [];
41
+ let bytes = 0;
42
+ // Guard against unexpectedly large bodies (e.g. an avatar image
43
+ // if a caller forgets d=404). 256 KiB is ample for JSON APIs.
44
+ const maxBytes = 256 * 1024;
45
+ res.on("data", (chunk) => {
46
+ bytes += chunk.length;
47
+ if (bytes <= maxBytes) {
48
+ chunks.push(chunk);
49
+ }
50
+ else {
51
+ res.destroy();
52
+ }
53
+ });
54
+ res.on("end", () => resolve({ status, body: Buffer.concat(chunks).toString("utf8") }));
55
+ res.on("error", reject);
56
+ });
57
+ req.setTimeout(options.timeoutMs, () => {
58
+ req.destroy(new Error("http_timeout"));
59
+ });
60
+ req.on("error", reject);
61
+ if (options.body !== undefined && method === "POST") {
62
+ req.write(options.body);
63
+ }
64
+ req.end();
65
+ });
66
+ }
67
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Fingerprint a domain's mail provider from its MX hostnames.
3
+ *
4
+ * Two uses:
5
+ * 1. Routing — knowing a domain is Microsoft-backed tells us the O365 account
6
+ * check is authoritative.
7
+ * 2. Confidence weighting — "security gateways" (Proofpoint, Mimecast, ...)
8
+ * sit in front of the real mailserver and routinely accept every RCPT TO
9
+ * before filtering later, so an SMTP "accepted" from them is weak evidence.
10
+ */
11
+ export type MxProviderId = "google" | "microsoft" | "zoho" | "yahoo" | "yandex" | "fastmail" | "proofpoint" | "mimecast" | "barracuda" | "cisco_ironport" | "other" | "none";
12
+ export interface MxProvider {
13
+ id: MxProviderId;
14
+ name: string;
15
+ /** Security gateway that tends to accept-all at RCPT time. */
16
+ isGateway: boolean;
17
+ /** Domain mailboxes are (likely) hosted on Microsoft 365 / Exchange Online. */
18
+ microsoftBacked: boolean;
19
+ /** Domain mailboxes are (likely) hosted on Google Workspace / Gmail. */
20
+ googleBacked: boolean;
21
+ }
22
+ /** Classify a domain's mail provider from its ordered MX hostnames. */
23
+ export declare function fingerprintMx(mxHosts: string[]): MxProvider;
24
+ /**
25
+ * True when the provider hosts its own non-Microsoft mailboxes, so a "Managed"
26
+ * Azure AD namespace there is only a leftover tenant artifact and the O365
27
+ * account check must NOT be trusted. Gateways are excluded: O365 sits behind them.
28
+ */
29
+ export declare function hostsNonMicrosoftMailboxes(provider: MxProvider): boolean;
30
+ //# sourceMappingURL=mx-fingerprint.d.ts.map
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ /**
3
+ * Fingerprint a domain's mail provider from its MX hostnames.
4
+ *
5
+ * Two uses:
6
+ * 1. Routing — knowing a domain is Microsoft-backed tells us the O365 account
7
+ * check is authoritative.
8
+ * 2. Confidence weighting — "security gateways" (Proofpoint, Mimecast, ...)
9
+ * sit in front of the real mailserver and routinely accept every RCPT TO
10
+ * before filtering later, so an SMTP "accepted" from them is weak evidence.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.fingerprintMx = fingerprintMx;
14
+ exports.hostsNonMicrosoftMailboxes = hostsNonMicrosoftMailboxes;
15
+ // Ordered: gateways first so that, when a domain fronts its real provider with
16
+ // a gateway, the gateway (the connection we actually probe) wins.
17
+ const RULES = [
18
+ {
19
+ id: "proofpoint",
20
+ name: "Proofpoint",
21
+ needles: ["pphosted.com", "ppe-hosted.com", "proofpoint.com"],
22
+ isGateway: true,
23
+ },
24
+ {
25
+ id: "mimecast",
26
+ name: "Mimecast",
27
+ needles: ["mimecast.com", "mimecast.co.za", "mimecast-offshore.com"],
28
+ isGateway: true,
29
+ },
30
+ {
31
+ id: "barracuda",
32
+ name: "Barracuda",
33
+ needles: ["barracudanetworks.com", "cudaops.com", "cudamail.com"],
34
+ isGateway: true,
35
+ },
36
+ {
37
+ id: "cisco_ironport",
38
+ name: "Cisco IronPort",
39
+ needles: ["iphmx.com", "ironport.com"],
40
+ isGateway: true,
41
+ },
42
+ {
43
+ id: "microsoft",
44
+ name: "Microsoft 365",
45
+ needles: ["mail.protection.outlook.com", "olc.protection.outlook.com"],
46
+ microsoftBacked: true,
47
+ },
48
+ {
49
+ id: "google",
50
+ name: "Google Workspace",
51
+ needles: [
52
+ "aspmx.l.google.com",
53
+ "googlemail.com",
54
+ "aspmx.google.com",
55
+ "psmtp.com",
56
+ ],
57
+ googleBacked: true,
58
+ },
59
+ { id: "zoho", name: "Zoho Mail", needles: ["zoho.com", "zoho.eu", "zohomail"] },
60
+ { id: "yahoo", name: "Yahoo", needles: ["yahoodns.net"] },
61
+ { id: "yandex", name: "Yandex", needles: ["mx.yandex", "yandex.net"] },
62
+ {
63
+ id: "fastmail",
64
+ name: "Fastmail",
65
+ needles: ["messagingengine.com", "fastmail.com"],
66
+ },
67
+ ];
68
+ const NONE = {
69
+ id: "none",
70
+ name: "None",
71
+ isGateway: false,
72
+ microsoftBacked: false,
73
+ googleBacked: false,
74
+ };
75
+ const OTHER = {
76
+ id: "other",
77
+ name: "Other / Unknown",
78
+ isGateway: false,
79
+ microsoftBacked: false,
80
+ googleBacked: false,
81
+ };
82
+ /** Classify a domain's mail provider from its ordered MX hostnames. */
83
+ function fingerprintMx(mxHosts) {
84
+ if (mxHosts.length === 0) {
85
+ return NONE;
86
+ }
87
+ const hosts = mxHosts.map((host) => host.toLowerCase().replace(/\.$/, ""));
88
+ for (const rule of RULES) {
89
+ const hit = hosts.some((host) => rule.needles.some((needle) => host.includes(needle)));
90
+ if (hit) {
91
+ return {
92
+ id: rule.id,
93
+ name: rule.name,
94
+ isGateway: rule.isGateway ?? false,
95
+ microsoftBacked: rule.microsoftBacked ?? false,
96
+ googleBacked: rule.googleBacked ?? false,
97
+ };
98
+ }
99
+ }
100
+ return OTHER;
101
+ }
102
+ /**
103
+ * True when the provider hosts its own non-Microsoft mailboxes, so a "Managed"
104
+ * Azure AD namespace there is only a leftover tenant artifact and the O365
105
+ * account check must NOT be trusted. Gateways are excluded: O365 sits behind them.
106
+ */
107
+ function hostsNonMicrosoftMailboxes(provider) {
108
+ return (provider.googleBacked ||
109
+ provider.id === "zoho" ||
110
+ provider.id === "yahoo" ||
111
+ provider.id === "yandex" ||
112
+ provider.id === "fastmail");
113
+ }
114
+ //# sourceMappingURL=mx-fingerprint.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Normalize a person's name and build candidate email addresses ordered by
3
+ * real-world corporate frequency.
4
+ *
5
+ * The order is load-bearing: the verifier checks candidates top-down and stops
6
+ * on the first hit, so the most likely patterns must come first to minimize
7
+ * paid verifications. "common" keeps only the leading high-probability slice.
8
+ */
9
+ /** Lowercase, strip accents and keep only [a-z0-9]. */
10
+ export declare function normalizeNamePart(value: string): string;
11
+ /** Strip protocol, path, leading "@" and lowercase a domain input. */
12
+ export declare function normalizeDomain(value: string): string;
13
+ export type PatternSet = "common" | "all";
14
+ export interface GenerateEmailsParams {
15
+ firstName: string;
16
+ lastName: string;
17
+ domain: string;
18
+ /** "common" keeps only the top, highest-probability patterns. Default "all". */
19
+ patternSet?: PatternSet;
20
+ /** Hard cap on candidates returned, applied after ordering. */
21
+ maxVariants?: number;
22
+ }
23
+ /**
24
+ * Generate a de-duplicated list of candidate emails ordered by likelihood.
25
+ * Returns an empty array when the domain or both name parts are missing.
26
+ */
27
+ export declare function generateEmailCandidates(params: GenerateEmailsParams): string[];
28
+ //# sourceMappingURL=patterns.d.ts.map
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /**
3
+ * Normalize a person's name and build candidate email addresses ordered by
4
+ * real-world corporate frequency.
5
+ *
6
+ * The order is load-bearing: the verifier checks candidates top-down and stops
7
+ * on the first hit, so the most likely patterns must come first to minimize
8
+ * paid verifications. "common" keeps only the leading high-probability slice.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.normalizeNamePart = normalizeNamePart;
12
+ exports.normalizeDomain = normalizeDomain;
13
+ exports.generateEmailCandidates = generateEmailCandidates;
14
+ function stripDiacritics(input) {
15
+ let result = "";
16
+ for (const char of input.normalize("NFKD")) {
17
+ const code = char.codePointAt(0) ?? 0;
18
+ // Drop combining diacritical marks (U+0300–U+036F).
19
+ if (code >= 0x0300 && code <= 0x036f) {
20
+ continue;
21
+ }
22
+ result += char;
23
+ }
24
+ return result;
25
+ }
26
+ /** Lowercase, strip accents and keep only [a-z0-9]. */
27
+ function normalizeNamePart(value) {
28
+ return stripDiacritics(value ?? "")
29
+ .toLowerCase()
30
+ .replace(/[^a-z0-9]/g, "");
31
+ }
32
+ /** Strip protocol, path, leading "@" and lowercase a domain input. */
33
+ function normalizeDomain(value) {
34
+ return (value ?? "")
35
+ .trim()
36
+ .toLowerCase()
37
+ .replace(/^https?:\/\//, "")
38
+ .replace(/^@/, "")
39
+ .replace(/\/.*$/, "")
40
+ .replace(/\s+/g, "");
41
+ }
42
+ const LOCAL_PART_REGEX = /^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/;
43
+ /** Leading patterns that cover the large majority of real corporate addresses. */
44
+ const COMMON_LIMIT = 10;
45
+ const join = (a, sep, b) => a && b ? `${a}${sep}${b}` : "";
46
+ // Ordered by approximate real-world B2B frequency (most common first). Each
47
+ // trailing comment names the produced local-part so the terse builders stay
48
+ // readable. This order drives how many paid verifications an early-stop search
49
+ // performs, so changing it changes credit cost.
50
+ const PATTERNS = [
51
+ (p) => join(p.first, ".", p.last), // first.last
52
+ (p) => join(p.fi, "", p.last), // flast
53
+ (p) => p.first, // first
54
+ (p) => join(p.first, "", p.last), // firstlast
55
+ (p) => join(p.first, "_", p.last), // first_last
56
+ (p) => join(p.fi, ".", p.last), // f.last
57
+ (p) => join(p.first, "-", p.last), // first-last
58
+ (p) => p.last, // last
59
+ (p) => join(p.last, ".", p.first), // last.first
60
+ (p) => join(p.first, "", p.li), // firstl
61
+ (p) => join(p.fi, "", p.li), // fl
62
+ (p) => join(p.last, "", p.first), // lastfirst
63
+ (p) => join(p.last, "_", p.first), // last_first
64
+ (p) => join(p.last, "-", p.first), // last-first
65
+ (p) => join(p.fi, "_", p.last), // f_last
66
+ (p) => join(p.fi, "-", p.last), // f-last
67
+ (p) => join(p.last, "", p.fi), // lastf
68
+ (p) => join(p.last, ".", p.fi), // last.f
69
+ (p) => join(p.li, "", p.fi), // lf
70
+ (p) => join(p.first, ".", p.li), // first.l
71
+ ];
72
+ /**
73
+ * Generate a de-duplicated list of candidate emails ordered by likelihood.
74
+ * Returns an empty array when the domain or both name parts are missing.
75
+ */
76
+ function generateEmailCandidates(params) {
77
+ const first = normalizeNamePart(params.firstName);
78
+ const last = normalizeNamePart(params.lastName);
79
+ const domain = normalizeDomain(params.domain);
80
+ if (!domain || (!first && !last)) {
81
+ return [];
82
+ }
83
+ const parts = {
84
+ first,
85
+ last,
86
+ fi: first.charAt(0),
87
+ li: last.charAt(0),
88
+ };
89
+ const seen = new Set();
90
+ const locals = [];
91
+ for (const build of PATTERNS) {
92
+ const local = build(parts);
93
+ if (!local || seen.has(local) || !LOCAL_PART_REGEX.test(local)) {
94
+ continue;
95
+ }
96
+ seen.add(local);
97
+ locals.push(local);
98
+ }
99
+ const limit = params.maxVariants ??
100
+ (params.patternSet === "common" ? COMMON_LIMIT : locals.length);
101
+ return locals
102
+ .slice(0, Math.max(0, limit))
103
+ .map((local) => `${local}@${domain}`);
104
+ }
105
+ //# sourceMappingURL=patterns.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Microsoft 365 / Azure AD account-existence check.
3
+ *
4
+ * Two unauthenticated endpoints, used widely for O365 user enumeration:
5
+ *
6
+ * 1. getuserrealm.srf — classifies the domain's namespace:
7
+ * Managed → mailboxes live in Azure AD / Exchange Online (the account
8
+ * check below is authoritative).
9
+ * Federated → auth is delegated to on-prem ADFS/Okta/etc.; the account
10
+ * check is unreliable, so we skip the verdict.
11
+ * Unknown → not a Microsoft domain.
12
+ *
13
+ * 2. GetCredentialType — returns `IfExistsResult`, which reveals whether a
14
+ * given Azure AD / Microsoft account exists *without* SMTP. This is the
15
+ * single most valuable non-SMTP signal because a large share of corporate
16
+ * domains run on O365, including many that sit behind a security gateway.
17
+ *
18
+ * Everything fails soft: any network/throttle/parse problem yields an
19
+ * inconclusive result rather than a false verdict.
20
+ */
21
+ export type O365Namespace = "Managed" | "Federated" | "Unknown" | null;
22
+ export interface O365Result {
23
+ /** Whether GetCredentialType produced an interpretable verdict. */
24
+ checked: boolean;
25
+ /** true = account exists, false = does not exist, null = inconclusive. */
26
+ exists: boolean | null;
27
+ namespace: O365Namespace;
28
+ ifExistsResult: number | null;
29
+ reason: string;
30
+ }
31
+ export declare const O365_UNCHECKED: O365Result;
32
+ export declare function isMicrosoftConsumerDomain(domain: string): boolean;
33
+ /**
34
+ * Map an `IfExistsResult` code to an existence verdict.
35
+ * 0 → exists
36
+ * 1 → does not exist
37
+ * 6 → exists in a different Microsoft tenant (still a real account)
38
+ * 5 → different identity provider (ambiguous for this domain) → inconclusive
39
+ * other → inconclusive
40
+ */
41
+ export declare function interpretIfExists(code: number | null): boolean | null;
42
+ /** Classify a domain's Microsoft namespace. Returns null on any failure. */
43
+ export declare function getUserRealm(domain: string, timeoutMs: number): Promise<O365Namespace>;
44
+ /**
45
+ * Run the account-existence check for one email, given a pre-resolved namespace
46
+ * for its domain. `applicable` should be true only when the domain is known to
47
+ * be Microsoft-backed (Managed namespace, Microsoft MX, or a consumer domain);
48
+ * otherwise GetCredentialType returns noise for unrelated domains.
49
+ */
50
+ export declare function checkO365Account(email: string, namespace: O365Namespace, applicable: boolean, timeoutMs: number): Promise<O365Result>;
51
+ //# sourceMappingURL=o365.d.ts.map
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ /**
3
+ * Microsoft 365 / Azure AD account-existence check.
4
+ *
5
+ * Two unauthenticated endpoints, used widely for O365 user enumeration:
6
+ *
7
+ * 1. getuserrealm.srf — classifies the domain's namespace:
8
+ * Managed → mailboxes live in Azure AD / Exchange Online (the account
9
+ * check below is authoritative).
10
+ * Federated → auth is delegated to on-prem ADFS/Okta/etc.; the account
11
+ * check is unreliable, so we skip the verdict.
12
+ * Unknown → not a Microsoft domain.
13
+ *
14
+ * 2. GetCredentialType — returns `IfExistsResult`, which reveals whether a
15
+ * given Azure AD / Microsoft account exists *without* SMTP. This is the
16
+ * single most valuable non-SMTP signal because a large share of corporate
17
+ * domains run on O365, including many that sit behind a security gateway.
18
+ *
19
+ * Everything fails soft: any network/throttle/parse problem yields an
20
+ * inconclusive result rather than a false verdict.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.O365_UNCHECKED = void 0;
24
+ exports.isMicrosoftConsumerDomain = isMicrosoftConsumerDomain;
25
+ exports.interpretIfExists = interpretIfExists;
26
+ exports.getUserRealm = getUserRealm;
27
+ exports.checkO365Account = checkO365Account;
28
+ const http_1 = require("../http");
29
+ exports.O365_UNCHECKED = {
30
+ checked: false,
31
+ exists: null,
32
+ namespace: null,
33
+ ifExistsResult: null,
34
+ reason: "not_checked",
35
+ };
36
+ // Consumer Microsoft domains (MSA). getuserrealm often reports these as
37
+ // "Unknown", but GetCredentialType is still authoritative for them.
38
+ const CONSUMER_DOMAINS = new Set([
39
+ "outlook.com",
40
+ "hotmail.com",
41
+ "live.com",
42
+ "msn.com",
43
+ "passport.com",
44
+ "windowslive.com",
45
+ "hotmail.co.uk",
46
+ "hotmail.fr",
47
+ "hotmail.de",
48
+ "hotmail.it",
49
+ "hotmail.es",
50
+ "live.co.uk",
51
+ "live.fr",
52
+ "live.de",
53
+ "outlook.fr",
54
+ "outlook.de",
55
+ "outlook.es",
56
+ "outlook.jp",
57
+ ]);
58
+ function isMicrosoftConsumerDomain(domain) {
59
+ return CONSUMER_DOMAINS.has(domain.toLowerCase());
60
+ }
61
+ /**
62
+ * Map an `IfExistsResult` code to an existence verdict.
63
+ * 0 → exists
64
+ * 1 → does not exist
65
+ * 6 → exists in a different Microsoft tenant (still a real account)
66
+ * 5 → different identity provider (ambiguous for this domain) → inconclusive
67
+ * other → inconclusive
68
+ */
69
+ function interpretIfExists(code) {
70
+ if (code === 0 || code === 6) {
71
+ return true;
72
+ }
73
+ if (code === 1) {
74
+ return false;
75
+ }
76
+ return null;
77
+ }
78
+ /** Classify a domain's Microsoft namespace. Returns null on any failure. */
79
+ async function getUserRealm(domain, timeoutMs) {
80
+ const login = encodeURIComponent(`probe@${domain}`);
81
+ try {
82
+ const res = await (0, http_1.httpRequest)({
83
+ url: `https://login.microsoftonline.com/getuserrealm.srf?login=${login}&json=1`,
84
+ method: "GET",
85
+ timeoutMs,
86
+ });
87
+ if (res.status !== 200) {
88
+ return null;
89
+ }
90
+ const data = JSON.parse(res.body);
91
+ const ns = data.NameSpaceType;
92
+ if (ns === "Managed" || ns === "Federated" || ns === "Unknown") {
93
+ return ns;
94
+ }
95
+ return null;
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ /** Query GetCredentialType for a single address. Returns null code on failure. */
102
+ async function probeCredentialType(email, timeoutMs) {
103
+ try {
104
+ const res = await (0, http_1.httpRequest)({
105
+ url: "https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US",
106
+ method: "POST",
107
+ headers: { "Content-Type": "application/json; charset=UTF-8" },
108
+ body: JSON.stringify({
109
+ Username: email,
110
+ isOtherIdpSupported: true,
111
+ }),
112
+ timeoutMs,
113
+ });
114
+ if (res.status !== 200) {
115
+ return { ifExistsResult: null, throttled: false };
116
+ }
117
+ const data = JSON.parse(res.body);
118
+ return {
119
+ ifExistsResult: typeof data.IfExistsResult === "number"
120
+ ? data.IfExistsResult
121
+ : null,
122
+ throttled: (data.ThrottleStatus ?? 0) !== 0,
123
+ };
124
+ }
125
+ catch {
126
+ return { ifExistsResult: null, throttled: false };
127
+ }
128
+ }
129
+ /**
130
+ * Run the account-existence check for one email, given a pre-resolved namespace
131
+ * for its domain. `applicable` should be true only when the domain is known to
132
+ * be Microsoft-backed (Managed namespace, Microsoft MX, or a consumer domain);
133
+ * otherwise GetCredentialType returns noise for unrelated domains.
134
+ */
135
+ async function checkO365Account(email, namespace, applicable, timeoutMs) {
136
+ if (!applicable) {
137
+ return { ...exports.O365_UNCHECKED, namespace, reason: "not_microsoft_domain" };
138
+ }
139
+ const probe = await probeCredentialType(email, timeoutMs);
140
+ if (probe.throttled) {
141
+ return {
142
+ checked: false,
143
+ exists: null,
144
+ namespace,
145
+ ifExistsResult: probe.ifExistsResult,
146
+ reason: "throttled",
147
+ };
148
+ }
149
+ if (probe.ifExistsResult === null) {
150
+ return {
151
+ checked: false,
152
+ exists: null,
153
+ namespace,
154
+ ifExistsResult: null,
155
+ reason: "no_result",
156
+ };
157
+ }
158
+ const exists = interpretIfExists(probe.ifExistsResult);
159
+ return {
160
+ checked: exists !== null,
161
+ exists,
162
+ namespace,
163
+ ifExistsResult: probe.ifExistsResult,
164
+ reason: exists === true
165
+ ? "account_exists"
166
+ : exists === false
167
+ ? "account_not_found"
168
+ : "inconclusive",
169
+ };
170
+ }
171
+ //# sourceMappingURL=o365.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Provider-agnostic email-existence verifier.
3
+ *
4
+ * Both the direct O365 account check and the no2bounce API implement this, so
5
+ * the credit-saving search logic (early-stop, catch-all-stop) in `find-email`
6
+ * is identical regardless of which provider actually answers.
7
+ */
8
+ export type VerifyVerdict = "valid" | "invalid" | "catch_all" | "unknown";
9
+ export interface VerifyOutcome {
10
+ email: string;
11
+ verdict: VerifyVerdict;
12
+ /** Credits this single verification consumed (0 for the free O365 check). */
13
+ creditsUsed: number;
14
+ reason: string;
15
+ catchAll?: boolean;
16
+ raw?: unknown;
17
+ }
18
+ export interface Verifier {
19
+ readonly name: string;
20
+ verify(email: string): Promise<VerifyOutcome>;
21
+ }
22
+ //# sourceMappingURL=verifier.d.ts.map
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * Provider-agnostic email-existence verifier.
4
+ *
5
+ * Both the direct O365 account check and the no2bounce API implement this, so
6
+ * the credit-saving search logic (early-stop, catch-all-stop) in `find-email`
7
+ * is identical regardless of which provider actually answers.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ //# sourceMappingURL=verifier.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Verifier adapter for the no2bounce API (wire format confirmed against the
3
+ * live API).
4
+ *
5
+ * Async flow: POST a 1-email batch to get a `trackingId` (under `data`), then
6
+ * GET-poll `?trackingId=` until the *top-level* `overallStatus` is "Completed".
7
+ * For a single email the top-level count fields are that email's verdict.
8
+ * Billing is 1 credit per email (`creditDebited`), with no refund on undeliverable.
9
+ */
10
+ import type { Verifier, VerifyVerdict } from "../verifier";
11
+ export interface No2BounceOptions {
12
+ apitoken: string;
13
+ timeoutMs: number;
14
+ pollIntervalMs?: number;
15
+ maxWaitMs?: number;
16
+ }
17
+ interface PollResult {
18
+ overallStatus?: string;
19
+ percent?: number;
20
+ creditDebited?: number;
21
+ Deliverable?: number;
22
+ Undeliverable?: number;
23
+ "Deliverable/AcceptAll"?: number;
24
+ "UnDeliverable/AcceptAll"?: number;
25
+ "Risky/AcceptAll"?: number;
26
+ }
27
+ export interface DecodedResult {
28
+ verdict: VerifyVerdict;
29
+ catchAll: boolean;
30
+ }
31
+ export declare function decodeSingle(r: PollResult): DecodedResult;
32
+ export declare function createNo2BounceVerifier(options: No2BounceOptions): Verifier;
33
+ export {};
34
+ //# sourceMappingURL=no2bounce-verifier.d.ts.map