@oxygen-agent/cli 1.1014.1138 → 1.1014.1221

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.
Files changed (82) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +2 -0
  3. package/dist/command-support.d.ts +8 -0
  4. package/dist/command-support.js +11 -0
  5. package/dist/commands/api-keys.d.ts +9 -0
  6. package/dist/commands/api-keys.js +4 -1
  7. package/dist/commands/capabilities.js +4 -2
  8. package/dist/commands/crm.js +28 -2
  9. package/dist/commands/dashboards.js +2 -2
  10. package/dist/commands/db.js +34 -0
  11. package/dist/commands/find.js +1 -1
  12. package/dist/commands/knowledge.js +2 -0
  13. package/dist/commands/mailboxes.js +3 -2
  14. package/dist/commands/messages.js +6 -1
  15. package/dist/commands/senders.js +2 -0
  16. package/dist/commands/sequences.js +1 -1
  17. package/dist/commands/signals.js +11 -2
  18. package/dist/deliverability-evidence-commands.d.ts +4 -0
  19. package/dist/deliverability-evidence-commands.js +104 -0
  20. package/dist/diagnostic-context.d.ts +21 -0
  21. package/dist/diagnostic-context.js +25 -0
  22. package/dist/diagnostics.d.ts +1 -3
  23. package/dist/diagnostics.js +12 -30
  24. package/dist/http-client.js +1 -1
  25. package/dist/network-mining-host.d.ts +6 -0
  26. package/dist/network-mining-host.js +10 -0
  27. package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +1 -1
  28. package/node_modules/@oxygen/formula/dist/value-normalizers.js +5 -0
  29. package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +169 -0
  30. package/node_modules/@oxygen/shared/dist/agent-sessions.js +17 -0
  31. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
  32. package/node_modules/@oxygen/shared/dist/capability-discovery.js +114 -5
  33. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
  34. package/node_modules/@oxygen/shared/dist/contracts/operation-request.d.ts +92 -0
  35. package/node_modules/@oxygen/shared/dist/contracts/operation-request.js +36 -0
  36. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +3 -90
  37. package/node_modules/@oxygen/shared/dist/contracts/operations.js +5 -39
  38. package/node_modules/@oxygen/shared/dist/contracts/reply-oxygent.d.ts +2 -0
  39. package/node_modules/@oxygen/shared/dist/contracts/reply-oxygent.js +123 -0
  40. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +1 -0
  41. package/node_modules/@oxygen/shared/dist/crm-activity-events.d.ts +4 -0
  42. package/node_modules/@oxygen/shared/dist/crm-activity-events.js +1 -0
  43. package/node_modules/@oxygen/shared/dist/crm-record-brief.d.ts +10 -0
  44. package/node_modules/@oxygen/shared/dist/crm-record-brief.js +22 -0
  45. package/node_modules/@oxygen/shared/dist/deliverability-blacklist.d.ts +74 -0
  46. package/node_modules/@oxygen/shared/dist/deliverability-blacklist.js +118 -0
  47. package/node_modules/@oxygen/shared/dist/deliverability-checks.d.ts +138 -0
  48. package/node_modules/@oxygen/shared/dist/deliverability-checks.js +124 -0
  49. package/node_modules/@oxygen/shared/dist/deliverability-evidence.d.ts +366 -0
  50. package/node_modules/@oxygen/shared/dist/deliverability-evidence.js +169 -0
  51. package/node_modules/@oxygen/shared/dist/deliverability-loop.d.ts +334 -0
  52. package/node_modules/@oxygen/shared/dist/deliverability-loop.js +312 -0
  53. package/node_modules/@oxygen/shared/dist/deliverability-surface.d.ts +42 -0
  54. package/node_modules/@oxygen/shared/dist/deliverability-surface.js +66 -0
  55. package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +34 -4
  56. package/node_modules/@oxygen/shared/dist/domain-authentication.d.ts +24 -0
  57. package/node_modules/@oxygen/shared/dist/domain-authentication.js +20 -0
  58. package/node_modules/@oxygen/shared/dist/file-import.js +11 -3
  59. package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.d.ts +16 -0
  60. package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.js +30 -2
  61. package/node_modules/@oxygen/shared/dist/network-mining.d.ts +5 -0
  62. package/node_modules/@oxygen/shared/dist/network-mining.js +9 -0
  63. package/node_modules/@oxygen/shared/dist/received-authentication.d.ts +74 -0
  64. package/node_modules/@oxygen/shared/dist/received-authentication.js +452 -0
  65. package/node_modules/@oxygen/shared/dist/recipient-provider.d.ts +21 -0
  66. package/node_modules/@oxygen/shared/dist/recipient-provider.js +33 -0
  67. package/node_modules/@oxygen/shared/dist/reply-oxygent.d.ts +73 -0
  68. package/node_modules/@oxygen/shared/dist/reply-oxygent.js +31 -0
  69. package/node_modules/@oxygen/shared/dist/repricing.d.ts +10 -1
  70. package/node_modules/@oxygen/shared/dist/repricing.js +16 -4
  71. package/node_modules/@oxygen/shared/dist/research-findings.d.ts +80 -0
  72. package/node_modules/@oxygen/shared/dist/research-findings.js +199 -0
  73. package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.d.ts +2 -0
  74. package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.js +4 -0
  75. package/node_modules/@oxygen/shared/dist/sequences.d.ts +6 -0
  76. package/node_modules/@oxygen/shared/dist/sequences.js +8 -0
  77. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +20 -3
  78. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  79. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  80. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.d.ts +8 -2
  81. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +63 -0
  82. 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 release = await storageStage("lock", () => acquireDiagnosticStorageLock(join(directory, `.upload-${id.slice(0, 2).toLowerCase()}.lock`), waitMs));
141
- if (!release)
142
- throw new StorageFailure("lock", { code: "lock_timeout" });
143
- return release;
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 and exact keys claim their columns before any alias can.
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.replace(/^urn:li:(?:fs_miniProfile|fsd_profile|member):/, "")}`] : [];
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.trim().replace(/^urn:li:(?:fs_miniProfile|fsd_profile|member):/, ""));
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;