@oxygen-agent/cli 1.162.10 → 1.177.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/http-client.js +2 -78
- package/dist/index.js +1312 -527
- package/dist/run-wait.d.ts +23 -0
- package/dist/run-wait.js +57 -0
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +0 -5
- package/node_modules/@oxygen/shared/dist/cell-format.d.ts +2 -14
- package/node_modules/@oxygen/shared/dist/cell-format.js +3 -10
- package/node_modules/@oxygen/shared/dist/cli-envelope.d.ts +27 -0
- package/node_modules/@oxygen/shared/dist/cli-envelope.js +102 -0
- package/node_modules/@oxygen/shared/dist/cli-result.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/cli-result.js +52 -0
- package/node_modules/@oxygen/shared/dist/credit-guidance.d.ts +0 -1
- package/node_modules/@oxygen/shared/dist/credit-guidance.js +1 -1
- package/node_modules/@oxygen/shared/dist/file-import.js +1 -1
- package/node_modules/@oxygen/shared/dist/index.d.ts +3 -39
- package/node_modules/@oxygen/shared/dist/index.js +3 -44
- package/node_modules/@oxygen/shared/dist/log.d.ts +0 -1
- package/node_modules/@oxygen/shared/dist/log.js +8 -3
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +0 -3
- package/node_modules/@oxygen/shared/dist/object-storage.js +1 -24
- package/node_modules/@oxygen/shared/dist/redaction.js +45 -4
- package/node_modules/@oxygen/shared/dist/search-vocab.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/search-vocab.js +151 -0
- package/node_modules/@oxygen/shared/dist/select-options.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/select-options.js +121 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +1 -1
- package/node_modules/@oxygen/shared/dist/sql-error.d.ts +0 -6
- package/node_modules/@oxygen/shared/dist/sql-error.js +67 -58
- package/node_modules/@oxygen/shared/dist/telemetry.d.ts +0 -1
- package/node_modules/@oxygen/shared/dist/telemetry.js +23 -18
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/dist/worker-failures-queue.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/worker-failures-queue.js +56 -0
- package/node_modules/@oxygen/workflows/dist/index.d.ts +12 -11
- package/node_modules/@oxygen/workflows/dist/index.js +58 -0
- package/package.json +1 -1
|
@@ -1,5 +1,29 @@
|
|
|
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
|
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)$/i;
|
|
3
|
+
// OXY-125: keyed redaction (SECRET_KEY_PATTERN) only catches whole fields named
|
|
4
|
+
// like a secret. Credentials also leak as *substrings* of otherwise-ordinary
|
|
5
|
+
// fields — most commonly an Authorization header dumped into error_message /
|
|
6
|
+
// error_stack. This matches the HTTP bearer scheme followed by its token (JWT /
|
|
7
|
+
// base64 / opaque, including our `oxy_live_`/`oxy_sess_` prefixes) anywhere in a
|
|
8
|
+
// string.
|
|
9
|
+
const BEARER_TOKEN_PATTERN = /\bBearer\s+[\w.~+/=-]+/gi;
|
|
10
|
+
// OXY-139: the prod log-hygiene sweep also flags `sk-…` API keys and DB
|
|
11
|
+
// connection URLs that ride along inside ordinary (non-secret-named) fields, so
|
|
12
|
+
// keyed + Bearer redaction is not enough. We scrub the two shapes that carry real
|
|
13
|
+
// secret *material* as substrings of any value:
|
|
14
|
+
// • DB URLs — the whole `postgres(ql)://…` (incl. scheme) so the marker cannot
|
|
15
|
+
// re-trip the sweep's `postgres://[^ ]+` clause.
|
|
16
|
+
// • `sk-…` keys — body allowed to contain `_`/`-` and no word boundary, matching
|
|
17
|
+
// the sweep regex byte-for-byte. This also neutralises the OXY-139 false
|
|
18
|
+
// positive: a Vercel `x-vercel-id` whose first segment ends in `sk`
|
|
19
|
+
// (e.g. `2g5sk-1781047899561-…`, logged as request_id on every cron summary
|
|
20
|
+
// line) is not a credential but is sweep-shaped, so it must be redacted to
|
|
21
|
+
// clear the signal; trace_id stays intact as the correlation key.
|
|
22
|
+
// The bare `OPENAI_API_KEY` *name* is deliberately NOT scrubbed — it is a variable
|
|
23
|
+
// name, not a secret, and hiding it would suppress legitimate "OPENAI_API_KEY is
|
|
24
|
+
// missing" diagnostics (its actual value is an `sk-…` string, already covered).
|
|
25
|
+
const DB_URL_PATTERN = /(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|rediss?|amqps?):\/\/[^\s"'<>()]+/gi;
|
|
26
|
+
const SK_TOKEN_PATTERN = /sk-[A-Za-z0-9_-]{20,}/g;
|
|
3
27
|
const MAX_LOG_STRING_LENGTH = 8_000;
|
|
4
28
|
const MAX_TELEMETRY_STRING_LENGTH = 500;
|
|
5
29
|
const MAX_ARRAY_LENGTH = 20;
|
|
@@ -36,7 +60,7 @@ function sanitizeValueForLog(value, key, depth) {
|
|
|
36
60
|
if (value === null || value === undefined)
|
|
37
61
|
return value;
|
|
38
62
|
if (typeof value === "string")
|
|
39
|
-
return truncateString(value, MAX_LOG_STRING_LENGTH);
|
|
63
|
+
return truncateString(redactSecretsInString(value), MAX_LOG_STRING_LENGTH);
|
|
40
64
|
if (typeof value === "number")
|
|
41
65
|
return Number.isFinite(value) ? value : null;
|
|
42
66
|
if (typeof value === "boolean")
|
|
@@ -67,7 +91,7 @@ function normalizeTelemetryValue(value, key) {
|
|
|
67
91
|
if (OMITTED_KEY_PATTERN.test(key))
|
|
68
92
|
return "[omitted]";
|
|
69
93
|
if (typeof value === "string")
|
|
70
|
-
return truncateString(value, MAX_TELEMETRY_STRING_LENGTH);
|
|
94
|
+
return truncateString(redactSecretsInString(value), MAX_TELEMETRY_STRING_LENGTH);
|
|
71
95
|
if (typeof value === "number")
|
|
72
96
|
return Number.isFinite(value) ? value : undefined;
|
|
73
97
|
if (typeof value === "boolean")
|
|
@@ -80,8 +104,12 @@ function normalizeTelemetryValue(value, key) {
|
|
|
80
104
|
.filter((entry) => typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean");
|
|
81
105
|
if (primitive.length === 0)
|
|
82
106
|
return undefined;
|
|
107
|
+
// Array elements need the same substring scrub as scalar strings: a string[]
|
|
108
|
+
// attribute (e.g. dumped header lines) carries the same Bearer/sk-/DB-URL
|
|
109
|
+
// material, and this branch was the one telemetry path that exported it
|
|
110
|
+
// verbatim. Redact before truncating so a cut cannot expose token material.
|
|
83
111
|
if (primitive.every((entry) => typeof entry === "string")) {
|
|
84
|
-
return primitive.map((entry) => truncateString(
|
|
112
|
+
return primitive.map((entry) => truncateString(redactSecretsInString(entry), MAX_TELEMETRY_STRING_LENGTH));
|
|
85
113
|
}
|
|
86
114
|
if (primitive.every((entry) => typeof entry === "number" && Number.isFinite(entry))) {
|
|
87
115
|
return primitive;
|
|
@@ -89,7 +117,7 @@ function normalizeTelemetryValue(value, key) {
|
|
|
89
117
|
if (primitive.every((entry) => typeof entry === "boolean")) {
|
|
90
118
|
return primitive;
|
|
91
119
|
}
|
|
92
|
-
return primitive.map((entry) => truncateString(String(entry), MAX_TELEMETRY_STRING_LENGTH));
|
|
120
|
+
return primitive.map((entry) => truncateString(redactSecretsInString(String(entry)), MAX_TELEMETRY_STRING_LENGTH));
|
|
93
121
|
}
|
|
94
122
|
if (typeof value === "object" && value) {
|
|
95
123
|
return truncateString(JSON.stringify(sanitizeValueForLog(value, key, 0)), MAX_TELEMETRY_STRING_LENGTH);
|
|
@@ -100,6 +128,19 @@ function sanitizeAttributeKey(key) {
|
|
|
100
128
|
const normalized = key.trim().replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 120);
|
|
101
129
|
return normalized || null;
|
|
102
130
|
}
|
|
131
|
+
// Replaces credential-shaped substrings with token-free markers. Order matters:
|
|
132
|
+
// Bearer first so an `Authorization: Bearer sk-…` header collapses to a single
|
|
133
|
+
// `Bearer[REDACTED]` instead of leaving a stray `Bearer ` (the sweep matches
|
|
134
|
+
// `Bearer ` with a trailing space). Each marker is chosen so a second pass — and
|
|
135
|
+
// every prod-sweep regex (`Bearer `, `sk-…`, `postgres://…`) — finds nothing, so
|
|
136
|
+
// redaction stays idempotent. Sharing the global regexes is safe because
|
|
137
|
+
// String.replace resets their lastIndex between calls.
|
|
138
|
+
function redactSecretsInString(value) {
|
|
139
|
+
return value
|
|
140
|
+
.replace(BEARER_TOKEN_PATTERN, "Bearer[REDACTED]")
|
|
141
|
+
.replace(DB_URL_PATTERN, "[REDACTED_DB_URL]")
|
|
142
|
+
.replace(SK_TOKEN_PATTERN, "[REDACTED_SK]");
|
|
143
|
+
}
|
|
103
144
|
function truncateString(value, maxLength) {
|
|
104
145
|
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
|
|
105
146
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function normalizeText(value: string): string;
|
|
2
|
+
export declare function escapeRegExp(value: string): string;
|
|
3
|
+
export declare function extractUrls(value: string): string[];
|
|
4
|
+
export declare function extractDomains(value: string): string[];
|
|
5
|
+
export declare function parseMoney(amount: string, unit: string | undefined): number;
|
|
6
|
+
export declare function regionDisplayName(code: string): string | null;
|
|
7
|
+
export declare const COUNTRY_NAME_TO_ISO: Map<string, string>;
|
|
8
|
+
export declare const COUNTRY_ALIAS_TO_ISO: Record<string, string>;
|
|
9
|
+
export declare const CITY_TO_ISO: Record<string, string>;
|
|
10
|
+
export declare const KNOWN_TECHNOLOGIES: readonly ["salesforce", "hubspot", "shopify", "stripe", "aws", "azure", "gcp", "google cloud", "snowflake", "databricks", "postgres", "postgresql", "mysql", "mongodb", "redis", "kafka", "kubernetes", "docker", "react", "nextjs", "next.js", "vue", "angular", "node", "python", "ruby on rails", "django", "segment", "amplitude", "mixpanel", "intercom", "zendesk", "marketo", "pardot", "outreach", "salesloft", "gong", "clari", "netsuite", "workday", "sap", "oracle", "twilio", "klaviyo", "webflow", "wordpress", "magento", "bigcommerce", "woocommerce"];
|
|
11
|
+
export declare const INDUSTRY_TERMS: readonly ["saas", "b2b saas", "fintech", "healthtech", "healthcare", "edtech", "martech", "adtech", "proptech", "insurtech", "legaltech", "hrtech", "regtech", "biotech", "medtech", "cleantech", "climate tech", "cybersecurity", "security", "devtools", "developer tools", "e-commerce", "ecommerce", "logistics", "supply chain", "manufacturing", "construction", "real estate", "hospitality", "retail", "gaming", "media", "telecom", "automotive", "aerospace", "agritech", "foodtech", "ai", "artificial intelligence", "machine learning", "data analytics", "consulting", "staffing", "recruiting"];
|
|
12
|
+
export declare const EMPLOYEE_SEGMENTS: Record<string, {
|
|
13
|
+
min?: number;
|
|
14
|
+
max?: number;
|
|
15
|
+
}>;
|
|
16
|
+
export declare const KEYWORD_STOPWORDS: Set<string>;
|
|
17
|
+
export declare const DEPARTMENT_TERMS: Record<string, string>;
|
|
18
|
+
export declare const SENIORITY_TERMS: Record<string, string>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Shared search-planner vocabulary + stateless text helpers.
|
|
2
|
+
//
|
|
3
|
+
// Single source of truth for the constraint vocabulary and pure text helpers
|
|
4
|
+
// consumed by BOTH the legacy web search planners
|
|
5
|
+
// (apps/web/src/lib/company-search-plan.ts, people-search-plan.ts) and the
|
|
6
|
+
// @oxygen/routing classifier/compiler (via packages/routing/src/vocab.ts,
|
|
7
|
+
// which layers its named tuning deltas on top of these bases). Keeping one copy
|
|
8
|
+
// here is what stops the two surfaces from silently drifting (OXY-414).
|
|
9
|
+
//
|
|
10
|
+
// Everything here is a pure function or a static table: no Date.now, no
|
|
11
|
+
// randomness, no I/O — safe to import from any layer.
|
|
12
|
+
export function normalizeText(value) {
|
|
13
|
+
return value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
14
|
+
}
|
|
15
|
+
export function escapeRegExp(value) {
|
|
16
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17
|
+
}
|
|
18
|
+
export function extractUrls(value) {
|
|
19
|
+
return [...new Set((value.match(/https?:\/\/[^\s)]+/gi) ?? []).map((url) => url.replace(/[.,;]+$/, "")))];
|
|
20
|
+
}
|
|
21
|
+
export function extractDomains(value) {
|
|
22
|
+
const domains = value.match(/\b(?:[a-z0-9-]+\.)+[a-z]{2,}\b/gi) ?? [];
|
|
23
|
+
return [...new Set(domains.map((domain) => domain.toLowerCase()).filter((domain) => !domain.includes("@")))];
|
|
24
|
+
}
|
|
25
|
+
export function parseMoney(amount, unit) {
|
|
26
|
+
const value = Number(amount);
|
|
27
|
+
const normalized = (unit ?? "").toLowerCase();
|
|
28
|
+
if (normalized.startsWith("b"))
|
|
29
|
+
return Math.round(value * 1_000_000_000);
|
|
30
|
+
if (normalized.startsWith("m"))
|
|
31
|
+
return Math.round(value * 1_000_000);
|
|
32
|
+
if (normalized.startsWith("k"))
|
|
33
|
+
return Math.round(value * 1_000);
|
|
34
|
+
return Math.round(value);
|
|
35
|
+
}
|
|
36
|
+
// ISO 3166-1 alpha-2 handling is backed by the platform's Intl region data instead of
|
|
37
|
+
// a hardcoded country table, so every assigned code validates and renders a display name.
|
|
38
|
+
const REGION_DISPLAY = new Intl.DisplayNames(["en"], { type: "region" });
|
|
39
|
+
export function regionDisplayName(code) {
|
|
40
|
+
if (!/^[A-Z]{2}$/.test(code))
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
const name = REGION_DISPLAY.of(code);
|
|
44
|
+
return name && name !== code ? name : null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Lowercased country display name -> ISO code, generated from Intl region data at module load.
|
|
51
|
+
export const COUNTRY_NAME_TO_ISO = (() => {
|
|
52
|
+
const map = new Map();
|
|
53
|
+
for (let first = 65; first <= 90; first += 1) {
|
|
54
|
+
for (let second = 65; second <= 90; second += 1) {
|
|
55
|
+
const code = String.fromCharCode(first) + String.fromCharCode(second);
|
|
56
|
+
const name = regionDisplayName(code);
|
|
57
|
+
if (name)
|
|
58
|
+
map.set(name.toLowerCase(), code);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return map;
|
|
62
|
+
})();
|
|
63
|
+
// Common aliases and demonyms that Intl display names do not cover. Keys are lowercase prompt phrases.
|
|
64
|
+
export const COUNTRY_ALIAS_TO_ISO = {
|
|
65
|
+
"usa": "US", "u.s.": "US", "u.s.a.": "US", "us": "US", "america": "US", "american": "US", "the states": "US",
|
|
66
|
+
"uk": "GB", "u.k.": "GB", "britain": "GB", "great britain": "GB", "british": "GB", "england": "GB",
|
|
67
|
+
"german": "DE", "french": "FR", "dutch": "NL", "the netherlands": "NL", "holland": "NL",
|
|
68
|
+
"canadian": "CA", "australian": "AU", "austrian": "AT", "swiss": "CH", "swedish": "SE", "danish": "DK",
|
|
69
|
+
"norwegian": "NO", "finnish": "FI", "spanish": "ES", "italian": "IT", "portuguese": "PT", "polish": "PL",
|
|
70
|
+
"irish": "IE", "belgian": "BE", "brazilian": "BR", "mexican": "MX", "indian": "IN", "japanese": "JP",
|
|
71
|
+
"korean": "KR", "south korean": "KR", "chinese": "CN", "singaporean": "SG", "israeli": "IL",
|
|
72
|
+
"emirati": "AE", "uae": "AE", "saudi": "SA", "south african": "ZA", "nordic": "SE", "scandinavian": "SE",
|
|
73
|
+
};
|
|
74
|
+
// Major business-hub cities -> country code, used as geo hints when no country is named.
|
|
75
|
+
export const CITY_TO_ISO = {
|
|
76
|
+
"san francisco": "US", "new york": "US", "nyc": "US", "boston": "US", "austin": "US", "seattle": "US",
|
|
77
|
+
"los angeles": "US", "chicago": "US", "miami": "US", "denver": "US", "atlanta": "US",
|
|
78
|
+
"london": "GB", "manchester": "GB", "berlin": "DE", "munich": "DE", "hamburg": "DE", "cologne": "DE",
|
|
79
|
+
"paris": "FR", "amsterdam": "NL", "rotterdam": "NL", "stockholm": "SE", "copenhagen": "DK", "oslo": "NO",
|
|
80
|
+
"helsinki": "FI", "zurich": "CH", "geneva": "CH", "vienna": "AT", "madrid": "ES", "barcelona": "ES",
|
|
81
|
+
"milan": "IT", "lisbon": "PT", "dublin": "IE", "brussels": "BE", "warsaw": "PL", "toronto": "CA",
|
|
82
|
+
"vancouver": "CA", "montreal": "CA", "sydney": "AU", "melbourne": "AU", "singapore": "SG",
|
|
83
|
+
"tokyo": "JP", "tel aviv": "IL", "dubai": "AE", "bangalore": "IN", "mumbai": "IN", "sao paulo": "BR",
|
|
84
|
+
};
|
|
85
|
+
// Common B2B technologies recognized at query time. Unrecognized "using <X>" phrases become unparsed hints.
|
|
86
|
+
export const KNOWN_TECHNOLOGIES = [
|
|
87
|
+
"salesforce", "hubspot", "shopify", "stripe", "aws", "azure", "gcp", "google cloud", "snowflake",
|
|
88
|
+
"databricks", "postgres", "postgresql", "mysql", "mongodb", "redis", "kafka", "kubernetes", "docker",
|
|
89
|
+
"react", "nextjs", "next.js", "vue", "angular", "node", "python", "ruby on rails", "django",
|
|
90
|
+
"segment", "amplitude", "mixpanel", "intercom", "zendesk", "marketo", "pardot", "outreach",
|
|
91
|
+
"salesloft", "gong", "clari", "netsuite", "workday", "sap", "oracle", "twilio", "klaviyo", "webflow",
|
|
92
|
+
"wordpress", "magento", "bigcommerce", "woocommerce",
|
|
93
|
+
];
|
|
94
|
+
// Industry-ish terms that go to both keywords and industries (provider enum resolution is reported separately).
|
|
95
|
+
export const INDUSTRY_TERMS = [
|
|
96
|
+
"saas", "b2b saas", "fintech", "healthtech", "healthcare", "edtech", "martech", "adtech", "proptech",
|
|
97
|
+
"insurtech", "legaltech", "hrtech", "regtech", "biotech", "medtech", "cleantech", "climate tech",
|
|
98
|
+
"cybersecurity", "security", "devtools", "developer tools", "e-commerce", "ecommerce", "logistics",
|
|
99
|
+
"supply chain", "manufacturing", "construction", "real estate", "hospitality", "retail", "gaming",
|
|
100
|
+
"media", "telecom", "automotive", "aerospace", "agritech", "foodtech", "ai", "artificial intelligence",
|
|
101
|
+
"machine learning", "data analytics", "consulting", "staffing", "recruiting",
|
|
102
|
+
];
|
|
103
|
+
export const EMPLOYEE_SEGMENTS = {
|
|
104
|
+
"smb": { min: 1, max: 200 },
|
|
105
|
+
"small business": { min: 1, max: 200 },
|
|
106
|
+
"small businesses": { min: 1, max: 200 },
|
|
107
|
+
"mid-market": { min: 50, max: 1000 },
|
|
108
|
+
"mid market": { min: 50, max: 1000 },
|
|
109
|
+
"midmarket": { min: 50, max: 1000 },
|
|
110
|
+
"enterprise": { min: 1000 },
|
|
111
|
+
"enterprises": { min: 1000 },
|
|
112
|
+
"startup": { min: 1, max: 50 },
|
|
113
|
+
"startups": { min: 1, max: 50 },
|
|
114
|
+
"scaleup": { min: 50, max: 500 },
|
|
115
|
+
"scaleups": { min: 50, max: 500 },
|
|
116
|
+
};
|
|
117
|
+
// Words to drop when harvesting free-text keyword phrases (geo/size words are captured by dedicated extractors).
|
|
118
|
+
export const KEYWORD_STOPWORDS = new Set([
|
|
119
|
+
"find", "source", "build", "list", "of", "for", "the", "a", "an", "top", "best", "new", "all", "some",
|
|
120
|
+
"more", "with", "and", "or", "in", "at", "us", "uk", "eu", "based", "headquartered", "located",
|
|
121
|
+
"small", "large", "mid-market", "enterprise", "smb", "startup", "early-stage", "growth-stage",
|
|
122
|
+
]);
|
|
123
|
+
// Department phrase -> canonical department label (people search).
|
|
124
|
+
export const DEPARTMENT_TERMS = {
|
|
125
|
+
sales: "Sales",
|
|
126
|
+
marketing: "Marketing",
|
|
127
|
+
engineering: "Engineering",
|
|
128
|
+
product: "Product",
|
|
129
|
+
finance: "Finance",
|
|
130
|
+
"human resources": "Human Resources",
|
|
131
|
+
"people ops": "Human Resources",
|
|
132
|
+
recruiting: "Human Resources",
|
|
133
|
+
operations: "Operations",
|
|
134
|
+
revops: "Revenue Operations",
|
|
135
|
+
"revenue operations": "Revenue Operations",
|
|
136
|
+
"customer success": "Customer Success",
|
|
137
|
+
support: "Customer Success",
|
|
138
|
+
legal: "Legal",
|
|
139
|
+
it: "Information Technology",
|
|
140
|
+
security: "Information Technology",
|
|
141
|
+
data: "Data",
|
|
142
|
+
design: "Design",
|
|
143
|
+
};
|
|
144
|
+
// Seniority phrase -> canonical seniority band (people search).
|
|
145
|
+
export const SENIORITY_TERMS = {
|
|
146
|
+
"c-suite": "C-Suite", "c-level": "C-Suite", chief: "C-Suite", founder: "C-Suite", ceo: "C-Suite",
|
|
147
|
+
cto: "C-Suite", cfo: "C-Suite", coo: "C-Suite", cmo: "C-Suite", cro: "C-Suite", president: "C-Suite",
|
|
148
|
+
vp: "VP", "vice president": "VP", svp: "VP", evp: "VP",
|
|
149
|
+
director: "Director", "head of": "Director",
|
|
150
|
+
manager: "Manager", lead: "Manager", principal: "Manager",
|
|
151
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const TAG_COLORS: readonly ["gray", "red", "orange", "amber", "yellow", "green", "teal", "blue", "indigo", "purple", "pink"];
|
|
2
|
+
export type TagColor = (typeof TAG_COLORS)[number];
|
|
3
|
+
export declare const DEFAULT_TAG_COLOR: TagColor;
|
|
4
|
+
export declare function isTagColor(value: unknown): value is TagColor;
|
|
5
|
+
export declare function tagColorForIndex(index: number): TagColor;
|
|
6
|
+
export declare const SELECT_COLUMN_SEMANTICS: readonly ["select", "single_select", "multi_select", "tags", "status"];
|
|
7
|
+
export type SelectColumnSemantic = (typeof SELECT_COLUMN_SEMANTICS)[number];
|
|
8
|
+
export declare function isSelectColumnSemantic(semanticType: string | null | undefined): boolean;
|
|
9
|
+
export declare function isMultiSelectSemantic(semanticType: string | null | undefined): boolean;
|
|
10
|
+
export declare function isStatusSemantic(semanticType: string | null | undefined): boolean;
|
|
11
|
+
export type SelectOption = {
|
|
12
|
+
id: string;
|
|
13
|
+
value: string;
|
|
14
|
+
label: string;
|
|
15
|
+
color: TagColor;
|
|
16
|
+
order: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function normalizeSelectOptions(raw: unknown): SelectOption[];
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Canonical colored option model for select / multi-select / status columns.
|
|
2
|
+
//
|
|
3
|
+
// Shared across server validation, the web option editor, colored-pill
|
|
4
|
+
// rendering, filters, and the kanban view so every surface agrees. Colors are
|
|
5
|
+
// NAMED (theme-safe), stored once on the option — never raw hex — mirroring how
|
|
6
|
+
// Attio/Notion model status & select option colors.
|
|
7
|
+
export const TAG_COLORS = [
|
|
8
|
+
"gray",
|
|
9
|
+
"red",
|
|
10
|
+
"orange",
|
|
11
|
+
"amber",
|
|
12
|
+
"yellow",
|
|
13
|
+
"green",
|
|
14
|
+
"teal",
|
|
15
|
+
"blue",
|
|
16
|
+
"indigo",
|
|
17
|
+
"purple",
|
|
18
|
+
"pink",
|
|
19
|
+
];
|
|
20
|
+
export const DEFAULT_TAG_COLOR = "gray";
|
|
21
|
+
export function isTagColor(value) {
|
|
22
|
+
return typeof value === "string" && TAG_COLORS.includes(value);
|
|
23
|
+
}
|
|
24
|
+
// Auto-assign colors for fresh options, skipping gray so a new column looks
|
|
25
|
+
// colorful out of the box; gray stays a deliberate manual choice.
|
|
26
|
+
export function tagColorForIndex(index) {
|
|
27
|
+
const palette = TAG_COLORS.filter((color) => color !== "gray");
|
|
28
|
+
return palette[Math.abs(index) % palette.length] ?? DEFAULT_TAG_COLOR;
|
|
29
|
+
}
|
|
30
|
+
// Semantic types (the column's `semanticType`) that present a fixed option set.
|
|
31
|
+
// `select`/`status` are single-value; `multi_select`/`tags` are array-valued.
|
|
32
|
+
// `single_select` is accepted as an alias of `select`.
|
|
33
|
+
export const SELECT_COLUMN_SEMANTICS = [
|
|
34
|
+
"select",
|
|
35
|
+
"single_select",
|
|
36
|
+
"multi_select",
|
|
37
|
+
"tags",
|
|
38
|
+
"status",
|
|
39
|
+
];
|
|
40
|
+
export function isSelectColumnSemantic(semanticType) {
|
|
41
|
+
return Boolean(semanticType) && SELECT_COLUMN_SEMANTICS.includes(semanticType);
|
|
42
|
+
}
|
|
43
|
+
export function isMultiSelectSemantic(semanticType) {
|
|
44
|
+
return semanticType === "multi_select" || semanticType === "tags";
|
|
45
|
+
}
|
|
46
|
+
export function isStatusSemantic(semanticType) {
|
|
47
|
+
return semanticType === "status";
|
|
48
|
+
}
|
|
49
|
+
const MAX_OPTIONS = 200;
|
|
50
|
+
// Normalize a raw options array (from a column `definition.options`, CLI/MCP, or
|
|
51
|
+
// the editor) into canonical SelectOptions: dedupe by value, default colors,
|
|
52
|
+
// canonical 0..n ordering. Accepts strings or {value,label,color,order} objects.
|
|
53
|
+
export function normalizeSelectOptions(raw) {
|
|
54
|
+
if (!Array.isArray(raw))
|
|
55
|
+
return [];
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const collected = [];
|
|
58
|
+
for (const entry of raw) {
|
|
59
|
+
const option = normalizeSelectOption(entry, collected.length);
|
|
60
|
+
if (!option)
|
|
61
|
+
continue;
|
|
62
|
+
const dedupeKey = option.value.toLowerCase();
|
|
63
|
+
if (seen.has(dedupeKey))
|
|
64
|
+
continue;
|
|
65
|
+
seen.add(dedupeKey);
|
|
66
|
+
collected.push(option);
|
|
67
|
+
if (collected.length >= MAX_OPTIONS)
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
collected.sort((a, b) => a.order - b.order);
|
|
71
|
+
return collected.map((option, index) => ({ ...option, order: index }));
|
|
72
|
+
}
|
|
73
|
+
function normalizeSelectOption(entry, fallbackIndex) {
|
|
74
|
+
if (typeof entry === "string")
|
|
75
|
+
return optionFromString(entry, fallbackIndex);
|
|
76
|
+
if (!entry || typeof entry !== "object")
|
|
77
|
+
return null;
|
|
78
|
+
const record = entry;
|
|
79
|
+
const rawValue = readOptionValue(record);
|
|
80
|
+
if (!rawValue)
|
|
81
|
+
return null;
|
|
82
|
+
const label = readTrimmedString(record.label) ?? rawValue;
|
|
83
|
+
const id = readTrimmedString(record.id) ?? slugifyOption(rawValue);
|
|
84
|
+
const color = isTagColor(record.color) ? record.color : tagColorForIndex(fallbackIndex);
|
|
85
|
+
const order = typeof record.order === "number" && Number.isFinite(record.order) ? record.order : fallbackIndex;
|
|
86
|
+
return { id, value: rawValue, label, color, order };
|
|
87
|
+
}
|
|
88
|
+
// A bare string entry becomes a fully-defaulted option (value == label == the
|
|
89
|
+
// trimmed string); blank strings are dropped.
|
|
90
|
+
function optionFromString(entry, fallbackIndex) {
|
|
91
|
+
const value = entry.trim();
|
|
92
|
+
if (!value)
|
|
93
|
+
return null;
|
|
94
|
+
return {
|
|
95
|
+
id: slugifyOption(value),
|
|
96
|
+
value,
|
|
97
|
+
label: value,
|
|
98
|
+
color: tagColorForIndex(fallbackIndex),
|
|
99
|
+
order: fallbackIndex,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// The cell value comes from `value`, falling back to `label`; "" when neither is
|
|
103
|
+
// a non-blank string (so the caller drops the option).
|
|
104
|
+
function readOptionValue(record) {
|
|
105
|
+
return readTrimmedString(record.value) ?? readTrimmedString(record.label) ?? "";
|
|
106
|
+
}
|
|
107
|
+
// Returns the trimmed string when the field is a non-blank string, else null.
|
|
108
|
+
function readTrimmedString(value) {
|
|
109
|
+
if (typeof value !== "string")
|
|
110
|
+
return null;
|
|
111
|
+
const trimmed = value.trim();
|
|
112
|
+
return trimmed ? trimmed : null;
|
|
113
|
+
}
|
|
114
|
+
function slugifyOption(value) {
|
|
115
|
+
const slug = value
|
|
116
|
+
.toLowerCase()
|
|
117
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
118
|
+
.replace(/^_+|_+$/g, "")
|
|
119
|
+
.slice(0, 60);
|
|
120
|
+
return slug || "option";
|
|
121
|
+
}
|
|
@@ -21,12 +21,6 @@ export type SqlErrorAttribution = {
|
|
|
21
21
|
/** Neon/HTTP transport status code (numeric) when the failure carries one. */
|
|
22
22
|
statusCode: number | null;
|
|
23
23
|
};
|
|
24
|
-
/**
|
|
25
|
-
* Classifies a pg/drizzle error into structured, non-secret attribution.
|
|
26
|
-
* Returns null when the error carries no SQL/connection signal at all (so
|
|
27
|
-
* non-DB errors never gain spurious `db.*` attributes). Never throws.
|
|
28
|
-
*/
|
|
29
|
-
export declare function describeSqlError(error: unknown): SqlErrorAttribution | null;
|
|
30
24
|
/** Log-field shape (snake_case) for merging into `errorFields(error)` payloads. */
|
|
31
25
|
export declare function sqlErrorFields(error: unknown): Record<string, unknown>;
|
|
32
26
|
/**
|
|
@@ -68,23 +68,14 @@ const TRANSIENT_CAUSES = new Set([
|
|
|
68
68
|
* Returns null when the error carries no SQL/connection signal at all (so
|
|
69
69
|
* non-DB errors never gain spurious `db.*` attributes). Never throws.
|
|
70
70
|
*/
|
|
71
|
-
|
|
71
|
+
function describeSqlError(error) {
|
|
72
72
|
try {
|
|
73
73
|
const sqlstate = readSqlstate(error);
|
|
74
74
|
const errno = readErrno(error);
|
|
75
75
|
const message = readMessage(error);
|
|
76
76
|
const drizzle = isDrizzleQueryError(error, message);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (errno && CONNECT_TIMEOUT_ERRNOS.has(errno))
|
|
80
|
-
cause = "connect_timeout";
|
|
81
|
-
else if (errno && CONNECTION_ERRNOS.has(errno))
|
|
82
|
-
cause = "connection";
|
|
83
|
-
else if (CONNECT_TIMEOUT_MESSAGE.test(message))
|
|
84
|
-
cause = "connect_timeout";
|
|
85
|
-
else if (CONNECTION_MESSAGE.test(message))
|
|
86
|
-
cause = "connection";
|
|
87
|
-
}
|
|
77
|
+
const sqlstateCause = sqlstate ? categoryFromSqlstate(sqlstate) ?? "unknown" : "unknown";
|
|
78
|
+
const cause = sqlstateCause !== "unknown" ? sqlstateCause : causeFromErrnoOrMessage(errno, message);
|
|
88
79
|
const hasSignal = Boolean(sqlstate) || Boolean(errno) || cause !== "unknown" || drizzle;
|
|
89
80
|
if (!hasSignal)
|
|
90
81
|
return null;
|
|
@@ -110,22 +101,35 @@ export function describeSqlError(error) {
|
|
|
110
101
|
return null;
|
|
111
102
|
}
|
|
112
103
|
}
|
|
104
|
+
// Connection-level fallback when no SQLSTATE resolved a cause: prefer the
|
|
105
|
+
// errno (set before/around the TCP+TLS handshake), then the message text.
|
|
106
|
+
function causeFromErrnoOrMessage(errno, message) {
|
|
107
|
+
if (errno && CONNECT_TIMEOUT_ERRNOS.has(errno))
|
|
108
|
+
return "connect_timeout";
|
|
109
|
+
if (errno && CONNECTION_ERRNOS.has(errno))
|
|
110
|
+
return "connection";
|
|
111
|
+
if (CONNECT_TIMEOUT_MESSAGE.test(message))
|
|
112
|
+
return "connect_timeout";
|
|
113
|
+
if (CONNECTION_MESSAGE.test(message))
|
|
114
|
+
return "connection";
|
|
115
|
+
return "unknown";
|
|
116
|
+
}
|
|
113
117
|
/** Log-field shape (snake_case) for merging into `errorFields(error)` payloads. */
|
|
114
118
|
export function sqlErrorFields(error) {
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
119
|
+
const attribution = describeSqlError(error);
|
|
120
|
+
if (!attribution)
|
|
117
121
|
return {};
|
|
118
122
|
return {
|
|
119
|
-
...(
|
|
120
|
-
sql_error_cause:
|
|
121
|
-
...(
|
|
122
|
-
...(
|
|
123
|
-
sql_error_transient:
|
|
124
|
-
...(
|
|
125
|
-
...(
|
|
126
|
-
...(
|
|
127
|
-
...(
|
|
128
|
-
...(
|
|
123
|
+
...(attribution.pgCode ? { sql_error_pg_code: attribution.pgCode } : {}),
|
|
124
|
+
sql_error_cause: attribution.cause,
|
|
125
|
+
...(attribution.queryKind ? { sql_error_query_kind: attribution.queryKind } : {}),
|
|
126
|
+
...(attribution.table ? { sql_error_table: attribution.table } : {}),
|
|
127
|
+
sql_error_transient: attribution.transient,
|
|
128
|
+
...(attribution.severity ? { sql_error_severity: attribution.severity } : {}),
|
|
129
|
+
...(attribution.schema ? { sql_error_schema: attribution.schema } : {}),
|
|
130
|
+
...(attribution.column ? { sql_error_column: attribution.column } : {}),
|
|
131
|
+
...(attribution.constraint ? { sql_error_constraint: attribution.constraint } : {}),
|
|
132
|
+
...(attribution.statusCode !== null ? { sql_error_status: attribution.statusCode } : {}),
|
|
129
133
|
};
|
|
130
134
|
}
|
|
131
135
|
/**
|
|
@@ -144,47 +148,52 @@ export function redactSqlParameters(text) {
|
|
|
144
148
|
}
|
|
145
149
|
/** Telemetry-attribute shape (dotted keys) for span error attribution. */
|
|
146
150
|
export function sqlErrorTelemetryAttributes(error) {
|
|
147
|
-
const
|
|
148
|
-
if (!
|
|
151
|
+
const attribution = describeSqlError(error);
|
|
152
|
+
if (!attribution)
|
|
149
153
|
return {};
|
|
150
154
|
return {
|
|
151
|
-
...(
|
|
152
|
-
"db.error.cause":
|
|
153
|
-
...(
|
|
154
|
-
...(
|
|
155
|
-
"db.error.transient":
|
|
156
|
-
...(
|
|
157
|
-
...(
|
|
158
|
-
...(
|
|
159
|
-
...(
|
|
160
|
-
...(
|
|
155
|
+
...(attribution.pgCode ? { "db.pg_code": attribution.pgCode } : {}),
|
|
156
|
+
"db.error.cause": attribution.cause,
|
|
157
|
+
...(attribution.queryKind ? { "db.error.query_kind": attribution.queryKind } : {}),
|
|
158
|
+
...(attribution.table ? { "db.error.table": attribution.table } : {}),
|
|
159
|
+
"db.error.transient": attribution.transient,
|
|
160
|
+
...(attribution.severity ? { "db.error.severity": attribution.severity } : {}),
|
|
161
|
+
...(attribution.schema ? { "db.error.schema": attribution.schema } : {}),
|
|
162
|
+
...(attribution.column ? { "db.error.column": attribution.column } : {}),
|
|
163
|
+
...(attribution.constraint ? { "db.error.constraint": attribution.constraint } : {}),
|
|
164
|
+
...(attribution.statusCode !== null ? { "db.error.status": attribution.statusCode } : {}),
|
|
161
165
|
};
|
|
162
166
|
}
|
|
167
|
+
// Specific SQLSTATEs that must resolve before the coarser class-prefix lookup
|
|
168
|
+
// (e.g. 40001 serialization / 40P01 deadlock are more precise than class 40).
|
|
169
|
+
const EXACT_SQLSTATE_CAUSES = new Map([
|
|
170
|
+
["57014", "statement_timeout"],
|
|
171
|
+
["53300", "too_many_connections"],
|
|
172
|
+
["40001", "serialization"],
|
|
173
|
+
["40P01", "deadlock"],
|
|
174
|
+
]);
|
|
175
|
+
// SQLSTATE class (first two chars) → cause, applied only after the exact and
|
|
176
|
+
// schema-drift lookups above so the precise codes keep their dedicated cause.
|
|
177
|
+
const SQLSTATE_CLASS_CAUSES = new Map([
|
|
178
|
+
["08", "connection"],
|
|
179
|
+
["28", "auth"],
|
|
180
|
+
["22", "data_exception"],
|
|
181
|
+
["23", "integrity_constraint"],
|
|
182
|
+
["40", "transaction_rollback"],
|
|
183
|
+
["42", "syntax_or_access"],
|
|
184
|
+
["53", "insufficient_resources"],
|
|
185
|
+
["57", "admin_shutdown"],
|
|
186
|
+
["58", "internal"],
|
|
187
|
+
["XX", "internal"],
|
|
188
|
+
["3F", "schema_drift"],
|
|
189
|
+
]);
|
|
163
190
|
function categoryFromSqlstate(code) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return "too_many_connections";
|
|
168
|
-
if (code === "40001")
|
|
169
|
-
return "serialization";
|
|
170
|
-
if (code === "40P01")
|
|
171
|
-
return "deadlock";
|
|
191
|
+
const exact = EXACT_SQLSTATE_CAUSES.get(code);
|
|
192
|
+
if (exact)
|
|
193
|
+
return exact;
|
|
172
194
|
if (SCHEMA_DRIFT_SQLSTATES.has(code))
|
|
173
195
|
return "schema_drift";
|
|
174
|
-
|
|
175
|
-
case "08": return "connection";
|
|
176
|
-
case "28": return "auth";
|
|
177
|
-
case "22": return "data_exception";
|
|
178
|
-
case "23": return "integrity_constraint";
|
|
179
|
-
case "40": return "transaction_rollback";
|
|
180
|
-
case "42": return "syntax_or_access";
|
|
181
|
-
case "53": return "insufficient_resources";
|
|
182
|
-
case "57": return "admin_shutdown";
|
|
183
|
-
case "58": return "internal";
|
|
184
|
-
case "XX": return "internal";
|
|
185
|
-
case "3F": return "schema_drift";
|
|
186
|
-
default: return null;
|
|
187
|
-
}
|
|
196
|
+
return SQLSTATE_CLASS_CAUSES.get(code.slice(0, 2)) ?? null;
|
|
188
197
|
}
|
|
189
198
|
// Walk the Error.cause chain (bounded) and return the first non-null value the
|
|
190
199
|
// reader extracts. drizzle wraps the pg error on `.cause`, and custom wrappers
|
|
@@ -5,7 +5,6 @@ export type WithTelemetrySpanOptions = {
|
|
|
5
5
|
export declare function withTelemetrySpan<T>(tracerName: string, name: string, attributes: TelemetryAttributes | undefined, fn: () => Promise<T>, options?: WithTelemetrySpanOptions): Promise<T>;
|
|
6
6
|
export declare function setActiveTelemetryAttributes(attributes: TelemetryAttributes): void;
|
|
7
7
|
export declare function markActiveTelemetryError(message: string, attributes?: TelemetryAttributes): void;
|
|
8
|
-
export declare function addTelemetryEvent(name: string, attributes?: TelemetryAttributes): void;
|
|
9
8
|
export declare function recordTelemetryCounter(name: string, value?: number, attributes?: TelemetryAttributes): void;
|
|
10
9
|
export declare function recordTelemetryHistogram(name: string, value: number, attributes?: TelemetryAttributes): void;
|
|
11
10
|
export declare function commonTelemetryAttributes(attributes?: TelemetryAttributes): TelemetryAttributes;
|