@oxygen-agent/cli 1.1003.12 → 1.1010.644
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/column-decision-options.d.ts +20 -0
- package/dist/column-decision-options.js +54 -0
- package/dist/command-manifest.js +15 -2
- package/dist/functions-commands.js +11 -11
- package/dist/index.js +1222 -159
- package/dist/search-ai-filter-notice.d.ts +17 -0
- package/dist/search-ai-filter-notice.js +38 -0
- package/dist/skills.js +34 -10
- package/dist/util.d.ts +9 -0
- package/dist/util.js +14 -0
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +296 -140
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +9 -0
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +34 -0
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/billing.js +45 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.js +5 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +223 -13
- package/node_modules/@oxygen/shared/dist/cli-http-error.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/cli-http-error.js +8 -0
- package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
- package/node_modules/@oxygen/shared/dist/column-autofill.js +5 -23
- package/node_modules/@oxygen/shared/dist/column-decision.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/column-decision.js +228 -0
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +9 -4
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +11 -8
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +4 -4
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +4 -4
- package/node_modules/@oxygen/shared/dist/cutover-freeze.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/cutover-freeze.js +52 -0
- package/node_modules/@oxygen/shared/dist/data-suppliers.d.ts +57 -0
- package/node_modules/@oxygen/shared/dist/data-suppliers.js +59 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +6 -2
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +13 -23
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +60 -4
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +125 -10
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/index.js +2 -0
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +44 -1
- package/node_modules/@oxygen/shared/dist/langfuse.js +407 -14
- package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.js +2 -0
- package/node_modules/@oxygen/shared/dist/linkedin-country-timezones.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/linkedin-country-timezones.js +276 -0
- package/node_modules/@oxygen/shared/dist/linkedin-message-deletion.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/linkedin-message-deletion.js +5 -0
- package/node_modules/@oxygen/shared/dist/linkedin-post-keywords.d.ts +44 -0
- package/node_modules/@oxygen/shared/dist/linkedin-post-keywords.js +116 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +96 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +123 -0
- package/node_modules/@oxygen/shared/dist/llm-durable-capture.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/llm-durable-capture.js +89 -0
- package/node_modules/@oxygen/shared/dist/llm-prompts.d.ts +75 -0
- package/node_modules/@oxygen/shared/dist/llm-prompts.js +161 -0
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +90 -0
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.js +130 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.js +73 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +29 -4
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +189 -36
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +21 -2
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +21 -1
- package/node_modules/@oxygen/shared/dist/redaction.js +4 -1
- package/node_modules/@oxygen/shared/dist/scraper-lane-credential.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/scraper-lane-credential.js +23 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +5 -1
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +80 -0
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +198 -0
- package/node_modules/@oxygen/shared/dist/social-capabilities.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/social-capabilities.js +25 -16
- package/node_modules/@oxygen/shared/dist/social-post-metrics-core.d.ts +32 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-core.js +32 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-linkedin.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-linkedin.js +103 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-series.d.ts +96 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +213 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-x.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-x.js +78 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics.js +51 -0
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +184 -0
- package/node_modules/@oxygen/shared/dist/stripe-subscription-kind.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/stripe-subscription-kind.js +44 -0
- package/node_modules/@oxygen/shared/dist/table-limits.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/table-limits.js +3 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +94 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +298 -0
- package/node_modules/@oxygen/shared/dist/telemetry.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +19 -1
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +22 -11
- package/node_modules/@oxygen/shared/dist/ugc.js +10 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/version.generated.js +2 -0
- package/node_modules/@oxygen/shared/dist/version.js +8 -1
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +0 -23
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +31 -0
- package/node_modules/@oxygen/shared/package.json +9 -0
- package/package.json +2 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type LogLevel, type LogSink } from "./log.js";
|
|
2
2
|
/**
|
|
3
3
|
* Ships `log()` records to an OTLP/HTTP logs receiver (the self-hosted SigNoz
|
|
4
4
|
* collector) instead of — or alongside — Axiom.
|
|
@@ -19,9 +19,16 @@ import type { LogSink } from "./log.js";
|
|
|
19
19
|
* inside `log()`, so it must never await, throw, or block), the queue drops the
|
|
20
20
|
* OLDEST record on overflow because the newest lines describe the incident being
|
|
21
21
|
* debugged, and a collector outage is a dropped batch rather than a process
|
|
22
|
-
* failure. Failures
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* failure. Failures and queue overflow are reported through `log()`, at most
|
|
23
|
+
* once per minute each whatever the flush interval (a dead collector behind a
|
|
24
|
+
* 2 s flush would otherwise write ~43k lines a day into Axiom), with the
|
|
25
|
+
* failures and drops a suppressed report would have carried accumulated into
|
|
26
|
+
* the next one, under the re-entrancy guard the Axiom shippers use
|
|
27
|
+
* (`emittingInternal`): this sink skips the records it emits itself, so a broken
|
|
28
|
+
* collector cannot feed its own failure line back into its queue, while every
|
|
29
|
+
* sink composed beside it (Axiom under `OXYGEN_LOG_SINK=both`) and stdout still
|
|
30
|
+
* receive the line. `log()` always writes stdout before it calls a sink, which
|
|
31
|
+
* is why the former direct stderr write is not kept as a fallback.
|
|
25
32
|
*/
|
|
26
33
|
export type OtlpLogSinkOptions = {
|
|
27
34
|
/** Full receiver URL, e.g. `http://collector.internal:4318/v1/logs`. */
|
|
@@ -36,6 +43,14 @@ export type OtlpLogSinkOptions = {
|
|
|
36
43
|
/** `0` disables the interval timer, for runtimes that flush off a request lifecycle. */
|
|
37
44
|
flushIntervalMs?: number | undefined;
|
|
38
45
|
maxQueue?: number | undefined;
|
|
46
|
+
/** Clock for the report throttle; tests only. */
|
|
47
|
+
now?: (() => number) | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Lowest level this sink ships; records below it never enter the queue. Null
|
|
50
|
+
* or absent ships every level. Sinks composed beside this one (Axiom) and
|
|
51
|
+
* stdout are unaffected; see `resolveOtlpLogMinLevel`.
|
|
52
|
+
*/
|
|
53
|
+
minLevel?: LogLevel | null | undefined;
|
|
39
54
|
fetch?: typeof globalThis.fetch | undefined;
|
|
40
55
|
};
|
|
41
56
|
export type OtlpLogSinkHandle = {
|
|
@@ -43,6 +58,16 @@ export type OtlpLogSinkHandle = {
|
|
|
43
58
|
flush: () => Promise<void>;
|
|
44
59
|
stop: () => Promise<void>;
|
|
45
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Reads `OXYGEN_LOG_OTLP_MIN_LEVEL` (debug|info|warn|error, case-insensitive).
|
|
63
|
+
*
|
|
64
|
+
* The OTLP collector is the one transport a noisy tier can drown, so it alone
|
|
65
|
+
* gets a floor; Axiom and stdout keep every level. Unset ships everything, which
|
|
66
|
+
* is the behaviour before the variable existed. An unrecognised value is named
|
|
67
|
+
* through `log()` and treated as unset: dropping more than was asked is the
|
|
68
|
+
* failure an operator cannot see, so a typo errs toward shipping.
|
|
69
|
+
*/
|
|
70
|
+
export declare function resolveOtlpLogMinLevel(env?: NodeJS.ProcessEnv): LogLevel | null;
|
|
46
71
|
/**
|
|
47
72
|
* `k=v,k=v` — the same shape as `OTEL_EXPORTER_OTLP_HEADERS`, so an operator who
|
|
48
73
|
* knows the OTel variable can set ours without learning a second syntax. A pair
|
|
@@ -1,14 +1,86 @@
|
|
|
1
|
+
import { log } from "./log.js";
|
|
2
|
+
import { resolveDeployEnv } from "./deploy-env.js";
|
|
3
|
+
import { sanitizeLogFields } from "./redaction.js";
|
|
4
|
+
import { trace } from "@opentelemetry/api";
|
|
5
|
+
import { operationalLogSnapshot } from "./operational-telemetry.js";
|
|
6
|
+
import { telemetryEndpointForLog } from "./telemetry-export-observer.js";
|
|
1
7
|
const DEFAULT_BATCH_SIZE = 500;
|
|
2
8
|
const DEFAULT_FLUSH_INTERVAL_MS = 2_000;
|
|
3
9
|
const DEFAULT_MAX_QUEUE = 10_000;
|
|
4
10
|
const EXPORT_TIMEOUT_MS = 10_000;
|
|
5
|
-
const
|
|
6
|
-
const
|
|
11
|
+
const REPORT_WINDOW_MS = 60_000;
|
|
12
|
+
const MAX_RESPONSE_BYTES = 65_536;
|
|
13
|
+
async function rejectedRecords(response, batchLength) {
|
|
14
|
+
const reader = response.body?.getReader();
|
|
15
|
+
if (!reader)
|
|
16
|
+
return 0;
|
|
17
|
+
const decoder = new TextDecoder();
|
|
18
|
+
let text = "";
|
|
19
|
+
let bytes = 0;
|
|
20
|
+
try {
|
|
21
|
+
for (;;) {
|
|
22
|
+
const chunk = await reader.read();
|
|
23
|
+
if (chunk.done)
|
|
24
|
+
break;
|
|
25
|
+
bytes += chunk.value.byteLength;
|
|
26
|
+
if (bytes > MAX_RESPONSE_BYTES)
|
|
27
|
+
throw new Error("invalid_response");
|
|
28
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
29
|
+
}
|
|
30
|
+
text += decoder.decode();
|
|
31
|
+
if (!text.trim())
|
|
32
|
+
return 0;
|
|
33
|
+
const data = JSON.parse(text);
|
|
34
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
35
|
+
throw new Error("invalid_response");
|
|
36
|
+
const partial = data.partialSuccess;
|
|
37
|
+
const rejected = Number(partial?.rejectedLogRecords ?? 0);
|
|
38
|
+
if (!Number.isSafeInteger(rejected) || rejected < 0 || rejected > batchLength)
|
|
39
|
+
throw new Error("invalid_response");
|
|
40
|
+
return rejected;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await reader.cancel().catch(() => undefined);
|
|
44
|
+
reader.releaseLock();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
7
47
|
// OTLP severity numbers (logs data model): the base of each 4-value band.
|
|
8
48
|
const SEVERITY_NUMBERS = { debug: 5, info: 9, warn: 13, error: 17 };
|
|
9
49
|
// Set by log() on every record; they become the OTLP record's own fields rather
|
|
10
50
|
// than attributes, so carrying them twice would just inflate every batch.
|
|
11
|
-
const RECORD_FIELDS_NOT_ATTRIBUTES = new Set(["ts", "level", "msg"]);
|
|
51
|
+
const RECORD_FIELDS_NOT_ATTRIBUTES = new Set(["ts", "level", "msg", "trace_id", "span_id"]);
|
|
52
|
+
/**
|
|
53
|
+
* Reads `OXYGEN_LOG_OTLP_MIN_LEVEL` (debug|info|warn|error, case-insensitive).
|
|
54
|
+
*
|
|
55
|
+
* The OTLP collector is the one transport a noisy tier can drown, so it alone
|
|
56
|
+
* gets a floor; Axiom and stdout keep every level. Unset ships everything, which
|
|
57
|
+
* is the behaviour before the variable existed. An unrecognised value is named
|
|
58
|
+
* through `log()` and treated as unset: dropping more than was asked is the
|
|
59
|
+
* failure an operator cannot see, so a typo errs toward shipping.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveOtlpLogMinLevel(env = process.env) {
|
|
62
|
+
const raw = env.OXYGEN_LOG_OTLP_MIN_LEVEL?.trim().toLowerCase();
|
|
63
|
+
if (!raw)
|
|
64
|
+
return null;
|
|
65
|
+
if (Object.hasOwn(SEVERITY_NUMBERS, raw))
|
|
66
|
+
return raw;
|
|
67
|
+
log("warn", "otlp_log_sink.min_level_invalid", {
|
|
68
|
+
min_level: raw.slice(0, 32),
|
|
69
|
+
allowed: Object.keys(SEVERITY_NUMBERS).join("|"),
|
|
70
|
+
});
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The `code` of a transport error's `cause` (undici puts ECONNREFUSED,
|
|
75
|
+
* UND_ERR_CONNECT_TIMEOUT and the like there), and nothing else: the message
|
|
76
|
+
* and the error itself can carry the request URL and, with it, collector
|
|
77
|
+
* tokens. Anything that is not a short identifier-shaped code is dropped.
|
|
78
|
+
*/
|
|
79
|
+
function sanitizedErrorCause(error) {
|
|
80
|
+
const cause = error && typeof error === "object" ? error.cause : undefined;
|
|
81
|
+
const code = cause && typeof cause === "object" ? cause.code : undefined;
|
|
82
|
+
return typeof code === "string" && /^[A-Z][A-Z0-9_]{1,63}$/.test(code) ? code : null;
|
|
83
|
+
}
|
|
12
84
|
/**
|
|
13
85
|
* `k=v,k=v` — the same shape as `OTEL_EXPORTER_OTLP_HEADERS`, so an operator who
|
|
14
86
|
* knows the OTel variable can set ours without learning a second syntax. A pair
|
|
@@ -71,15 +143,37 @@ function timeUnixNano(ts) {
|
|
|
71
143
|
// Decimal string, not a number: nanoseconds since the epoch exceed 2^53.
|
|
72
144
|
return `${millis}000000`;
|
|
73
145
|
}
|
|
74
|
-
|
|
75
|
-
|
|
146
|
+
/**
|
|
147
|
+
* The identity contract's tier when `OXYGEN_DEPLOY_ENV` is set, else null. An
|
|
148
|
+
* invalid value is the boot guard's to refuse; a telemetry sink created before
|
|
149
|
+
* that guard must not throw on it and take the logger down with it.
|
|
150
|
+
*/
|
|
151
|
+
function deployTierOrNull() {
|
|
152
|
+
if (!process.env.OXYGEN_DEPLOY_ENV)
|
|
153
|
+
return null;
|
|
154
|
+
try {
|
|
155
|
+
return resolveDeployEnv().tier;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function recordLevel(record) {
|
|
162
|
+
return typeof record.level === "string" && Object.hasOwn(SEVERITY_NUMBERS, record.level)
|
|
76
163
|
? record.level
|
|
77
164
|
: "info";
|
|
165
|
+
}
|
|
166
|
+
function toLogRecord(record) {
|
|
167
|
+
const level = recordLevel(record);
|
|
78
168
|
return {
|
|
79
169
|
timeUnixNano: timeUnixNano(record.ts),
|
|
80
170
|
severityNumber: SEVERITY_NUMBERS[level],
|
|
81
171
|
severityText: level.toUpperCase(),
|
|
82
172
|
body: { stringValue: typeof record.msg === "string" ? record.msg : String(record.msg ?? "") },
|
|
173
|
+
...(typeof record.trace_id === "string" && /^[a-f\d]{32}$/i.test(record.trace_id)
|
|
174
|
+
&& !/^0+$/.test(record.trace_id) ? { traceId: record.trace_id.toLowerCase() } : {}),
|
|
175
|
+
...(typeof record.span_id === "string" && /^[a-f\d]{16}$/i.test(record.span_id)
|
|
176
|
+
&& !/^0+$/.test(record.span_id) ? { spanId: record.span_id.toLowerCase() } : {}),
|
|
83
177
|
attributes: toAttributes(record, RECORD_FIELDS_NOT_ATTRIBUTES),
|
|
84
178
|
};
|
|
85
179
|
}
|
|
@@ -90,54 +184,105 @@ export function createOtlpLogSink(options) {
|
|
|
90
184
|
? DEFAULT_FLUSH_INTERVAL_MS
|
|
91
185
|
: options.flushIntervalMs;
|
|
92
186
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
93
|
-
const
|
|
187
|
+
const now = options.now ?? Date.now;
|
|
188
|
+
const minSeverity = options.minLevel ? SEVERITY_NUMBERS[options.minLevel] : null;
|
|
94
189
|
// A null/undefined override is dropped rather than applied, so a caller that
|
|
95
190
|
// resolves its own environment and comes back empty-handed falls back to the
|
|
96
191
|
// default here instead of shipping records with no `deployment.environment`.
|
|
97
192
|
const resourceOverrides = Object.fromEntries(Object.entries(options.resource ?? {}).filter(([, value]) => value !== null && value !== undefined));
|
|
98
193
|
const resourceAttributes = toAttributes({
|
|
99
194
|
"service.name": options.serviceName,
|
|
100
|
-
"deployment.environment":
|
|
195
|
+
"deployment.environment": deployTierOrNull()
|
|
101
196
|
?? process.env.VERCEL_ENV
|
|
102
197
|
?? process.env.NODE_ENV
|
|
103
198
|
?? null,
|
|
199
|
+
"deployment.sha": process.env.OXYGEN_GIT_SHA ?? process.env.VERCEL_GIT_COMMIT_SHA,
|
|
104
200
|
...resourceOverrides,
|
|
105
201
|
});
|
|
106
202
|
const queue = [];
|
|
107
203
|
let inFlight = null;
|
|
108
204
|
let stopped = false;
|
|
109
|
-
let lastReportAt =
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
205
|
+
let lastReportAt = Number.NEGATIVE_INFINITY;
|
|
206
|
+
let lastOverflowReportAt = Number.NEGATIVE_INFINITY;
|
|
207
|
+
// Failed exports a throttled report skipped, and the records they dropped;
|
|
208
|
+
// both ride on the next export_failed line, as telemetry.export_failed does.
|
|
209
|
+
let suppressedFailures = 0;
|
|
210
|
+
let pendingDropped = 0;
|
|
211
|
+
// Records the overflow path dropped that no report has carried yet.
|
|
212
|
+
let droppedOverflow = 0;
|
|
213
|
+
// Re-entrancy guard, as in apps/worker/src/log-shipper.ts. log() calls this
|
|
214
|
+
// sink synchronously, so while it is set the record on offer is this sink's
|
|
215
|
+
// own report: skipped here, so a failing flush never enqueues its own error
|
|
216
|
+
// line and re-fails on it forever.
|
|
217
|
+
let emittingInternal = false;
|
|
218
|
+
const endpointForLog = telemetryEndpointForLog(options.endpoint);
|
|
219
|
+
const emitInternal = (msg, fields) => {
|
|
220
|
+
emittingInternal = true;
|
|
119
221
|
try {
|
|
120
|
-
|
|
121
|
-
level: "error",
|
|
122
|
-
msg: "otlp_log_sink.export_failed",
|
|
123
|
-
endpoint: options.endpoint,
|
|
124
|
-
...fields,
|
|
125
|
-
})}\n`);
|
|
222
|
+
log("warn", msg, { endpoint: endpointForLog, ...fields });
|
|
126
223
|
}
|
|
127
224
|
catch {
|
|
128
225
|
// A telemetry failure that cannot even be reported is still not a fault
|
|
129
226
|
// of the code being observed.
|
|
130
227
|
}
|
|
228
|
+
finally {
|
|
229
|
+
emittingInternal = false;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
const report = (droppedEvents, fields) => {
|
|
233
|
+
pendingDropped += droppedEvents;
|
|
234
|
+
const at = now();
|
|
235
|
+
if (at - lastReportAt < REPORT_WINDOW_MS) {
|
|
236
|
+
suppressedFailures += 1;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
lastReportAt = at;
|
|
240
|
+
const overflow = droppedOverflow;
|
|
241
|
+
const dropped = pendingDropped;
|
|
242
|
+
const suppressed = suppressedFailures;
|
|
243
|
+
droppedOverflow = 0;
|
|
244
|
+
pendingDropped = 0;
|
|
245
|
+
suppressedFailures = 0;
|
|
246
|
+
emitInternal("otlp_log_sink.export_failed", {
|
|
247
|
+
...fields,
|
|
248
|
+
dropped_events: dropped,
|
|
249
|
+
dropped_overflow: overflow,
|
|
250
|
+
suppressed_failures: suppressed,
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
// Overflow is data loss even while every export succeeds (a burst faster than
|
|
254
|
+
// the collector drains), so it is reported on its own when no failure line
|
|
255
|
+
// has carried it.
|
|
256
|
+
const reportOverflow = () => {
|
|
257
|
+
if (droppedOverflow === 0)
|
|
258
|
+
return;
|
|
259
|
+
const at = now();
|
|
260
|
+
if (at - lastOverflowReportAt < REPORT_WINDOW_MS)
|
|
261
|
+
return;
|
|
262
|
+
lastOverflowReportAt = at;
|
|
263
|
+
const overflow = droppedOverflow;
|
|
264
|
+
droppedOverflow = 0;
|
|
265
|
+
emitInternal("otlp_log_sink.queue_overflow", { dropped_events: overflow });
|
|
131
266
|
};
|
|
132
267
|
const sink = (record) => {
|
|
133
|
-
if (stopped)
|
|
268
|
+
if (stopped || emittingInternal)
|
|
269
|
+
return;
|
|
270
|
+
// An unknown level ships as INFO (see toLogRecord), so it filters as one.
|
|
271
|
+
if (minSeverity !== null && SEVERITY_NUMBERS[recordLevel(record)] < minSeverity)
|
|
134
272
|
return;
|
|
135
273
|
// Drop the OLDEST record and keep this one: under sustained backpressure the
|
|
136
274
|
// newest lines are the ones describing the incident being debugged.
|
|
137
275
|
while (queue.length >= maxQueue) {
|
|
138
276
|
queue.shift();
|
|
277
|
+
droppedOverflow += 1;
|
|
139
278
|
}
|
|
140
|
-
|
|
279
|
+
const activeSpan = trace.getActiveSpan()?.spanContext();
|
|
280
|
+
queue.push(operationalLogSnapshot(sanitizeLogFields({
|
|
281
|
+
...record,
|
|
282
|
+
...(activeSpan && (!record.trace_id || record.trace_id === activeSpan.traceId)
|
|
283
|
+
? { trace_id: activeSpan.traceId, span_id: activeSpan.spanId }
|
|
284
|
+
: {}),
|
|
285
|
+
})));
|
|
141
286
|
};
|
|
142
287
|
const flush = async () => {
|
|
143
288
|
if (inFlight)
|
|
@@ -166,25 +311,33 @@ export function createOtlpLogSink(options) {
|
|
|
166
311
|
body: payload,
|
|
167
312
|
signal: AbortSignal.timeout(EXPORT_TIMEOUT_MS),
|
|
168
313
|
});
|
|
169
|
-
if (response.ok)
|
|
314
|
+
if (response.ok) {
|
|
315
|
+
try {
|
|
316
|
+
const rejected = await rejectedRecords(response, batch.length);
|
|
317
|
+
if (rejected)
|
|
318
|
+
report(rejected, { status: response.status, error_code: "partial_rejection" });
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
report(batch.length, { status: response.status, error_code: "invalid_response" });
|
|
322
|
+
}
|
|
170
323
|
return;
|
|
171
|
-
let body = "";
|
|
172
|
-
try {
|
|
173
|
-
body = (await response.text()).slice(0, MAX_ERROR_BODY);
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
// An unreadable body is no worse than a status-only report.
|
|
177
324
|
}
|
|
325
|
+
// Receivers/proxies can echo rejected payloads or credentials. Never
|
|
326
|
+
// read arbitrary response text into operational diagnostics.
|
|
327
|
+
await response.body?.cancel().catch(() => undefined);
|
|
178
328
|
// The batch is gone deliberately: retrying into an unhealthy collector
|
|
179
329
|
// is how a telemetry path turns an ingest blip into memory pressure.
|
|
180
|
-
report({ status: response.status,
|
|
330
|
+
report(batch.length, { status: response.status, error_code: "receiver_rejected" });
|
|
181
331
|
}
|
|
182
332
|
catch (error) {
|
|
183
|
-
report({
|
|
184
|
-
|
|
185
|
-
|
|
333
|
+
report(batch.length, {
|
|
334
|
+
error_code: "transport_failed",
|
|
335
|
+
error_cause: sanitizedErrorCause(error),
|
|
186
336
|
});
|
|
187
337
|
}
|
|
338
|
+
finally {
|
|
339
|
+
reportOverflow();
|
|
340
|
+
}
|
|
188
341
|
})();
|
|
189
342
|
inFlight = run.finally(() => {
|
|
190
343
|
inFlight = null;
|
|
@@ -42,7 +42,6 @@ export declare const PRODUCT_EVENTS: {
|
|
|
42
42
|
readonly FIRST_PLAY_STARTED: "first_play_started";
|
|
43
43
|
readonly FIRST_PLAY_STEP_COMPLETED: "first_play_step_completed";
|
|
44
44
|
readonly FIRST_PLAY_COMPLETED: "first_play_completed";
|
|
45
|
-
readonly FIRST_PLAY_SKIPPED: "first_play_skipped";
|
|
46
45
|
/**
|
|
47
46
|
* Usage — what was ASKED for. `mcp_tool_called` used to sit here as the
|
|
48
47
|
* second highest-volume event; it was REPLACED (not joined) by the reserved
|
|
@@ -70,13 +69,33 @@ export declare const PRODUCT_EVENTS: {
|
|
|
70
69
|
readonly TABLE_INGESTION_RUN_SETTLED: "table_ingestion_run_settled";
|
|
71
70
|
readonly AGENT_RUN_SETTLED: "agent_run_settled";
|
|
72
71
|
readonly SEQUENCE_ENROLLMENT_SETTLED: "sequence_enrollment_settled";
|
|
72
|
+
/**
|
|
73
|
+
* A Workspace Copilot turn reached completed / failed / cancelled. The
|
|
74
|
+
* Copilot is a Control surface, not a primitive, but its turns are durable
|
|
75
|
+
* worker runs like the others and it had no outcome event at all. Counts,
|
|
76
|
+
* credits and ids only — never the user message or the assistant text.
|
|
77
|
+
*/
|
|
78
|
+
readonly COPILOT_TURN_SETTLED: "copilot_turn_settled";
|
|
73
79
|
/** Revenue lifecycle. */
|
|
80
|
+
/**
|
|
81
|
+
* A Stripe Checkout was opened for a first subscription. `source` says which
|
|
82
|
+
* surface opened it (a paywall, the sidebar Upgrade row, Billing) and
|
|
83
|
+
* `blocked_capability` which wall, so signup → wall → checkout →
|
|
84
|
+
* `subscription_activated` is one funnel instead of a gap between pageviews.
|
|
85
|
+
*/
|
|
86
|
+
readonly CHECKOUT_STARTED: "checkout_started";
|
|
74
87
|
readonly TRIAL_STARTED: "trial_started";
|
|
75
88
|
readonly SUBSCRIPTION_ACTIVATED: "subscription_activated";
|
|
76
89
|
readonly SUBSCRIPTION_CHURNED: "subscription_churned";
|
|
77
90
|
readonly TRIAL_CANCEL_SCHEDULED: "trial_cancel_scheduled";
|
|
78
91
|
readonly TRIAL_CANCEL_RESUMED: "trial_cancel_resumed";
|
|
79
92
|
readonly TRIAL_ENDED_UNCONVERTED: "trial_ended_unconverted";
|
|
93
|
+
/**
|
|
94
|
+
* A paid credit top-up was granted (the Stripe Checkout fulfilment, after the
|
|
95
|
+
* ledger grant committed). `uuid` derives from the checkout session, so the
|
|
96
|
+
* webhook, the success redirect and every redelivery collapse to one event.
|
|
97
|
+
*/
|
|
98
|
+
readonly CREDITS_PURCHASED: "credits_purchased";
|
|
80
99
|
/**
|
|
81
100
|
* Marketing site. The public hero lookup on oxygen-agent.com: an ANONYMOUS
|
|
82
101
|
* visitor types their website and OXYGEN spends its own credits profiling it.
|
|
@@ -127,5 +146,5 @@ export declare const PRODUCT_EVENT_NAMES: readonly ProductEventName[];
|
|
|
127
146
|
* emitter cannot be handed a lifecycle event that only the control plane has
|
|
128
147
|
* the facts for.
|
|
129
148
|
*/
|
|
130
|
-
export declare const WORKER_RUN_OUTCOME_EVENTS: readonly ["workflow_run_settled", "table_action_run_settled", "table_ingestion_run_settled", "agent_run_settled", "sequence_enrollment_settled"];
|
|
149
|
+
export declare const WORKER_RUN_OUTCOME_EVENTS: readonly ["workflow_run_settled", "table_action_run_settled", "table_ingestion_run_settled", "agent_run_settled", "sequence_enrollment_settled", "copilot_turn_settled"];
|
|
131
150
|
export type WorkerRunOutcomeEventName = (typeof WORKER_RUN_OUTCOME_EVENTS)[number];
|
|
@@ -42,7 +42,6 @@ export const PRODUCT_EVENTS = {
|
|
|
42
42
|
FIRST_PLAY_STARTED: "first_play_started",
|
|
43
43
|
FIRST_PLAY_STEP_COMPLETED: "first_play_step_completed",
|
|
44
44
|
FIRST_PLAY_COMPLETED: "first_play_completed",
|
|
45
|
-
FIRST_PLAY_SKIPPED: "first_play_skipped",
|
|
46
45
|
/**
|
|
47
46
|
* Usage — what was ASKED for. `mcp_tool_called` used to sit here as the
|
|
48
47
|
* second highest-volume event; it was REPLACED (not joined) by the reserved
|
|
@@ -70,13 +69,33 @@ export const PRODUCT_EVENTS = {
|
|
|
70
69
|
TABLE_INGESTION_RUN_SETTLED: "table_ingestion_run_settled",
|
|
71
70
|
AGENT_RUN_SETTLED: "agent_run_settled",
|
|
72
71
|
SEQUENCE_ENROLLMENT_SETTLED: "sequence_enrollment_settled",
|
|
72
|
+
/**
|
|
73
|
+
* A Workspace Copilot turn reached completed / failed / cancelled. The
|
|
74
|
+
* Copilot is a Control surface, not a primitive, but its turns are durable
|
|
75
|
+
* worker runs like the others and it had no outcome event at all. Counts,
|
|
76
|
+
* credits and ids only — never the user message or the assistant text.
|
|
77
|
+
*/
|
|
78
|
+
COPILOT_TURN_SETTLED: "copilot_turn_settled",
|
|
73
79
|
/** Revenue lifecycle. */
|
|
80
|
+
/**
|
|
81
|
+
* A Stripe Checkout was opened for a first subscription. `source` says which
|
|
82
|
+
* surface opened it (a paywall, the sidebar Upgrade row, Billing) and
|
|
83
|
+
* `blocked_capability` which wall, so signup → wall → checkout →
|
|
84
|
+
* `subscription_activated` is one funnel instead of a gap between pageviews.
|
|
85
|
+
*/
|
|
86
|
+
CHECKOUT_STARTED: "checkout_started",
|
|
74
87
|
TRIAL_STARTED: "trial_started",
|
|
75
88
|
SUBSCRIPTION_ACTIVATED: "subscription_activated",
|
|
76
89
|
SUBSCRIPTION_CHURNED: "subscription_churned",
|
|
77
90
|
TRIAL_CANCEL_SCHEDULED: "trial_cancel_scheduled",
|
|
78
91
|
TRIAL_CANCEL_RESUMED: "trial_cancel_resumed",
|
|
79
92
|
TRIAL_ENDED_UNCONVERTED: "trial_ended_unconverted",
|
|
93
|
+
/**
|
|
94
|
+
* A paid credit top-up was granted (the Stripe Checkout fulfilment, after the
|
|
95
|
+
* ledger grant committed). `uuid` derives from the checkout session, so the
|
|
96
|
+
* webhook, the success redirect and every redelivery collapse to one event.
|
|
97
|
+
*/
|
|
98
|
+
CREDITS_PURCHASED: "credits_purchased",
|
|
80
99
|
/**
|
|
81
100
|
* Marketing site. The public hero lookup on oxygen-agent.com: an ANONYMOUS
|
|
82
101
|
* visitor types their website and OXYGEN spends its own credits profiling it.
|
|
@@ -132,4 +151,5 @@ export const WORKER_RUN_OUTCOME_EVENTS = Object.freeze([
|
|
|
132
151
|
PRODUCT_EVENTS.TABLE_INGESTION_RUN_SETTLED,
|
|
133
152
|
PRODUCT_EVENTS.AGENT_RUN_SETTLED,
|
|
134
153
|
PRODUCT_EVENTS.SEQUENCE_ENROLLMENT_SETTLED,
|
|
154
|
+
PRODUCT_EVENTS.COPILOT_TURN_SETTLED,
|
|
135
155
|
]);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const SECRET_KEY_PATTERN = /(api[_-]?key|x[_-]?key|authorization|bearer|cookie|password|secret|token|ciphertext|connection[_-]?string|connection[_-]?uri|database[_-]?url|dsn)/i;
|
|
2
|
-
|
|
2
|
+
// `content_preview` is raw model output (ai-column-runner's invalid_ai_output
|
|
3
|
+
// details); web routes log `error_details` on a 4xx, and model text belongs in
|
|
4
|
+
// Langfuse only (ADR 0014), never in operational logs.
|
|
5
|
+
const OMITTED_KEY_PATTERN = /^(body|payload|prompt|prompts|raw_prompt|raw_prompts|row|rows|input|inputs|output|outputs|request|response|provider_payload|provider_response|customer_data|content_preview)$/i;
|
|
3
6
|
// OXY-125: keyed redaction (SECRET_KEY_PATTERN) only catches whole fields named
|
|
4
7
|
// like a secret. Credentials also leak as *substrings* of otherwise-ordinary
|
|
5
8
|
// fields — most commonly an Authorization header dumped into error_message /
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which key a LinkedIn scraper lane runs on (Philipp, 2026-09-25). Every lane
|
|
3
|
+
* defaults to OXYGEN's managed credits; a workspace may instead run it on its own
|
|
4
|
+
* account with the lane's vendor, which OXYGEN then never bills. The vendor is a
|
|
5
|
+
* property of the lane, so a caller picks only the mode.
|
|
6
|
+
*
|
|
7
|
+
* Browser-safe on purpose: the New table lanes render the menu from this list.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SCRAPER_LANE_CREDENTIAL_MODES: readonly ["managed", "user_api_key"];
|
|
10
|
+
export type ScraperLaneCredentialMode = (typeof SCRAPER_LANE_CREDENTIAL_MODES)[number];
|
|
11
|
+
export type ScraperLane = "linkedin_posts" | "linkedin_post_engagers" | "linkedin_company_followers";
|
|
12
|
+
/** The integration that stores each lane's customer key, and its display name. */
|
|
13
|
+
export declare const SCRAPER_LANE_KEY_VENDOR: Readonly<Record<ScraperLane, {
|
|
14
|
+
integrationId: "up2data" | "scrapeli";
|
|
15
|
+
name: string;
|
|
16
|
+
}>>;
|
|
17
|
+
/** Absent means managed; anything outside the two modes is null, for the caller to refuse. */
|
|
18
|
+
export declare function readScraperLaneCredentialMode(value: unknown): ScraperLaneCredentialMode | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which key a LinkedIn scraper lane runs on (Philipp, 2026-09-25). Every lane
|
|
3
|
+
* defaults to OXYGEN's managed credits; a workspace may instead run it on its own
|
|
4
|
+
* account with the lane's vendor, which OXYGEN then never bills. The vendor is a
|
|
5
|
+
* property of the lane, so a caller picks only the mode.
|
|
6
|
+
*
|
|
7
|
+
* Browser-safe on purpose: the New table lanes render the menu from this list.
|
|
8
|
+
*/
|
|
9
|
+
export const SCRAPER_LANE_CREDENTIAL_MODES = ["managed", "user_api_key"];
|
|
10
|
+
/** The integration that stores each lane's customer key, and its display name. */
|
|
11
|
+
export const SCRAPER_LANE_KEY_VENDOR = {
|
|
12
|
+
linkedin_posts: { integrationId: "up2data", name: "Up2Data" },
|
|
13
|
+
linkedin_post_engagers: { integrationId: "up2data", name: "Up2Data" },
|
|
14
|
+
linkedin_company_followers: { integrationId: "scrapeli", name: "ScrapeLi" },
|
|
15
|
+
};
|
|
16
|
+
/** Absent means managed; anything outside the two modes is null, for the caller to refuse. */
|
|
17
|
+
export function readScraperLaneCredentialMode(value) {
|
|
18
|
+
if (value === undefined || value === null || value === "")
|
|
19
|
+
return "managed";
|
|
20
|
+
return SCRAPER_LANE_CREDENTIAL_MODES.includes(value)
|
|
21
|
+
? value
|
|
22
|
+
: null;
|
|
23
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OxygenError } from "./cli-result.js";
|
|
2
2
|
import { hashVariantKey, renderTemplate, spintaxSyntaxIssues, templateColumnKeys, } from "./sequence-template.js";
|
|
3
3
|
import { isRecord } from "./type-guards.js";
|
|
4
|
+
import { LINKEDIN_INVITE_NOTE_LIMIT } from "./linkedin-sequences.js";
|
|
4
5
|
/**
|
|
5
6
|
* Multichannel sequence DSL — the shared contract validated identically by CLI,
|
|
6
7
|
* MCP, API, and web. A sequence is an ordered list of steps applied to each
|
|
@@ -997,7 +998,10 @@ export const SEQUENCE_CRM_IDENTITY_KINDS = [
|
|
|
997
998
|
export const SEQUENCE_CONNECTION_BRANCH_CONDITIONS = ["connection_accepted", "already_connected", "open_profile"];
|
|
998
999
|
const MAX_SEQUENCE_STEPS = 50;
|
|
999
1000
|
const MAX_TEMPLATE_LENGTH = 8_000;
|
|
1000
|
-
|
|
1001
|
+
// The Premium ceiling, and only an authoring hint: the limit that actually
|
|
1002
|
+
// binds is the SENDER's, which is lower on a Basic account and is enforced
|
|
1003
|
+
// against the RENDERED note at dispatch. See LINKEDIN_INVITE_NOTE_LIMIT_BASIC.
|
|
1004
|
+
const MAX_NOTE_LENGTH = LINKEDIN_INVITE_NOTE_LIMIT;
|
|
1001
1005
|
const MAX_CRM_TASK_PROPERTIES = 100;
|
|
1002
1006
|
const MAX_CRM_TASK_ASSOCIATIONS = 20;
|
|
1003
1007
|
const MAX_CRM_RECORD_MAPPINGS = 10;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export type JsonRecord = Record<string, unknown>;
|
|
2
|
+
export type SignupLeadWebhookSource = "clerk" | "onboarding" | "backfill" | "stripe";
|
|
3
|
+
export type CustomerLifecycleEvent = "signup"
|
|
4
|
+
/** Creator research completed by the onboarding bootstrap; produced by @oxygen/control-db, never by Clerk. */
|
|
5
|
+
| "signup_research" | "trial_created" | "paid" | "churned";
|
|
6
|
+
export type SignupLeadWebhookInput = {
|
|
7
|
+
eventId?: string | null | undefined;
|
|
8
|
+
providerEventId?: string | null | undefined;
|
|
9
|
+
type: string;
|
|
10
|
+
source: SignupLeadWebhookSource;
|
|
11
|
+
lifecycleEvent?: CustomerLifecycleEvent | null | undefined;
|
|
12
|
+
occurredAt?: Date | string | number | null | undefined;
|
|
13
|
+
user?: {
|
|
14
|
+
clerkUserId?: string | null;
|
|
15
|
+
email?: string | null;
|
|
16
|
+
firstName?: string | null;
|
|
17
|
+
lastName?: string | null;
|
|
18
|
+
createdAt?: Date | string | number | null | undefined;
|
|
19
|
+
/** A profile URL the provider already knows for this person; the consent-recorded creator URL still wins at enqueue. */
|
|
20
|
+
linkedinUrl?: string | null;
|
|
21
|
+
};
|
|
22
|
+
organization?: {
|
|
23
|
+
clerkOrgId?: string | null;
|
|
24
|
+
name?: string | null;
|
|
25
|
+
slug?: string | null;
|
|
26
|
+
domain?: string | null;
|
|
27
|
+
createdAt?: Date | string | number | null | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Clerk user id of the organization's creator (Clerk `organization.created_by`).
|
|
30
|
+
* Downstream qualification only treats the exact creator as an acquisition
|
|
31
|
+
* lead; invited members get onboarding but no founder outreach.
|
|
32
|
+
*/
|
|
33
|
+
createdBy?: string | null;
|
|
34
|
+
};
|
|
35
|
+
membership?: {
|
|
36
|
+
role?: string | null;
|
|
37
|
+
};
|
|
38
|
+
stripe?: {
|
|
39
|
+
customerId?: string | null;
|
|
40
|
+
subscriptionId?: string | null;
|
|
41
|
+
invoiceId?: string | null;
|
|
42
|
+
priceId?: string | null;
|
|
43
|
+
tier?: string | null;
|
|
44
|
+
status?: string | null;
|
|
45
|
+
trialStart?: Date | string | number | null | undefined;
|
|
46
|
+
trialEnd?: Date | string | number | null | undefined;
|
|
47
|
+
periodStart?: Date | string | number | null | undefined;
|
|
48
|
+
periodEnd?: Date | string | number | null | undefined;
|
|
49
|
+
amountPaid?: number | null;
|
|
50
|
+
currency?: string | null;
|
|
51
|
+
billingReason?: string | null;
|
|
52
|
+
};
|
|
53
|
+
metadata?: JsonRecord;
|
|
54
|
+
/** Allow-listed research evidence (`person_facts` / `company_facts`) when the producer already holds it. */
|
|
55
|
+
qualificationEvidence?: JsonRecord;
|
|
56
|
+
/** Research intent or status attached to the fact (`expected`, `scope`, `run_id`, ...). */
|
|
57
|
+
research?: JsonRecord;
|
|
58
|
+
rawEvent?: JsonRecord;
|
|
59
|
+
};
|
|
60
|
+
export type SignupLeadWebhookPayload = JsonRecord & {
|
|
61
|
+
id: string;
|
|
62
|
+
event_id: string;
|
|
63
|
+
event: string;
|
|
64
|
+
type: string;
|
|
65
|
+
occurred_at: string;
|
|
66
|
+
source: SignupLeadWebhookSource;
|
|
67
|
+
event_type: string;
|
|
68
|
+
provider_event_id: string;
|
|
69
|
+
};
|
|
70
|
+
export type WorkEmailClassification = {
|
|
71
|
+
domain: string | null;
|
|
72
|
+
isGenericMailbox: boolean;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Classifies an email address into a (likely) company work-email domain.
|
|
76
|
+
* Consumer/generic mailboxes return `{ domain: null, isGenericMailbox: true }`.
|
|
77
|
+
*/
|
|
78
|
+
export declare function classifyWorkEmail(email: string | null | undefined): WorkEmailClassification;
|
|
79
|
+
export declare function isGenericMailboxDomain(domain: string | null | undefined): boolean;
|
|
80
|
+
export declare function buildSignupLeadWebhookPayload(input: SignupLeadWebhookInput): SignupLeadWebhookPayload;
|