@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,42 @@
|
|
|
1
|
+
import { type PlacementObservationSource } from "./deliverability-checks.js";
|
|
2
|
+
/**
|
|
3
|
+
* One read contract for the stored deliverability evidence on every surface: the
|
|
4
|
+
* `/api/cli/deliverability/{checks,observations,evidence,compare,score-copy}` routes,
|
|
5
|
+
* `oxygen mailboxes deliverability …` and `oxygen_mailboxes_deliverability_*`. The
|
|
6
|
+
* enums and bounds here are the ones the routes enforce, so CLI help, MCP schemas
|
|
7
|
+
* and route validation cannot drift apart.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DELIVERABILITY_SURFACE_DEFAULT_LIMIT = 25;
|
|
10
|
+
export declare const DELIVERABILITY_SURFACE_MAX_LIMIT = 100;
|
|
11
|
+
/** A check's stored raw evidence is capped at 64 KB; reads return it whole up to this size. */
|
|
12
|
+
export declare const DELIVERABILITY_SURFACE_RAW_MAX_CHARS = 16000;
|
|
13
|
+
export declare const DELIVERABILITY_SURFACE_EVIDENCE_DEFAULT_DAYS = 28;
|
|
14
|
+
export declare const DELIVERABILITY_SURFACE_CHECK_KINDS: readonly ["dns_auth", "bimi", "tracking_cname", "blacklist", "content_score", "header_proof", "placement"];
|
|
15
|
+
export declare const DELIVERABILITY_SURFACE_SUBJECT_TYPES: readonly ["domain", "mailbox", "copy", "placement_run"];
|
|
16
|
+
export declare const DELIVERABILITY_SURFACE_RUN_STATUSES: readonly ["succeeded", "partial", "failed", "unknown"];
|
|
17
|
+
export declare const DELIVERABILITY_SURFACE_FINDING_STATUSES: readonly ["pass", "warn", "fail", "unknown"];
|
|
18
|
+
export declare const DELIVERABILITY_SURFACE_FOLDERS: readonly ["inbox", "spam", "other", "not_observed", "unknown"];
|
|
19
|
+
export declare const DELIVERABILITY_SURFACE_BUCKETS: readonly ["day", "week"];
|
|
20
|
+
export declare const DELIVERABILITY_SURFACE_RECEIVER_PROVIDERS: readonly ["google", "microsoft", "yahoo", "other", "unknown"];
|
|
21
|
+
/**
|
|
22
|
+
* Placement sources by their public names. Managed vendor rails are branded on customer
|
|
23
|
+
* surfaces (OXYGEN_PRODUCT.md): stored `emailguard` reads as `oxygen_deliverability` and
|
|
24
|
+
* stored `sendkit` as `oxygen_placement`. Storage keeps the backend ids.
|
|
25
|
+
*/
|
|
26
|
+
export declare const DELIVERABILITY_SURFACE_SOURCES: readonly ["workspace", "network", "oxygen_deliverability", "zapmail", "oxygen_placement"];
|
|
27
|
+
export type DeliverabilitySurfaceSource = (typeof DELIVERABILITY_SURFACE_SOURCES)[number];
|
|
28
|
+
export declare function publicPlacementSource(source: PlacementObservationSource): DeliverabilitySurfaceSource;
|
|
29
|
+
/** Public name to stored id; the backend ids stay accepted as hidden compatibility spellings. */
|
|
30
|
+
export declare function storedPlacementSource(value: string): PlacementObservationSource | null;
|
|
31
|
+
/** Query fields of each read, in the spelling all three surfaces send. */
|
|
32
|
+
export declare const DELIVERABILITY_CHECK_LIST_FIELDS: readonly ["kind", "subject_type", "subject", "status", "finding_status", "since", "until", "limit", "cursor"];
|
|
33
|
+
export declare const DELIVERABILITY_OBSERVATION_LIST_FIELDS: readonly ["source", "sender_domain", "sender_mailbox_id", "receiver_provider", "folder", "run_ref", "content_hash", "since", "until", "limit", "cursor"];
|
|
34
|
+
export declare const DELIVERABILITY_EVIDENCE_FIELDS: readonly ["sender_domain", "sender_mailbox_id", "content_hash", "receiver_provider", "since", "until", "bucket"];
|
|
35
|
+
export declare const DELIVERABILITY_SCORE_COPY_FIELDS: readonly ["subject", "body_text", "body_html", "from", "request_id"];
|
|
36
|
+
/**
|
|
37
|
+
* Used as the From address when score-copy names no sender. RFC 2606 reserves the
|
|
38
|
+
* domain, so no real sender's reputation is implied; the content hash ignores From.
|
|
39
|
+
*/
|
|
40
|
+
export declare const DELIVERABILITY_SCORE_COPY_DEFAULT_FROM = "sender@example.com";
|
|
41
|
+
/** Builds a query string from the declared fields, skipping absent and empty values. */
|
|
42
|
+
export declare function deliverabilitySurfaceQuery(fields: readonly string[], values: Record<string, unknown>): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { DELIVERABILITY_CHECK_KINDS, DELIVERABILITY_FINDING_STATUSES, DELIVERABILITY_RUN_STATUSES, DELIVERABILITY_SUBJECT_TYPES, PLACEMENT_FOLDERS, } from "./deliverability-checks.js";
|
|
2
|
+
import { DELIVERABILITY_EVIDENCE_BUCKETS } from "./deliverability-evidence.js";
|
|
3
|
+
/**
|
|
4
|
+
* One read contract for the stored deliverability evidence on every surface: the
|
|
5
|
+
* `/api/cli/deliverability/{checks,observations,evidence,compare,score-copy}` routes,
|
|
6
|
+
* `oxygen mailboxes deliverability …` and `oxygen_mailboxes_deliverability_*`. The
|
|
7
|
+
* enums and bounds here are the ones the routes enforce, so CLI help, MCP schemas
|
|
8
|
+
* and route validation cannot drift apart.
|
|
9
|
+
*/
|
|
10
|
+
export const DELIVERABILITY_SURFACE_DEFAULT_LIMIT = 25;
|
|
11
|
+
export const DELIVERABILITY_SURFACE_MAX_LIMIT = 100;
|
|
12
|
+
/** A check's stored raw evidence is capped at 64 KB; reads return it whole up to this size. */
|
|
13
|
+
export const DELIVERABILITY_SURFACE_RAW_MAX_CHARS = 16_000;
|
|
14
|
+
export const DELIVERABILITY_SURFACE_EVIDENCE_DEFAULT_DAYS = 28;
|
|
15
|
+
export const DELIVERABILITY_SURFACE_CHECK_KINDS = DELIVERABILITY_CHECK_KINDS;
|
|
16
|
+
export const DELIVERABILITY_SURFACE_SUBJECT_TYPES = DELIVERABILITY_SUBJECT_TYPES;
|
|
17
|
+
export const DELIVERABILITY_SURFACE_RUN_STATUSES = DELIVERABILITY_RUN_STATUSES;
|
|
18
|
+
export const DELIVERABILITY_SURFACE_FINDING_STATUSES = DELIVERABILITY_FINDING_STATUSES;
|
|
19
|
+
export const DELIVERABILITY_SURFACE_FOLDERS = PLACEMENT_FOLDERS;
|
|
20
|
+
export const DELIVERABILITY_SURFACE_BUCKETS = DELIVERABILITY_EVIDENCE_BUCKETS;
|
|
21
|
+
export const DELIVERABILITY_SURFACE_RECEIVER_PROVIDERS = ["google", "microsoft", "yahoo", "other", "unknown"];
|
|
22
|
+
/**
|
|
23
|
+
* Placement sources by their public names. Managed vendor rails are branded on customer
|
|
24
|
+
* surfaces (OXYGEN_PRODUCT.md): stored `emailguard` reads as `oxygen_deliverability` and
|
|
25
|
+
* stored `sendkit` as `oxygen_placement`. Storage keeps the backend ids.
|
|
26
|
+
*/
|
|
27
|
+
export const DELIVERABILITY_SURFACE_SOURCES = ["workspace", "network", "oxygen_deliverability", "zapmail", "oxygen_placement"];
|
|
28
|
+
const STORED_TO_PUBLIC = {
|
|
29
|
+
workspace: "workspace", network: "network", emailguard: "oxygen_deliverability", zapmail: "zapmail", sendkit: "oxygen_placement",
|
|
30
|
+
};
|
|
31
|
+
export function publicPlacementSource(source) {
|
|
32
|
+
return STORED_TO_PUBLIC[source] ?? source;
|
|
33
|
+
}
|
|
34
|
+
/** Public name to stored id; the backend ids stay accepted as hidden compatibility spellings. */
|
|
35
|
+
export function storedPlacementSource(value) {
|
|
36
|
+
const normalized = value.trim().toLowerCase();
|
|
37
|
+
for (const [stored, publicName] of Object.entries(STORED_TO_PUBLIC)) {
|
|
38
|
+
if (normalized === publicName || normalized === stored)
|
|
39
|
+
return stored;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/** Query fields of each read, in the spelling all three surfaces send. */
|
|
44
|
+
export const DELIVERABILITY_CHECK_LIST_FIELDS = ["kind", "subject_type", "subject", "status", "finding_status", "since", "until", "limit", "cursor"];
|
|
45
|
+
export const DELIVERABILITY_OBSERVATION_LIST_FIELDS = [
|
|
46
|
+
"source", "sender_domain", "sender_mailbox_id", "receiver_provider", "folder", "run_ref", "content_hash", "since", "until", "limit", "cursor",
|
|
47
|
+
];
|
|
48
|
+
export const DELIVERABILITY_EVIDENCE_FIELDS = ["sender_domain", "sender_mailbox_id", "content_hash", "receiver_provider", "since", "until", "bucket"];
|
|
49
|
+
export const DELIVERABILITY_SCORE_COPY_FIELDS = ["subject", "body_text", "body_html", "from", "request_id"];
|
|
50
|
+
/**
|
|
51
|
+
* Used as the From address when score-copy names no sender. RFC 2606 reserves the
|
|
52
|
+
* domain, so no real sender's reputation is implied; the content hash ignores From.
|
|
53
|
+
*/
|
|
54
|
+
export const DELIVERABILITY_SCORE_COPY_DEFAULT_FROM = "sender@example.com";
|
|
55
|
+
/** Builds a query string from the declared fields, skipping absent and empty values. */
|
|
56
|
+
export function deliverabilitySurfaceQuery(fields, values) {
|
|
57
|
+
const params = new URLSearchParams();
|
|
58
|
+
for (const field of fields) {
|
|
59
|
+
const value = values[field];
|
|
60
|
+
if (value === undefined || value === null || value === "")
|
|
61
|
+
continue;
|
|
62
|
+
params.set(field, String(value));
|
|
63
|
+
}
|
|
64
|
+
const query = params.toString();
|
|
65
|
+
return query ? `?${query}` : "";
|
|
66
|
+
}
|
|
@@ -135,12 +135,42 @@ async function writeDiagnosticSpool(directory, id, compressed) {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
+
const uploadStripes = new Map();
|
|
138
139
|
/** A fixed set of stripes bounds SQLite lock files while coordinating direct and retry owners. */
|
|
139
140
|
export async function lockDiagnosticUpload(directory, id, waitMs = 0) {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
const path = join(directory, `.upload-${id.slice(0, 2).toLowerCase()}.lock`);
|
|
142
|
+
const deadline = performance.now() + waitMs;
|
|
143
|
+
while (true) {
|
|
144
|
+
// Unrelated records in this process share the stripe's kernel lock; only the
|
|
145
|
+
// same record or another process consumes the bounded wait. Holders keep the
|
|
146
|
+
// stripe across receipt admission, which can queue behind spool inspections.
|
|
147
|
+
let stripe = uploadStripes.get(path);
|
|
148
|
+
if (!stripe) {
|
|
149
|
+
const release = await storageStage("lock", () => acquireDiagnosticStorageLock(path));
|
|
150
|
+
if (release)
|
|
151
|
+
uploadStripes.set(path, stripe = { release, records: new Set() });
|
|
152
|
+
else
|
|
153
|
+
stripe = uploadStripes.get(path);
|
|
154
|
+
}
|
|
155
|
+
if (stripe && !stripe.records.has(id)) {
|
|
156
|
+
const held = stripe;
|
|
157
|
+
held.records.add(id);
|
|
158
|
+
let released = false;
|
|
159
|
+
return async () => {
|
|
160
|
+
if (released)
|
|
161
|
+
return;
|
|
162
|
+
released = true;
|
|
163
|
+
held.records.delete(id);
|
|
164
|
+
if (held.records.size || uploadStripes.get(path) !== held)
|
|
165
|
+
return;
|
|
166
|
+
uploadStripes.delete(path);
|
|
167
|
+
await held.release();
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (performance.now() >= deadline)
|
|
171
|
+
throw new StorageFailure("lock", { code: "lock_timeout" });
|
|
172
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(20, Math.max(1, deadline - performance.now()))));
|
|
173
|
+
}
|
|
144
174
|
}
|
|
145
175
|
export async function readUploadProgress(directory, id, storeFingerprint) {
|
|
146
176
|
const path = join(directory, `${id}.receipt.json`);
|
|
@@ -13,6 +13,28 @@ export type DomainAuthenticationCheck = {
|
|
|
13
13
|
content: string;
|
|
14
14
|
}>;
|
|
15
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* The BIMI assertion record at `default._bimi.<domain>`. BIMI is optional: absence is the normal
|
|
18
|
+
* case and never makes a domain unhealthy. `unknown` means the lookup failed, not that it is absent.
|
|
19
|
+
*/
|
|
20
|
+
export type DomainBimiObservation = {
|
|
21
|
+
state: "present" | "absent" | "invalid" | "unknown";
|
|
22
|
+
/**
|
|
23
|
+
* The domain whose `default._bimi` answered last: the checked domain, or the organizational
|
|
24
|
+
* (DMARC policy) domain a receiver falls back to when the checked subdomain publishes none.
|
|
25
|
+
* Absent on observations recorded before the fallback was read.
|
|
26
|
+
*/
|
|
27
|
+
domain?: string | null;
|
|
28
|
+
record: string | null;
|
|
29
|
+
/** `l=`: the SVG logo URL. */
|
|
30
|
+
logoUrl: string | null;
|
|
31
|
+
/** `a=`: the mark certificate (VMC/CMC) URL. */
|
|
32
|
+
authorityUrl: string | null;
|
|
33
|
+
/** `v=BIMI1; l=;` explicitly declines a logo. */
|
|
34
|
+
declined: boolean;
|
|
35
|
+
};
|
|
36
|
+
/** Parse the TXT strings at `default._bimi.<domain>`; null means the lookup failed. Non-BIMI strings are ignored. */
|
|
37
|
+
export declare function parseBimiRecords(records: readonly string[] | null, domain?: string | null): DomainBimiObservation;
|
|
16
38
|
export type DomainAuthenticationObservation = {
|
|
17
39
|
domain: string;
|
|
18
40
|
source: "public_dns";
|
|
@@ -26,6 +48,8 @@ export type DomainAuthenticationObservation = {
|
|
|
26
48
|
spfLookupTerms: number | null;
|
|
27
49
|
spfLookupComplete: boolean;
|
|
28
50
|
messageAlignment: "unknown";
|
|
51
|
+
/** Absent on observations recorded before BIMI was read; never part of `state`. */
|
|
52
|
+
bimi?: DomainBimiObservation;
|
|
29
53
|
};
|
|
30
54
|
export type DomainAuthenticationSnapshot = {
|
|
31
55
|
domain: string;
|
|
@@ -7,6 +7,26 @@ export const DOMAIN_AUTHENTICATION_CHECKS = [
|
|
|
7
7
|
"mx",
|
|
8
8
|
"nameservers",
|
|
9
9
|
];
|
|
10
|
+
/** Parse the TXT strings at `default._bimi.<domain>`; null means the lookup failed. Non-BIMI strings are ignored. */
|
|
11
|
+
export function parseBimiRecords(records, domain = null) {
|
|
12
|
+
const empty = { domain, record: null, logoUrl: null, authorityUrl: null, declined: false };
|
|
13
|
+
if (records === null)
|
|
14
|
+
return { state: "unknown", ...empty };
|
|
15
|
+
const bimi = records.map((record) => record.trim()).filter((record) => /^v\s*=\s*BIMI1\s*(;|$)/i.test(record));
|
|
16
|
+
if (bimi.length === 0)
|
|
17
|
+
return { state: "absent", ...empty };
|
|
18
|
+
if (bimi.length > 1)
|
|
19
|
+
return { state: "invalid", ...empty, record: bimi.join("\n") };
|
|
20
|
+
const tags = new Map();
|
|
21
|
+
for (const part of bimi[0].split(";")) {
|
|
22
|
+
const at = part.indexOf("=");
|
|
23
|
+
if (at > 0)
|
|
24
|
+
tags.set(part.slice(0, at).trim().toLowerCase(), part.slice(at + 1).trim());
|
|
25
|
+
}
|
|
26
|
+
const logoUrl = tags.get("l") || null;
|
|
27
|
+
const authorityUrl = tags.get("a") || null;
|
|
28
|
+
return { state: "present", domain, record: bimi[0], logoUrl, authorityUrl, declined: !logoUrl && !authorityUrl };
|
|
29
|
+
}
|
|
10
30
|
export function domainAuthenticationStale(snapshot, now = Date.now()) {
|
|
11
31
|
return (!snapshot.observation ||
|
|
12
32
|
now - Date.parse(snapshot.observation.checkedAt) >=
|
|
@@ -304,17 +304,25 @@ export function createImportHeaderProjector(input) {
|
|
|
304
304
|
keyByHeader.set(header, key);
|
|
305
305
|
claimed.add(key);
|
|
306
306
|
};
|
|
307
|
-
// Explicit choices
|
|
307
|
+
// Explicit choices, then exact keys, claim their columns before any alias can.
|
|
308
|
+
// A column takes one header: a later explicit choice or exact key for an
|
|
309
|
+
// already claimed column is left out, never written over the first value.
|
|
308
310
|
for (const header of pending) {
|
|
309
311
|
if (ignore.has(header)) {
|
|
310
312
|
skip(header, "not_selected");
|
|
311
313
|
continue;
|
|
312
314
|
}
|
|
313
315
|
const explicit = explicitMap[header];
|
|
314
|
-
if (explicit)
|
|
316
|
+
if (!explicit)
|
|
317
|
+
continue;
|
|
318
|
+
if (claimed.has(explicit))
|
|
319
|
+
skip(header, "duplicate_target", explicit);
|
|
320
|
+
else
|
|
315
321
|
claim(header, explicit);
|
|
322
|
+
}
|
|
323
|
+
for (const header of pending) {
|
|
324
|
+
if (keyByHeader.has(header) || ignored.has(header))
|
|
316
325
|
continue;
|
|
317
|
-
}
|
|
318
326
|
const key = exact.get(header) ?? exact.get(header.trim());
|
|
319
327
|
if (key && !claimed.has(key))
|
|
320
328
|
claim(header, key);
|
|
@@ -4,6 +4,22 @@ export type LinkedInConnectionIdentity = {
|
|
|
4
4
|
profileUrl?: string | null | undefined;
|
|
5
5
|
publicIdentifier?: string | null | undefined;
|
|
6
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* `linkedin_member_id_v1`: the people identity every LinkedIn event joins on.
|
|
9
|
+
*
|
|
10
|
+
* One namespace, the Classic profile id (`ACoAA…`), because it is the id
|
|
11
|
+
* Unipile reports for every chat attendee, relation, connection and sequence
|
|
12
|
+
* enrollment. It accepts that id bare, as a profile URN or inside a profile
|
|
13
|
+
* URL, and keeps its case: it is base64url, so a folded id names nobody.
|
|
14
|
+
*
|
|
15
|
+
* Cross-provider rule: Up2Data/Harvest numeric member ids (`urn:li:member:N`)
|
|
16
|
+
* and Sales Navigator ids (`ACwAA…`) are different values for the same person
|
|
17
|
+
* and cannot be translated without a profile read, so they normalize to "" and
|
|
18
|
+
* are never stored or matched here. A payload that carries both (a Harvest
|
|
19
|
+
* profile's `profile_urn` beside its numeric `linkedin_id`) contributes its
|
|
20
|
+
* `ACo` value — aliases only ever come from the same provider person.
|
|
21
|
+
*/
|
|
22
|
+
export declare function normalizeLinkedinMemberId(value: string): string;
|
|
7
23
|
export declare function linkedinConnectionKeys(identity: LinkedInConnectionIdentity): string[];
|
|
8
24
|
/** Return the original waiting IDs so frozen enrollment identity guards remain authoritative. */
|
|
9
25
|
export declare function connectedLinkedInLeadIds(targets: LinkedInConnectionIdentity[], connections: LinkedInConnectionIdentity[]): string[];
|
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
import { normalizeLinkedinProfileUrl, isLinkedinMemberIdHandle } from "./linkedin-url.js";
|
|
2
|
+
const LINKEDIN_PROFILE_URN_PREFIX = /^urn:li:(?:fs_miniProfile|fsd_profile|member):/;
|
|
3
|
+
const LINKEDIN_CLASSIC_MEMBER_ID = /^ACo[A-Za-z0-9_-]+$/;
|
|
4
|
+
function stripLinkedinProfileUrn(value) {
|
|
5
|
+
return value.trim().replace(LINKEDIN_PROFILE_URN_PREFIX, "");
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `linkedin_member_id_v1`: the people identity every LinkedIn event joins on.
|
|
9
|
+
*
|
|
10
|
+
* One namespace, the Classic profile id (`ACoAA…`), because it is the id
|
|
11
|
+
* Unipile reports for every chat attendee, relation, connection and sequence
|
|
12
|
+
* enrollment. It accepts that id bare, as a profile URN or inside a profile
|
|
13
|
+
* URL, and keeps its case: it is base64url, so a folded id names nobody.
|
|
14
|
+
*
|
|
15
|
+
* Cross-provider rule: Up2Data/Harvest numeric member ids (`urn:li:member:N`)
|
|
16
|
+
* and Sales Navigator ids (`ACwAA…`) are different values for the same person
|
|
17
|
+
* and cannot be translated without a profile read, so they normalize to "" and
|
|
18
|
+
* are never stored or matched here. A payload that carries both (a Harvest
|
|
19
|
+
* profile's `profile_urn` beside its numeric `linkedin_id`) contributes its
|
|
20
|
+
* `ACo` value — aliases only ever come from the same provider person.
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeLinkedinMemberId(value) {
|
|
23
|
+
let id = stripLinkedinProfileUrn(value);
|
|
24
|
+
if (/linkedin\.com\//i.test(id)) {
|
|
25
|
+
const handle = normalizeLinkedinProfileUrl(id)?.dispatchHandle;
|
|
26
|
+
id = handle ? stripLinkedinProfileUrn(handle) : "";
|
|
27
|
+
}
|
|
28
|
+
return LINKEDIN_CLASSIC_MEMBER_ID.test(id) ? id : "";
|
|
29
|
+
}
|
|
2
30
|
export function linkedinConnectionKeys(identity) {
|
|
3
31
|
const id = identity.providerId.trim();
|
|
4
|
-
const keys = id ? [`id:${id
|
|
32
|
+
const keys = id ? [`id:${stripLinkedinProfileUrn(id)}`] : [];
|
|
5
33
|
const publicIdentifier = identity.publicIdentifier?.trim();
|
|
6
34
|
const publicUrl = publicIdentifier && !/[\s/?#]/.test(publicIdentifier)
|
|
7
35
|
? `https://www.linkedin.com/in/${publicIdentifier}` : null;
|
|
@@ -29,7 +57,7 @@ export function connectedLinkedInLeadIds(targets, connections) {
|
|
|
29
57
|
for (const connection of connections) {
|
|
30
58
|
for (const key of linkedinConnectionKeys(connection)) {
|
|
31
59
|
const ids = owners.get(key) ?? new Set();
|
|
32
|
-
ids.add(connection.providerId
|
|
60
|
+
ids.add(stripLinkedinProfileUrn(connection.providerId));
|
|
33
61
|
owners.set(key, ids);
|
|
34
62
|
}
|
|
35
63
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network Mining is DEV-only (Philipp, 2026-10-17). Unknown tiers and production-shaped data are always denied,
|
|
3
|
+
* independent of NODE_ENV; the staff flag is set in Doppler's dev config.
|
|
4
|
+
*/
|
|
5
|
+
export declare function networkMiningEnabled(env?: Record<string, string | undefined>): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { isProductionData } from "./deploy-env.js";
|
|
2
|
+
/**
|
|
3
|
+
* Network Mining is DEV-only (Philipp, 2026-10-17). Unknown tiers and production-shaped data are always denied,
|
|
4
|
+
* independent of NODE_ENV; the staff flag is set in Doppler's dev config.
|
|
5
|
+
*/
|
|
6
|
+
export function networkMiningEnabled(env = process.env) {
|
|
7
|
+
return env.OXYGEN_DEPLOY_ENV === "selfhost-dev" && !isProductionData(env)
|
|
8
|
+
&& env.OXYGEN_NETWORK_MINING_ENABLED === "staff";
|
|
9
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { DeliverabilityFinding } from "./deliverability-checks.js";
|
|
2
|
+
/**
|
|
3
|
+
* Receiver-side authentication proof from a received probe's headers. Only the receiving provider's
|
|
4
|
+
* own Authentication-Results row is trusted; rows from any other authserv-id (a sender, a forwarder or
|
|
5
|
+
* a forged header) are ignored. No raw header, receiver address or message id leaves this module.
|
|
6
|
+
*/
|
|
7
|
+
export declare const RECEIVED_AUTH_RESULTS: readonly ["pass", "fail", "softfail", "neutral", "none", "temperror", "permerror", "unknown"];
|
|
8
|
+
export type ReceivedAuthResult = (typeof RECEIVED_AUTH_RESULTS)[number];
|
|
9
|
+
export type ReceivedAuthProvider = "google" | "microsoft";
|
|
10
|
+
export type ReceivedHeader = {
|
|
11
|
+
name: string;
|
|
12
|
+
value: string;
|
|
13
|
+
};
|
|
14
|
+
/** The provider header names a receiver read must request for the proof. */
|
|
15
|
+
export declare const RECEIVED_AUTHENTICATION_HEADERS: readonly ["Authentication-Results", "ARC-Authentication-Results", "Received", "DKIM-Signature", "From"];
|
|
16
|
+
/** PTR and forward-confirmed reverse DNS of the sending IP; `unknown` when the lookup could not be answered. */
|
|
17
|
+
export type ReverseDnsProof = {
|
|
18
|
+
ip: string;
|
|
19
|
+
ptr: "pass" | "fail" | "unknown";
|
|
20
|
+
hostnames: string[];
|
|
21
|
+
fcrdns: "pass" | "fail" | "unknown";
|
|
22
|
+
confirmed_hostname: string | null;
|
|
23
|
+
};
|
|
24
|
+
export type ReceivedAuthentication = {
|
|
25
|
+
/** The trusted row's authserv-id; null for Exchange Online's id-less row or when nothing was trusted. */
|
|
26
|
+
authserv_id: string | null;
|
|
27
|
+
source: "authentication_results" | "arc_authentication_results" | "none";
|
|
28
|
+
spf: ReceivedAuthResult;
|
|
29
|
+
spf_domain: string | null;
|
|
30
|
+
dkim: ReceivedAuthResult;
|
|
31
|
+
dkim_domain: string | null;
|
|
32
|
+
dkim_selector: string | null;
|
|
33
|
+
dmarc: ReceivedAuthResult;
|
|
34
|
+
/** Microsoft composite authentication; null for other receivers. */
|
|
35
|
+
compauth: ReceivedAuthResult | null;
|
|
36
|
+
header_from_domain: string | null;
|
|
37
|
+
/** DMARC pass, or a DKIM pass whose d= aligns (relaxed) with the From domain; null when unproven. */
|
|
38
|
+
aligned: boolean | null;
|
|
39
|
+
/** First hop outside the receiving provider. */
|
|
40
|
+
sending_ip: string | null;
|
|
41
|
+
reverse_dns?: ReverseDnsProof;
|
|
42
|
+
};
|
|
43
|
+
/** Splits a raw header block (RFC 5322 section 2.2.3 folding) into ordered entries, top first. */
|
|
44
|
+
export declare function parseRawHeaderBlock(raw: string): ReceivedHeader[];
|
|
45
|
+
export declare function normalizeAuthDomain(value: string | null | undefined): string | null;
|
|
46
|
+
/** Organizational-domain approximation without a public-suffix list (relaxed DMARC alignment). */
|
|
47
|
+
export declare function organizationalDomain(domain: string): string;
|
|
48
|
+
export declare const relaxedAligned: (a: string, b: string) => boolean;
|
|
49
|
+
/** Lowercased IP text or null; rejects anything that is not one IPv4 or IPv6 address. */
|
|
50
|
+
export declare function normalizeIp(value: string | null | undefined): string | null;
|
|
51
|
+
/** Eight 16-bit groups, or null when the text is not an IPv6 address. */
|
|
52
|
+
export declare function expandIpv6(value: string): number[] | null;
|
|
53
|
+
/** Parses the receiving provider's verdict from an ordered (top-first) header list. */
|
|
54
|
+
export declare function parseReceivedAuthentication(input: {
|
|
55
|
+
provider: ReceivedAuthProvider;
|
|
56
|
+
headers: ReadonlyArray<ReceivedHeader>;
|
|
57
|
+
}): ReceivedAuthentication;
|
|
58
|
+
/**
|
|
59
|
+
* The proof a cross-workspace (network) probe may keep. The receiving organization's own servers can
|
|
60
|
+
* surface as the authserv-id, an inbound gateway or forwarder (the SPF identity and connecting IP it
|
|
61
|
+
* relays from, its PTR names) or a gateway's added DKIM signature. So every domain survives only when it
|
|
62
|
+
* aligns with the probe's sender domain, and the sending IP (with its reverse DNS) only when the receiver
|
|
63
|
+
* reported SPF pass for that aligned identity: then the IP is the sender's, authorized by its own domain.
|
|
64
|
+
* Verdicts (SPF, DKIM, DMARC, compauth, alignment) describe only the sender and are kept.
|
|
65
|
+
*/
|
|
66
|
+
export declare function networkReceivedAuthentication(auth: ReceivedAuthentication, senderAddress: string): ReceivedAuthentication;
|
|
67
|
+
/** Defensive read of a stored proof; malformed fields become null/unknown, a non-object becomes null. */
|
|
68
|
+
export declare function readReceivedAuthentication(value: unknown): ReceivedAuthentication | null;
|
|
69
|
+
/** Typed findings for one received probe; PTR/FCrDNS appear only when the reverse lookup ran. */
|
|
70
|
+
export declare function headerProofFindings(auth: ReceivedAuthentication | null): DeliverabilityFinding[];
|
|
71
|
+
/** The reverse-lookup name for an IP (in-addr.arpa / nibble ip6.arpa); null for anything else. */
|
|
72
|
+
export declare function reverseDnsName(ip: string): string | null;
|
|
73
|
+
/** Same address regardless of IPv6 spelling. */
|
|
74
|
+
export declare function sameIp(a: string, b: string): boolean;
|