@oxygen-agent/cli 1.987.20 → 1.1010.1
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/admin-primary-providers-render.d.ts +0 -2
- package/dist/admin-primary-providers-render.js +1 -1
- package/dist/browser-login.js +1 -4
- package/dist/column-decision-options.d.ts +20 -0
- package/dist/column-decision-options.js +54 -0
- package/dist/command-manifest.d.ts +3 -2
- package/dist/command-manifest.js +25 -2
- package/dist/credentials.d.ts +1 -1
- package/dist/functions-commands.js +13 -9
- package/dist/help.d.ts +8 -0
- package/dist/help.js +46 -0
- package/dist/index.js +2751 -242
- package/dist/knowledge-mirror.d.ts +2 -2
- package/dist/runtime.d.ts +0 -15
- package/dist/runtime.js +1 -1
- package/dist/search-ai-filter-notice.d.ts +17 -0
- package/dist/search-ai-filter-notice.js +38 -0
- package/dist/session.d.ts +4 -3
- package/dist/skills.d.ts +8 -7
- package/dist/skills.js +58 -20
- package/dist/transcript.d.ts +2 -1
- package/dist/util.d.ts +10 -1
- package/dist/util.js +14 -2
- 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/formula/dist/coerce.d.ts +10 -0
- package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.js +65 -0
- package/node_modules/@oxygen/formula/dist/hash.d.ts +19 -0
- package/node_modules/@oxygen/formula/dist/hash.js +199 -0
- package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +6 -1
- package/node_modules/@oxygen/formula/dist/value-cleaners.js +10 -26
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/array-utils.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/array-utils.js +11 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +99 -22
- package/node_modules/@oxygen/shared/dist/billing.js +195 -40
- package/node_modules/@oxygen/shared/dist/byok-connect.js +5 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +27 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +311 -28
- 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.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/column-autofill.js +62 -0
- 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/column-output-fields.js +2 -6
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +113 -0
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +548 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +6 -6
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +6 -6
- 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/deploy-env.d.ts +74 -0
- package/node_modules/@oxygen/shared/dist/deploy-env.js +82 -0
- package/node_modules/@oxygen/shared/dist/dnc-rules.d.ts +130 -0
- package/node_modules/@oxygen/shared/dist/dnc-rules.js +221 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +107 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +809 -0
- package/node_modules/@oxygen/shared/dist/error-message.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/error-message.js +3 -0
- package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -3
- package/node_modules/@oxygen/shared/dist/external-write-policy.d.ts +33 -0
- package/node_modules/@oxygen/shared/dist/external-write-policy.js +68 -0
- package/node_modules/@oxygen/shared/dist/format-percent.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/format-percent.js +13 -0
- package/node_modules/@oxygen/shared/dist/freemail-domains.d.ts +81 -0
- package/node_modules/@oxygen/shared/dist/freemail-domains.js +157 -0
- package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1 -1
- 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 +15 -0
- package/node_modules/@oxygen/shared/dist/index.js +15 -0
- package/node_modules/@oxygen/shared/dist/json-path.js +1 -3
- package/node_modules/@oxygen/shared/dist/knowledge-bases.js +1 -3
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +2 -2
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +44 -1
- package/node_modules/@oxygen/shared/dist/langfuse.js +416 -14
- package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +33 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.js +361 -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/log-sink-selector.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/log-sink-selector.js +56 -0
- package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/log.js +6 -1
- 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/object-storage.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +21 -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 +79 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +366 -0
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +1 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +23 -22
- package/node_modules/@oxygen/shared/dist/plan-limits.js +45 -18
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +48 -41
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +36 -25
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +22 -22
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +40 -34
- package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +9 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +36 -2
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +36 -1
- package/node_modules/@oxygen/shared/dist/rate-window.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/rate-window.js +8 -0
- package/node_modules/@oxygen/shared/dist/redaction.js +4 -1
- package/node_modules/@oxygen/shared/dist/research-output-contract.js +1 -3
- 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/search-vocab.js +4 -5
- package/node_modules/@oxygen/shared/dist/select-options.js +6 -1
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +1 -5
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +115 -5
- 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/spend-safety.d.ts +22 -10
- package/node_modules/@oxygen/shared/dist/spend-safety.js +15 -21
- package/node_modules/@oxygen/shared/dist/sql-rows.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/sql-rows.js +3 -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 +28 -2
- package/node_modules/@oxygen/shared/dist/type-guards.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/type-guards.js +35 -0
- 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/value-readers.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/value-readers.js +59 -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 +59 -0
- package/node_modules/@oxygen/workflows/dist/graph/expression.js +2 -5
- package/node_modules/@oxygen/workflows/dist/graph/params.js +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// The cutover write freeze, and the one place that reads `OXYGEN_CUTOVER_FREEZE`.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS. Moving production between database hosts has a short window
|
|
4
|
+
// in which every committed write must already be on the source when the final
|
|
5
|
+
// checkpoint is taken, and nothing may commit on the source afterwards. The
|
|
6
|
+
// operator sets `OXYGEN_CUTOVER_FREEZE=1` on the stack being retired; every
|
|
7
|
+
// writer asks this module and stands down (`ops/selfhost/cutover/RUNBOOK.md`).
|
|
8
|
+
//
|
|
9
|
+
// UNSET MEANS NOT FROZEN, deliberately and permanently: a variable nobody set
|
|
10
|
+
// must never take production into maintenance. The recognised truthy values are
|
|
11
|
+
// the ones the three pre-existing readers accepted (`1`, `true`, `yes`, `on`,
|
|
12
|
+
// `enabled`), so consolidating them here changes no environment's behaviour.
|
|
13
|
+
//
|
|
14
|
+
// Read per call rather than memoized at module load: tests drive both states,
|
|
15
|
+
// and a snapshot taken at import would make the freeze untestable.
|
|
16
|
+
import { OxygenError } from "./cli-result.js";
|
|
17
|
+
export const CUTOVER_FREEZE_ENV_VAR = "OXYGEN_CUTOVER_FREEZE";
|
|
18
|
+
/**
|
|
19
|
+
* The machine code every maintenance refusal carries, on every surface (web,
|
|
20
|
+
* CLI, MCP, public webhooks). Retryable by contract: nothing the caller sent was
|
|
21
|
+
* wrong, and the same request succeeds once the window closes.
|
|
22
|
+
*/
|
|
23
|
+
export const MAINTENANCE_IN_PROGRESS_CODE = "maintenance_in_progress";
|
|
24
|
+
/** What `Retry-After` says during the window. Short enough that an agent loop resumes promptly. */
|
|
25
|
+
export const MAINTENANCE_RETRY_AFTER_SECONDS = 120;
|
|
26
|
+
/**
|
|
27
|
+
* Fixed customer copy (ADR 0023): says what is happening and what to do, names
|
|
28
|
+
* no host, database or vendor, and promises nothing about the exact end time.
|
|
29
|
+
*/
|
|
30
|
+
export const MAINTENANCE_IN_PROGRESS_MESSAGE = "OXYGEN is in a short scheduled maintenance window. Nothing was changed; retry in a few minutes.";
|
|
31
|
+
const TRUTHY = new Set(["1", "true", "yes", "on", "enabled"]);
|
|
32
|
+
export function isCutoverFreezeActive(env = process.env) {
|
|
33
|
+
const value = env[CUTOVER_FREEZE_ENV_VAR]?.trim().toLowerCase();
|
|
34
|
+
return value !== undefined && TRUTHY.has(value);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The typed maintenance fault. `reason` is a stable machine token (never prose)
|
|
38
|
+
* that tells an operator which fenced path refused, e.g.
|
|
39
|
+
* `tenant_schema_migration_pending` or `tenant_database_suspended`.
|
|
40
|
+
*/
|
|
41
|
+
export function maintenanceInProgressError(reason, details = {}) {
|
|
42
|
+
return new OxygenError(MAINTENANCE_IN_PROGRESS_CODE, MAINTENANCE_IN_PROGRESS_MESSAGE, {
|
|
43
|
+
details: {
|
|
44
|
+
reason,
|
|
45
|
+
retryable: true,
|
|
46
|
+
retry_after_seconds: MAINTENANCE_RETRY_AFTER_SECONDS,
|
|
47
|
+
next_step: "Retry the same request after the maintenance window; nothing needs to change.",
|
|
48
|
+
...details,
|
|
49
|
+
},
|
|
50
|
+
exitCode: 1,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The companies that supply OXYGEN's managed data, keyed by OXYGEN provider id.
|
|
3
|
+
*
|
|
4
|
+
* Founder decision (Philipp, 2026-09-25, reversing the 2026-09-11 rule that hid
|
|
5
|
+
* the managed LinkedIn rail's backend): data suppliers are NAMED — on
|
|
6
|
+
* /subprocessors, in provider labels, and in run provenance. What stays out of
|
|
7
|
+
* every customer surface is unchanged (ADR 0023): vendor hosts, keys, internal
|
|
8
|
+
* COGS, and upstream error text. Name the supplier from this map; never copy a
|
|
9
|
+
* vendor string into a label, descriptor, or help text by hand.
|
|
10
|
+
*
|
|
11
|
+
* Dependency-free on purpose: browser bundles (the picker, the record page)
|
|
12
|
+
* import it.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MANAGED_DATA_SUPPLIERS: {
|
|
15
|
+
/** The managed `scraper.*` LinkedIn rail. HarvestAPI is BYOK only and is not listed. */
|
|
16
|
+
readonly scraper: "Up2Data";
|
|
17
|
+
/** Company-page follower audiences, fulfilled by the vendor. */
|
|
18
|
+
readonly scrapeli: "ScrapeLi";
|
|
19
|
+
/** LinkedIn post datasets on OXYGEN's managed key. */
|
|
20
|
+
readonly brightdata: "Bright Data";
|
|
21
|
+
/** Indexed company and people data on OXYGEN's managed key. */
|
|
22
|
+
readonly crustdata: "Crustdata";
|
|
23
|
+
};
|
|
24
|
+
export type ManagedDataSupplierProvider = keyof typeof MANAGED_DATA_SUPPLIERS;
|
|
25
|
+
/**
|
|
26
|
+
* Customer-visible name of the managed LinkedIn data rail (`scraper.*`). Founder
|
|
27
|
+
* decision 2026-09-25: data-extraction labels say "Professional Network", while
|
|
28
|
+
* ids, routes and operation names keep `linkedin`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const PROFESSIONAL_NETWORK_DATA_NAME = "Professional Network Data";
|
|
31
|
+
/** The supplier behind a provider's MANAGED lane, or null when OXYGEN names none. */
|
|
32
|
+
export declare function managedDataSupplier(provider: string | null | undefined): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* `name · supplied by <supplier>` for a provider whose supplier is not already
|
|
35
|
+
* its name; the name unchanged otherwise ("Bright Data" never reads "Bright Data
|
|
36
|
+
* · supplied by Bright Data").
|
|
37
|
+
*/
|
|
38
|
+
export declare function suppliedByLabel(name: string, provider: string | null | undefined): string;
|
|
39
|
+
/** "Professional Network Data · supplied by Up2Data" — the managed LinkedIn rail's label. */
|
|
40
|
+
export declare const PROFESSIONAL_NETWORK_DATA_LABEL: string;
|
|
41
|
+
/**
|
|
42
|
+
* How a supplier obtains an operation's data. Informational only: it never gates,
|
|
43
|
+
* prices, or routes anything.
|
|
44
|
+
*
|
|
45
|
+
* - `supplier_collected`: the supplier collects and provides the data.
|
|
46
|
+
* - `supplier_may_use_logged_in_sessions`: the data is reachable only while
|
|
47
|
+
* signed in (engagers, activity feeds, search, admin-only lists), so the
|
|
48
|
+
* supplier may use logged-in sessions it operates. No customer account is used.
|
|
49
|
+
*/
|
|
50
|
+
export type DataSourcingMethod = "supplier_collected" | "supplier_may_use_logged_in_sessions";
|
|
51
|
+
export type DataSourcing = {
|
|
52
|
+
supplier: string;
|
|
53
|
+
method: DataSourcingMethod;
|
|
54
|
+
note: string;
|
|
55
|
+
};
|
|
56
|
+
/** The disclosure an operation carries, in one sentence per method. */
|
|
57
|
+
export declare function dataSourcingFor(provider: ManagedDataSupplierProvider, method: DataSourcingMethod): DataSourcing;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The companies that supply OXYGEN's managed data, keyed by OXYGEN provider id.
|
|
3
|
+
*
|
|
4
|
+
* Founder decision (Philipp, 2026-09-25, reversing the 2026-09-11 rule that hid
|
|
5
|
+
* the managed LinkedIn rail's backend): data suppliers are NAMED — on
|
|
6
|
+
* /subprocessors, in provider labels, and in run provenance. What stays out of
|
|
7
|
+
* every customer surface is unchanged (ADR 0023): vendor hosts, keys, internal
|
|
8
|
+
* COGS, and upstream error text. Name the supplier from this map; never copy a
|
|
9
|
+
* vendor string into a label, descriptor, or help text by hand.
|
|
10
|
+
*
|
|
11
|
+
* Dependency-free on purpose: browser bundles (the picker, the record page)
|
|
12
|
+
* import it.
|
|
13
|
+
*/
|
|
14
|
+
export const MANAGED_DATA_SUPPLIERS = {
|
|
15
|
+
/** The managed `scraper.*` LinkedIn rail. HarvestAPI is BYOK only and is not listed. */
|
|
16
|
+
scraper: "Up2Data",
|
|
17
|
+
/** Company-page follower audiences, fulfilled by the vendor. */
|
|
18
|
+
scrapeli: "ScrapeLi",
|
|
19
|
+
/** LinkedIn post datasets on OXYGEN's managed key. */
|
|
20
|
+
brightdata: "Bright Data",
|
|
21
|
+
/** Indexed company and people data on OXYGEN's managed key. */
|
|
22
|
+
crustdata: "Crustdata",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Customer-visible name of the managed LinkedIn data rail (`scraper.*`). Founder
|
|
26
|
+
* decision 2026-09-25: data-extraction labels say "Professional Network", while
|
|
27
|
+
* ids, routes and operation names keep `linkedin`.
|
|
28
|
+
*/
|
|
29
|
+
export const PROFESSIONAL_NETWORK_DATA_NAME = "Professional Network Data";
|
|
30
|
+
/** The supplier behind a provider's MANAGED lane, or null when OXYGEN names none. */
|
|
31
|
+
export function managedDataSupplier(provider) {
|
|
32
|
+
if (!provider || !Object.hasOwn(MANAGED_DATA_SUPPLIERS, provider))
|
|
33
|
+
return null;
|
|
34
|
+
return MANAGED_DATA_SUPPLIERS[provider];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `name · supplied by <supplier>` for a provider whose supplier is not already
|
|
38
|
+
* its name; the name unchanged otherwise ("Bright Data" never reads "Bright Data
|
|
39
|
+
* · supplied by Bright Data").
|
|
40
|
+
*/
|
|
41
|
+
export function suppliedByLabel(name, provider) {
|
|
42
|
+
const supplier = managedDataSupplier(provider);
|
|
43
|
+
if (!supplier || name.toLowerCase().includes(supplier.toLowerCase()))
|
|
44
|
+
return name;
|
|
45
|
+
return `${name} · supplied by ${supplier}`;
|
|
46
|
+
}
|
|
47
|
+
/** "Professional Network Data · supplied by Up2Data" — the managed LinkedIn rail's label. */
|
|
48
|
+
export const PROFESSIONAL_NETWORK_DATA_LABEL = suppliedByLabel(PROFESSIONAL_NETWORK_DATA_NAME, "scraper");
|
|
49
|
+
/** The disclosure an operation carries, in one sentence per method. */
|
|
50
|
+
export function dataSourcingFor(provider, method) {
|
|
51
|
+
const supplier = MANAGED_DATA_SUPPLIERS[provider];
|
|
52
|
+
return {
|
|
53
|
+
supplier,
|
|
54
|
+
method,
|
|
55
|
+
note: method === "supplier_collected"
|
|
56
|
+
? `Collected and provided by ${supplier}. No account of yours is used.`
|
|
57
|
+
: `Collected by ${supplier}, which may use signed-in sessions it operates. No account of yours is used.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One answer to "which deployment is this, and is it holding production data".
|
|
3
|
+
*
|
|
4
|
+
* Before this module the repo had at least four: `VERCEL_ENV === "production" ||
|
|
5
|
+
* NODE_ENV === "production"` (the recipe sandbox guard), `FLY_ENVIRONMENT ===
|
|
6
|
+
* "production" || NODE_ENV === "production"` (the worker health guard), the
|
|
7
|
+
* Fly-app-name chain in `apps/worker/src/worker-env.ts`, and the
|
|
8
|
+
* product-analytics / Langfuse environment pickers. Self-hosting adds
|
|
9
|
+
* environments that are neither the managed prod nor the managed dev app while
|
|
10
|
+
* carrying production-shaped data (`selfhost-shadow`), which is exactly the case
|
|
11
|
+
* every one of those expressions answers wrong.
|
|
12
|
+
*
|
|
13
|
+
* `OXYGEN_DEPLOY_ENV` is the discriminator. Unset means managed: every caller
|
|
14
|
+
* falls back to the legacy signals byte-identically, so a managed environment
|
|
15
|
+
* that sets nothing behaves precisely as production does today
|
|
16
|
+
* (`.agents/skills/oxygen-platform-portability/SKILL.md` rule 1).
|
|
17
|
+
*
|
|
18
|
+
* Deliberately dependency-free apart from `OxygenError`: the log shipper, the
|
|
19
|
+
* OTel registration and the worker boot guard all read it before any heavier
|
|
20
|
+
* module is loaded.
|
|
21
|
+
*/
|
|
22
|
+
export type DeployPlatform = "managed" | "selfhost";
|
|
23
|
+
/**
|
|
24
|
+
* `shadow` is a self-hosted environment running against production-shaped data
|
|
25
|
+
* during the cutover rehearsal. It is NOT production traffic, but it is
|
|
26
|
+
* production data — hence `isProductionData()` covers it while the telemetry
|
|
27
|
+
* environment keeps it out of the production dataset.
|
|
28
|
+
*/
|
|
29
|
+
export type DeployTier = "prod" | "shadow" | "dev" | "local";
|
|
30
|
+
export type DeployEnvSource = "OXYGEN_DEPLOY_ENV" | "VERCEL_ENV" | "FLY_APP_NAME" | "NODE_ENV" | "none";
|
|
31
|
+
export type DeployEnv = {
|
|
32
|
+
platform: DeployPlatform;
|
|
33
|
+
tier: DeployTier;
|
|
34
|
+
/** Stable human/telemetry label: the raw selector on self-hosted, `managed-<tier>` otherwise. */
|
|
35
|
+
label: string;
|
|
36
|
+
/** Which signal decided the tier — the field to print when an operator disputes the answer. */
|
|
37
|
+
source: DeployEnvSource;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Resolves the deployment identity of this process.
|
|
41
|
+
*
|
|
42
|
+
* Throws `invalid_deploy_env` on any non-empty `OXYGEN_DEPLOY_ENV` that is not
|
|
43
|
+
* one of the three self-hosted values. A typo must fail the process, never fall
|
|
44
|
+
* through to the managed chain: falling through would silently resolve a
|
|
45
|
+
* self-hosted box holding production data as `local`, which is the exact shape
|
|
46
|
+
* of the sandbox-guard landmine this module exists to close.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveDeployEnv(env?: NodeJS.ProcessEnv): DeployEnv;
|
|
49
|
+
/**
|
|
50
|
+
* True when this process may touch production data, i.e. when an unsandboxed
|
|
51
|
+
* executor, a duplicated sweep or an external write would hit real customers.
|
|
52
|
+
*
|
|
53
|
+
* With `OXYGEN_DEPLOY_ENV` set this is the tier (`prod` or `shadow`). With it
|
|
54
|
+
* unset this is EXACTLY the legacy expression the recipe sandbox guard has
|
|
55
|
+
* always used — not the resolved tier — because the tier chain reads
|
|
56
|
+
* `FLY_APP_NAME` before `NODE_ENV` and would therefore disengage the guard on a
|
|
57
|
+
* Fly dev app that a Doppler secret had put into `NODE_ENV=production`. Unset
|
|
58
|
+
* means managed, and managed means byte-identical.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isProductionData(env?: NodeJS.ProcessEnv): boolean;
|
|
61
|
+
/** True when this process runs on self-hosted infrastructure. */
|
|
62
|
+
export declare function isSelfhost(env?: NodeJS.ProcessEnv): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* The `deployment.environment` / tracing-environment value implied by
|
|
65
|
+
* `OXYGEN_DEPLOY_ENV`, or `null` when it is unset so the caller keeps its own
|
|
66
|
+
* managed chain untouched.
|
|
67
|
+
*
|
|
68
|
+
* `selfhost-shadow` maps to `development`: it carries production-shaped data but
|
|
69
|
+
* must never land in the production telemetry dataset, where it would be read as
|
|
70
|
+
* real customer traffic.
|
|
71
|
+
*/
|
|
72
|
+
export declare function deployEnvTelemetryEnvironment(env?: NodeJS.ProcessEnv): "production" | "development" | null;
|
|
73
|
+
/** The `oxygen.platform` telemetry attribute: `managed` or `selfhost`. */
|
|
74
|
+
export declare function deployPlatformAttribute(env?: NodeJS.ProcessEnv): DeployPlatform;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { OxygenError } from "./cli-result.js";
|
|
2
|
+
const SELFHOST_TIERS = new Map([
|
|
3
|
+
["selfhost-prod", "prod"],
|
|
4
|
+
["selfhost-shadow", "shadow"],
|
|
5
|
+
["selfhost-dev", "dev"],
|
|
6
|
+
]);
|
|
7
|
+
function managed(tier, source) {
|
|
8
|
+
return { platform: "managed", tier, label: `managed-${tier}`, source };
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolves the deployment identity of this process.
|
|
12
|
+
*
|
|
13
|
+
* Throws `invalid_deploy_env` on any non-empty `OXYGEN_DEPLOY_ENV` that is not
|
|
14
|
+
* one of the three self-hosted values. A typo must fail the process, never fall
|
|
15
|
+
* through to the managed chain: falling through would silently resolve a
|
|
16
|
+
* self-hosted box holding production data as `local`, which is the exact shape
|
|
17
|
+
* of the sandbox-guard landmine this module exists to close.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveDeployEnv(env = process.env) {
|
|
20
|
+
const configured = env.OXYGEN_DEPLOY_ENV?.trim();
|
|
21
|
+
if (configured) {
|
|
22
|
+
const tier = SELFHOST_TIERS.get(configured.toLowerCase());
|
|
23
|
+
if (!tier) {
|
|
24
|
+
throw new OxygenError("invalid_deploy_env", `OXYGEN_DEPLOY_ENV must be one of selfhost-prod, selfhost-shadow, selfhost-dev (received '${configured}').`, { details: { oxygen_deploy_env: configured }, exitCode: 1 });
|
|
25
|
+
}
|
|
26
|
+
return { platform: "selfhost", tier, label: `selfhost-${tier}`, source: "OXYGEN_DEPLOY_ENV" };
|
|
27
|
+
}
|
|
28
|
+
const vercelEnv = env.VERCEL_ENV?.trim();
|
|
29
|
+
if (vercelEnv === "production")
|
|
30
|
+
return managed("prod", "VERCEL_ENV");
|
|
31
|
+
if (vercelEnv === "preview" || vercelEnv === "development")
|
|
32
|
+
return managed("dev", "VERCEL_ENV");
|
|
33
|
+
const flyApp = env.FLY_APP_NAME?.trim();
|
|
34
|
+
if (flyApp)
|
|
35
|
+
return managed(flyApp.includes("-dev") ? "dev" : "prod", "FLY_APP_NAME");
|
|
36
|
+
const nodeEnv = env.NODE_ENV?.trim();
|
|
37
|
+
if (nodeEnv === "production")
|
|
38
|
+
return managed("prod", "NODE_ENV");
|
|
39
|
+
if (nodeEnv)
|
|
40
|
+
return managed("local", "NODE_ENV");
|
|
41
|
+
return managed("local", "none");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* True when this process may touch production data, i.e. when an unsandboxed
|
|
45
|
+
* executor, a duplicated sweep or an external write would hit real customers.
|
|
46
|
+
*
|
|
47
|
+
* With `OXYGEN_DEPLOY_ENV` set this is the tier (`prod` or `shadow`). With it
|
|
48
|
+
* unset this is EXACTLY the legacy expression the recipe sandbox guard has
|
|
49
|
+
* always used — not the resolved tier — because the tier chain reads
|
|
50
|
+
* `FLY_APP_NAME` before `NODE_ENV` and would therefore disengage the guard on a
|
|
51
|
+
* Fly dev app that a Doppler secret had put into `NODE_ENV=production`. Unset
|
|
52
|
+
* means managed, and managed means byte-identical.
|
|
53
|
+
*/
|
|
54
|
+
export function isProductionData(env = process.env) {
|
|
55
|
+
if (env.OXYGEN_DEPLOY_ENV?.trim()) {
|
|
56
|
+
const { tier } = resolveDeployEnv(env);
|
|
57
|
+
return tier === "prod" || tier === "shadow";
|
|
58
|
+
}
|
|
59
|
+
return env.VERCEL_ENV === "production" || env.NODE_ENV === "production";
|
|
60
|
+
}
|
|
61
|
+
/** True when this process runs on self-hosted infrastructure. */
|
|
62
|
+
export function isSelfhost(env = process.env) {
|
|
63
|
+
return resolveDeployEnv(env).platform === "selfhost";
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The `deployment.environment` / tracing-environment value implied by
|
|
67
|
+
* `OXYGEN_DEPLOY_ENV`, or `null` when it is unset so the caller keeps its own
|
|
68
|
+
* managed chain untouched.
|
|
69
|
+
*
|
|
70
|
+
* `selfhost-shadow` maps to `development`: it carries production-shaped data but
|
|
71
|
+
* must never land in the production telemetry dataset, where it would be read as
|
|
72
|
+
* real customer traffic.
|
|
73
|
+
*/
|
|
74
|
+
export function deployEnvTelemetryEnvironment(env = process.env) {
|
|
75
|
+
if (!env.OXYGEN_DEPLOY_ENV?.trim())
|
|
76
|
+
return null;
|
|
77
|
+
return resolveDeployEnv(env).tier === "prod" ? "production" : "development";
|
|
78
|
+
}
|
|
79
|
+
/** The `oxygen.platform` telemetry attribute: `managed` or `selfhost`. */
|
|
80
|
+
export function deployPlatformAttribute(env = process.env) {
|
|
81
|
+
return resolveDeployEnv(env).platform;
|
|
82
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace do-not-contact RULE CATALOG: which events may put somebody on the
|
|
3
|
+
* DNC list, and what the workspace has decided each one should do.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. Suppression ENFORCEMENT has always been absolute and layered —
|
|
6
|
+
* enrollment, plan and a dispatch-time re-check per channel — and none of that is
|
|
7
|
+
* configurable, on purpose: once an identity is on a ledger it always blocks. What
|
|
8
|
+
* was equally hardcoded, and should not have been, is ENTRY. Six writers decided
|
|
9
|
+
* unilaterally who lands on the list, and one of them was blunt enough to be a
|
|
10
|
+
* product complaint in its own right: ANY inbound LinkedIn message suppressed that
|
|
11
|
+
* person org-wide, forever, so a lead who replied "sure, send me pricing" could
|
|
12
|
+
* never be enrolled in another campaign again.
|
|
13
|
+
*
|
|
14
|
+
* The split this module draws is therefore: the workspace owns WHO GETS ON the
|
|
15
|
+
* list; the platform still owns WHAT A LIST ENTRY DOES. A rule resolves to one of
|
|
16
|
+
* three effects:
|
|
17
|
+
*
|
|
18
|
+
* off — the event is observed and recorded as it always was, but writes no
|
|
19
|
+
* suppression. Nothing else about the event changes.
|
|
20
|
+
* stop_only — end THIS enrollment, do not write the permanent ledger row. The
|
|
21
|
+
* missing middle: "a reply stops the cadence" without "blacklist this
|
|
22
|
+
* human from every future campaign".
|
|
23
|
+
* suppress — stop and write the ledger row. The default for every rule that
|
|
24
|
+
* shipped before this catalog existed, which is what makes the
|
|
25
|
+
* defaults a byte-for-byte reproduction of previous behaviour.
|
|
26
|
+
*
|
|
27
|
+
* DEFAULTS ARE THE OLD BEHAVIOUR. Every pre-existing rule defaults to `suppress`
|
|
28
|
+
* with the exact reason its writer used to hardcode, and every rule introduced with
|
|
29
|
+
* this catalog defaults to `off`. A workspace that never opens the Rules page keeps
|
|
30
|
+
* precisely the product it had, and a policy read that fails resolves to these
|
|
31
|
+
* defaults rather than dropping a suppression on the floor.
|
|
32
|
+
*
|
|
33
|
+
* COMPLIANCE RULES ARE CONFIGURABLE, AND FLAGGED. `email_unsubscribe_click` and
|
|
34
|
+
* `email_hard_bounce` carry `compliance: true`. That flag does not lock them — the
|
|
35
|
+
* product owner's decision (2026-09-19) is that a workspace may set any rule to any
|
|
36
|
+
* allowed effect — it marks the two whose surfaces must say plainly what turning
|
|
37
|
+
* them down means: an explicit unsubscribe request that is not honoured, and a
|
|
38
|
+
* hard-bounced address that keeps being mailed from the shared pool (which is also
|
|
39
|
+
* the numerator of the mailbox auto-pause). The warning is the product's job; the
|
|
40
|
+
* decision is the customer's.
|
|
41
|
+
*/
|
|
42
|
+
/** What a rule does when its event fires. */
|
|
43
|
+
export declare const DNC_RULE_EFFECTS: readonly ["off", "stop_only", "suppress"];
|
|
44
|
+
export type DncRuleEffect = (typeof DNC_RULE_EFFECTS)[number];
|
|
45
|
+
export declare const DNC_RULE_EFFECT_LABELS: Record<DncRuleEffect, string>;
|
|
46
|
+
/**
|
|
47
|
+
* Which ledger a rule writes when it resolves to `suppress`. This decides which
|
|
48
|
+
* reason enum the configured reason is validated against, so a rule can never be
|
|
49
|
+
* saved with a reason its target table's CHECK constraint would reject at write
|
|
50
|
+
* time — the failure has to land on the person changing the setting, not on the
|
|
51
|
+
* inbound webhook hours later.
|
|
52
|
+
*/
|
|
53
|
+
export declare const DNC_RULE_LEDGERS: readonly ["email", "contact", "phone"];
|
|
54
|
+
export type DncRuleLedger = (typeof DNC_RULE_LEDGERS)[number];
|
|
55
|
+
export type DncRuleDefinition = {
|
|
56
|
+
key: string;
|
|
57
|
+
/** Short label for a list row. */
|
|
58
|
+
label: string;
|
|
59
|
+
/** One sentence naming the exact event, in customer words. */
|
|
60
|
+
description: string;
|
|
61
|
+
/** Which DNC ledger a `suppress` writes. */
|
|
62
|
+
ledger: DncRuleLedger;
|
|
63
|
+
/** Effect when the workspace has not overridden the rule. */
|
|
64
|
+
defaultEffect: DncRuleEffect;
|
|
65
|
+
/** Reason stamped on the ledger row when the rule suppresses. */
|
|
66
|
+
defaultReason: string;
|
|
67
|
+
/**
|
|
68
|
+
* The effects this rule can actually express. A call disposition has no cadence
|
|
69
|
+
* step to stop, so `stop_only` is omitted rather than offered and silently
|
|
70
|
+
* behaving like `off`.
|
|
71
|
+
*/
|
|
72
|
+
allowedEffects: readonly DncRuleEffect[];
|
|
73
|
+
/** Surfaces must warn before this rule is set below `suppress`. See the header. */
|
|
74
|
+
compliance?: boolean;
|
|
75
|
+
/** Grouping for the surfaces. */
|
|
76
|
+
group: "automatic" | "reply_classification";
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The writers that existed before this catalog, each pinned to the reason and
|
|
80
|
+
* ledger it used to hardcode. Changing a `defaultEffect` or `defaultReason` here
|
|
81
|
+
* silently changes every workspace that has not overridden that rule, which is why
|
|
82
|
+
* `dnc-rules.test.ts` pins every one of them.
|
|
83
|
+
*
|
|
84
|
+
* `whatsapp_reply` is the LinkedIn rule's twin and was found the same way: an
|
|
85
|
+
* inbound WhatsApp message suppressed the lead org-wide with no way to say
|
|
86
|
+
* otherwise. Its DNC identity is the normalized chat JID rather than a lead
|
|
87
|
+
* provider id, because `whatsapp_message` is excluded from lead resolution — see
|
|
88
|
+
* the note in whatsapp-inbox-service.ts about why it is deliberately NOT the
|
|
89
|
+
* phone ledger.
|
|
90
|
+
*/
|
|
91
|
+
export declare const DNC_BUILTIN_RULES: readonly DncRuleDefinition[];
|
|
92
|
+
/**
|
|
93
|
+
* Reply-classification rules are addressed by label key, so an org's own inbox
|
|
94
|
+
* labels get rules on exactly the same footing as the shipped ones. The prefix
|
|
95
|
+
* keeps that namespace from ever colliding with a builtin key.
|
|
96
|
+
*/
|
|
97
|
+
export declare const DNC_REPLY_STATUS_RULE_PREFIX = "reply_status:";
|
|
98
|
+
export declare function replyStatusRuleKey(labelKey: string): string;
|
|
99
|
+
export declare function replyStatusLabelKey(ruleKey: string): string | null;
|
|
100
|
+
/**
|
|
101
|
+
* Build the rule definition for one inbox label. `ledger: "email"` is not a claim
|
|
102
|
+
* that the rule is email-only — it names which reason enum the reason is validated
|
|
103
|
+
* against. The applier picks the ledger per identity at write time, because the
|
|
104
|
+
* same classified conversation can carry an email address, a LinkedIn person, or
|
|
105
|
+
* both, and a classified LinkedIn DM must be able to suppress the LinkedIn person.
|
|
106
|
+
*/
|
|
107
|
+
export declare function replyStatusRule(label: {
|
|
108
|
+
key: string;
|
|
109
|
+
name?: string | null;
|
|
110
|
+
}): DncRuleDefinition;
|
|
111
|
+
/** The builtin definition for a key, or null when the key is not a builtin. */
|
|
112
|
+
export declare function builtinDncRule(key: string): DncRuleDefinition | null;
|
|
113
|
+
export declare function isDncRuleEffect(value: unknown): value is DncRuleEffect;
|
|
114
|
+
/**
|
|
115
|
+
* Resolve a stored rule key to its definition. A `reply_status:*` key resolves even
|
|
116
|
+
* when the org has since renamed or archived the label, so an override never
|
|
117
|
+
* becomes unreadable (and therefore unresettable) because a label moved.
|
|
118
|
+
*/
|
|
119
|
+
export declare function resolveDncRuleDefinition(key: string, labels?: ReadonlyArray<{
|
|
120
|
+
key: string;
|
|
121
|
+
name?: string | null;
|
|
122
|
+
}>): DncRuleDefinition | null;
|
|
123
|
+
/**
|
|
124
|
+
* The full catalog a surface renders: the six builtins, then one rule per inbox
|
|
125
|
+
* label the workspace can classify a reply as.
|
|
126
|
+
*/
|
|
127
|
+
export declare function dncRuleCatalog(labels?: ReadonlyArray<{
|
|
128
|
+
key: string;
|
|
129
|
+
name?: string | null;
|
|
130
|
+
}>): DncRuleDefinition[];
|