@oxygen-agent/cli 1.987.20 → 1.1003.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.d.ts +0 -2
  3. package/dist/admin-primary-providers-render.js +1 -1
  4. package/dist/browser-login.js +1 -4
  5. package/dist/command-manifest.d.ts +3 -2
  6. package/dist/command-manifest.js +10 -0
  7. package/dist/credentials.d.ts +1 -1
  8. package/dist/functions-commands.js +6 -2
  9. package/dist/help.d.ts +8 -0
  10. package/dist/help.js +46 -0
  11. package/dist/index.js +1569 -123
  12. package/dist/knowledge-mirror.d.ts +2 -2
  13. package/dist/runtime.d.ts +0 -15
  14. package/dist/runtime.js +1 -1
  15. package/dist/session.d.ts +4 -3
  16. package/dist/skills.d.ts +8 -7
  17. package/dist/skills.js +24 -10
  18. package/dist/transcript.d.ts +2 -1
  19. package/dist/util.d.ts +1 -1
  20. package/dist/util.js +1 -3
  21. package/node_modules/@oxygen/formula/dist/coerce.d.ts +10 -0
  22. package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
  23. package/node_modules/@oxygen/formula/dist/formula-functions.js +65 -0
  24. package/node_modules/@oxygen/formula/dist/hash.d.ts +19 -0
  25. package/node_modules/@oxygen/formula/dist/hash.js +199 -0
  26. package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +6 -1
  27. package/node_modules/@oxygen/formula/dist/value-cleaners.js +10 -26
  28. package/node_modules/@oxygen/shared/dist/array-utils.d.ts +5 -0
  29. package/node_modules/@oxygen/shared/dist/array-utils.js +11 -0
  30. package/node_modules/@oxygen/shared/dist/billing.d.ts +78 -22
  31. package/node_modules/@oxygen/shared/dist/billing.js +150 -40
  32. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +17 -0
  33. package/node_modules/@oxygen/shared/dist/capability-discovery.js +89 -16
  34. package/node_modules/@oxygen/shared/dist/column-autofill.d.ts +52 -0
  35. package/node_modules/@oxygen/shared/dist/column-autofill.js +80 -0
  36. package/node_modules/@oxygen/shared/dist/column-output-fields.js +2 -6
  37. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +108 -0
  38. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +545 -0
  39. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
  40. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
  41. package/node_modules/@oxygen/shared/dist/deploy-env.d.ts +74 -0
  42. package/node_modules/@oxygen/shared/dist/deploy-env.js +82 -0
  43. package/node_modules/@oxygen/shared/dist/dnc-rules.d.ts +130 -0
  44. package/node_modules/@oxygen/shared/dist/dnc-rules.js +221 -0
  45. package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +103 -0
  46. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +819 -0
  47. package/node_modules/@oxygen/shared/dist/error-message.d.ts +1 -0
  48. package/node_modules/@oxygen/shared/dist/error-message.js +3 -0
  49. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -3
  50. package/node_modules/@oxygen/shared/dist/external-write-policy.d.ts +33 -0
  51. package/node_modules/@oxygen/shared/dist/external-write-policy.js +68 -0
  52. package/node_modules/@oxygen/shared/dist/format-percent.d.ts +8 -0
  53. package/node_modules/@oxygen/shared/dist/format-percent.js +13 -0
  54. package/node_modules/@oxygen/shared/dist/freemail-domains.d.ts +81 -0
  55. package/node_modules/@oxygen/shared/dist/freemail-domains.js +157 -0
  56. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +1 -0
  57. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1 -1
  58. package/node_modules/@oxygen/shared/dist/index.d.ts +13 -0
  59. package/node_modules/@oxygen/shared/dist/index.js +13 -0
  60. package/node_modules/@oxygen/shared/dist/json-path.js +1 -3
  61. package/node_modules/@oxygen/shared/dist/knowledge-bases.js +1 -3
  62. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +2 -2
  63. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +2 -2
  64. package/node_modules/@oxygen/shared/dist/langfuse.js +9 -0
  65. package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +32 -0
  66. package/node_modules/@oxygen/shared/dist/linkedin-countries.js +359 -0
  67. package/node_modules/@oxygen/shared/dist/log-sink-selector.d.ts +39 -0
  68. package/node_modules/@oxygen/shared/dist/log-sink-selector.js +56 -0
  69. package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
  70. package/node_modules/@oxygen/shared/dist/log.js +6 -1
  71. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +17 -0
  72. package/node_modules/@oxygen/shared/dist/object-storage.js +21 -0
  73. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +54 -0
  74. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +213 -0
  75. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +1 -0
  76. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +23 -22
  77. package/node_modules/@oxygen/shared/dist/plan-limits.js +45 -18
  78. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +48 -41
  79. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +36 -25
  80. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +22 -22
  81. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +40 -34
  82. package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +9 -0
  83. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +15 -0
  84. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +15 -0
  85. package/node_modules/@oxygen/shared/dist/rate-window.d.ts +5 -0
  86. package/node_modules/@oxygen/shared/dist/rate-window.js +8 -0
  87. package/node_modules/@oxygen/shared/dist/research-output-contract.js +1 -3
  88. package/node_modules/@oxygen/shared/dist/search-vocab.js +4 -5
  89. package/node_modules/@oxygen/shared/dist/select-options.js +6 -1
  90. package/node_modules/@oxygen/shared/dist/sequence-failures.js +1 -5
  91. package/node_modules/@oxygen/shared/dist/sequences.d.ts +23 -0
  92. package/node_modules/@oxygen/shared/dist/sequences.js +110 -4
  93. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +22 -10
  94. package/node_modules/@oxygen/shared/dist/spend-safety.js +15 -21
  95. package/node_modules/@oxygen/shared/dist/sql-rows.d.ts +1 -0
  96. package/node_modules/@oxygen/shared/dist/sql-rows.js +3 -0
  97. package/node_modules/@oxygen/shared/dist/telemetry.js +9 -1
  98. package/node_modules/@oxygen/shared/dist/type-guards.d.ts +22 -0
  99. package/node_modules/@oxygen/shared/dist/type-guards.js +35 -0
  100. package/node_modules/@oxygen/shared/dist/value-readers.d.ts +21 -0
  101. package/node_modules/@oxygen/shared/dist/value-readers.js +59 -0
  102. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  103. package/node_modules/@oxygen/shared/package.json +50 -0
  104. package/node_modules/@oxygen/workflows/dist/graph/expression.js +2 -5
  105. package/node_modules/@oxygen/workflows/dist/graph/params.js +1 -1
  106. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  export declare const GENERATED_MIRROR_FILE_HEADER = "<!-- generated by oxygen knowledge sync \u2014 local edits are overwritten -->";
2
- export type MirrorPageRecord = {
2
+ type MirrorPageRecord = {
3
3
  id: string;
4
4
  revision: number;
5
5
  /** sha256 hex of the exact bytes the server rendered and this client wrote. */
@@ -98,7 +98,7 @@ export declare function acquireMirrorLock(dir: string, options?: {
98
98
  export declare function releaseMirrorLock(dir: string): void;
99
99
  export declare function writeGeneratedIndexFile(dir: string, index: MirrorIndexSnapshot): void;
100
100
  export declare function writeGeneratedLogFile(dir: string, entries: MirrorLogEntry[]): void;
101
- export type MirrorPushCandidate = {
101
+ type MirrorPushCandidate = {
102
102
  slug: string;
103
103
  /** dirty = tracked page whose local bytes drifted; new = untracked local file. */
104
104
  kind: "dirty" | "new";
package/dist/runtime.d.ts CHANGED
@@ -1,20 +1,5 @@
1
1
  declare const DEV_CLI_BINARY = "oxygen-dev";
2
2
  declare const PROD_CLI_BINARY = "oxygen";
3
- /**
4
- * Pins a terminal to a *host*, not to a profile label.
5
- *
6
- * `OXYGEN_PROFILE=default` was the guard prescribed after the last profile drift
7
- * (OXY-4081), and it cannot work: `default` is a name, and `login` writes an
8
- * `apiUrl` into whatever name is active — so the profile called `default` became
9
- * dev and the pin followed it there (OXY-4109). Every credit balance, ticket, and
10
- * whoami then came back from dev looking exactly like a healthy production answer.
11
- *
12
- * The invariant that a staff runbook actually needs is the host it dialed. Set
13
- * `OXYGEN_REQUIRE_API_URL=https://oxygen-agent.com` and any command that resolves
14
- * somewhere else refuses to run instead of quietly answering from the wrong
15
- * environment.
16
- */
17
- export declare const REQUIRED_API_URL_ENV = "OXYGEN_REQUIRE_API_URL";
18
3
  export type CliBinaryName = typeof DEV_CLI_BINARY | typeof PROD_CLI_BINARY;
19
4
  /**
20
5
  * Where a command actually sent its request, and what put it there.
package/dist/runtime.js CHANGED
@@ -17,7 +17,7 @@ const PROD_CLI_BINARY = "oxygen";
17
17
  * somewhere else refuses to run instead of quietly answering from the wrong
18
18
  * environment.
19
19
  */
20
- export const REQUIRED_API_URL_ENV = "OXYGEN_REQUIRE_API_URL";
20
+ const REQUIRED_API_URL_ENV = "OXYGEN_REQUIRE_API_URL";
21
21
  export function resolveCliBinaryName(env = process.env, argv = process.argv) {
22
22
  const explicit = normalizeBinaryName(env.OXYGEN_CLI_BINARY ?? env.OXYGEN_CLI_NAME);
23
23
  if (explicit)
package/dist/session.d.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  export type SessionStepStatus = "pending" | "running" | "completed" | "error" | "skipped";
2
- export type SessionStep = {
2
+ type SessionStep = {
3
3
  label: string;
4
4
  status: SessionStepStatus;
5
5
  startedAt: string | null;
6
6
  completedAt: string | null;
7
7
  };
8
- export type SessionStatusMessage = {
8
+ type SessionStatusMessage = {
9
9
  id: string;
10
10
  message: string;
11
11
  stepIndex: number | null;
12
12
  createdAt: string;
13
13
  };
14
- export type SessionOutput = {
14
+ type SessionOutput = {
15
15
  id: string;
16
16
  type: "csv" | "file" | "table";
17
17
  path: string | null;
@@ -54,3 +54,4 @@ export declare function addSessionOutput(input: {
54
54
  export declare function getSessionUsage(input: {
55
55
  sessionId?: string | null;
56
56
  }): Record<string, unknown>;
57
+ export {};
package/dist/skills.d.ts CHANGED
@@ -7,7 +7,8 @@ export type SkillsInstallOptions = {
7
7
  apiUrl?: string;
8
8
  agents?: string | string[];
9
9
  skill?: string;
10
- project?: boolean;
10
+ /** `true` for the current directory, or the project directory to install into. */
11
+ project?: boolean | string;
11
12
  copy?: boolean;
12
13
  };
13
14
  export type SkillsListOptions = {
@@ -33,12 +34,6 @@ export type AutomaticSkillsInstallResult = {
33
34
  details?: unknown;
34
35
  };
35
36
  };
36
- export type SkillsAccessResponse = {
37
- base_url: string;
38
- index_url: string;
39
- expires_at: string;
40
- skills_version?: string;
41
- };
42
37
  export type SkillsInstallSource = {
43
38
  authenticated: boolean;
44
39
  /** Base URL handed to `npx skills add` (tokenized when authenticated). */
@@ -72,6 +67,12 @@ options: SkillsInstallOptions, runtime?: {
72
67
  execFileSync?: ExecFileSyncLike;
73
68
  spawnSync?: SpawnSyncLike;
74
69
  } & SkillsSourceRuntime): Promise<Record<string, unknown>>;
70
+ /**
71
+ * `--project` is a bare flag for the current directory, or names the project
72
+ * directory to install into. `--project .` used to fail with "too many
73
+ * arguments" (2026-09-16 blind baseline) because the flag took no value.
74
+ */
75
+ export declare function resolveSkillProjectDir(project: boolean | string | undefined): string | null;
75
76
  export declare function runAutomaticSkillsInstall(options?: {
76
77
  apiUrl?: string;
77
78
  env?: NodeJS.ProcessEnv;
package/dist/skills.js CHANGED
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { homedir, tmpdir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
- import { OxygenError, inferUserCapabilityRoute, serializeCapabilityRoute, toFailure, } from "@oxygen/shared";
5
+ import { OxygenError, errorMessage, inferUserCapabilityRoute, serializeCapabilityRoute, toFailure, } from "@oxygen/shared";
6
6
  import { defaultApiUrl, loadCredentials, normalizeApiUrl } from "./credentials.js";
7
7
  import { requestOxygen } from "./http-client.js";
8
8
  import { isRecord, readOption } from "./util.js";
@@ -226,7 +226,8 @@ options, runtime = {}) {
226
226
  const env = runtime.env ?? process.env;
227
227
  const platform = runtime.platform ?? process.platform;
228
228
  const spawn = runtime.spawnSync ?? wrapExecFileSync(runtime.execFileSync) ?? spawnSync;
229
- const installLocations = resolveSkillInstallLocations(agents, options.project === true, env);
229
+ const projectDir = resolveSkillProjectDir(options.project);
230
+ const installLocations = resolveSkillInstallLocations(agents, projectDir, env);
230
231
  // The vendored installer writes immediately once spawned. Tell humans and
231
232
  // agents the exact destination first so a default global install is never a
232
233
  // surprise, while keeping stdout as one stable JSON envelope under --json.
@@ -268,6 +269,8 @@ options, runtime = {}) {
268
269
  const result = spawn(invocation.executable, invocation.args, {
269
270
  encoding: "utf8",
270
271
  env,
272
+ // A project install lands relative to the installer's working directory.
273
+ ...(projectDir ? { cwd: projectDir } : {}),
271
274
  stdio: ["ignore", "pipe", "pipe"],
272
275
  windowsHide: true,
273
276
  ...(platform === "win32" ? { windowsVerbatimArguments: true } : {}),
@@ -342,11 +345,25 @@ options, runtime = {}) {
342
345
  summary: summarizeInstallOutcome(agents.length, skippedAgents.length, 0),
343
346
  };
344
347
  }
345
- function resolveSkillInstallLocations(agents, project, env) {
348
+ /**
349
+ * `--project` is a bare flag for the current directory, or names the project
350
+ * directory to install into. `--project .` used to fail with "too many
351
+ * arguments" (2026-09-16 blind baseline) because the flag took no value.
352
+ */
353
+ export function resolveSkillProjectDir(project) {
354
+ if (project === true)
355
+ return process.cwd();
356
+ if (typeof project === "string" && project.trim())
357
+ return resolve(project.trim());
358
+ return null;
359
+ }
360
+ function resolveSkillInstallLocations(agents, projectDir, env) {
361
+ const project = projectDir !== null;
362
+ const base = projectDir ?? process.cwd();
346
363
  const projectRoots = {
347
- codex: resolve(".agents", "skills"),
348
- "claude-code": resolve(".claude", "skills"),
349
- cursor: resolve(".agents", "skills"),
364
+ codex: resolve(base, ".agents", "skills"),
365
+ "claude-code": resolve(base, ".claude", "skills"),
366
+ cursor: resolve(base, ".agents", "skills"),
350
367
  };
351
368
  const globalRoots = {
352
369
  codex: join(env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "skills"),
@@ -356,7 +373,7 @@ function resolveSkillInstallLocations(agents, project, env) {
356
373
  const roots = project ? projectRoots : globalRoots;
357
374
  return agents.map((agent) => ({
358
375
  agent,
359
- path: roots[agent] ?? (project ? resolve(".agents", "skills") : join(homedir(), ".agents", "skills")),
376
+ path: roots[agent] ?? (project ? resolve(base, ".agents", "skills") : join(homedir(), ".agents", "skills")),
360
377
  }));
361
378
  }
362
379
  /**
@@ -448,9 +465,6 @@ function stagingFailure(stage, details) {
448
465
  exitCode: 1,
449
466
  });
450
467
  }
451
- function errorMessage(error) {
452
- return error instanceof Error ? error.message : String(error);
453
- }
454
468
  // Best-effort resolution of a skill's declared prerequisite skills from the
455
469
  // index. Any failure (unreachable index, parse error, unknown skill) yields no
456
470
  // dependencies so a dependency lookup can never block or fail an install.
@@ -1,4 +1,4 @@
1
- export type TranscriptSource = "claude_code" | "codex" | "cursor" | "file";
1
+ type TranscriptSource = "claude_code" | "codex" | "cursor" | "file";
2
2
  export type CapturedTranscript = {
3
3
  source: TranscriptSource;
4
4
  session_id: string | null;
@@ -19,3 +19,4 @@ export declare class TranscriptCaptureError extends Error {
19
19
  }
20
20
  export declare function captureCurrentTranscript(options?: CaptureOptions): CapturedTranscript | null;
21
21
  export declare function collectFeedbackEnvironment(): Record<string, string>;
22
+ export {};
package/dist/util.d.ts CHANGED
@@ -1,3 +1,3 @@
1
+ export { isRecord } from "@oxygen/shared";
1
2
  export declare function readOption(value: string | undefined): string | null;
2
- export declare function isRecord(value: unknown): value is Record<string, unknown>;
3
3
  export declare function readErrorMessage(error: unknown): string;
package/dist/util.js CHANGED
@@ -1,9 +1,7 @@
1
+ export { isRecord } from "@oxygen/shared";
1
2
  export function readOption(value) {
2
3
  return typeof value === "string" && value.trim() ? value.trim() : null;
3
4
  }
4
- export function isRecord(value) {
5
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6
- }
7
5
  export function readErrorMessage(error) {
8
6
  return error instanceof Error ? error.message : "unknown error";
9
7
  }
@@ -3,6 +3,16 @@
3
3
  * imported from a host package) so the engine stays dependency-free and
4
4
  * browser-safe; tenant-db re-exports it from its own `coerce.ts` so the
5
5
  * existing internal import path keeps working.
6
+ *
7
+ * Do NOT replace this with `export { isRecord } from "@oxygen/shared"`. This
8
+ * file is value-re-exported by ./index.ts, so that edge drags the whole
9
+ * @oxygen/shared root barrel into every browser bundle that touches the formula
10
+ * engine — packages/workflows/src/graph/browser-safety.test.ts fails on exactly
11
+ * that ("packages/formula/src/coerce.ts imports @oxygen/shared"). @oxygen/shared
12
+ * does export a `./type-guards` leaf, but importing it would still make this
13
+ * browser-safe package depend on @oxygen/shared, which the same guard forbids
14
+ * (packages/formula declares no @oxygen/* dependency at all); the duplicate body
15
+ * is deliberate.
6
16
  */
7
17
  /** Loose record check: any non-array object (class instances included). */
8
18
  export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -3,6 +3,16 @@
3
3
  * imported from a host package) so the engine stays dependency-free and
4
4
  * browser-safe; tenant-db re-exports it from its own `coerce.ts` so the
5
5
  * existing internal import path keeps working.
6
+ *
7
+ * Do NOT replace this with `export { isRecord } from "@oxygen/shared"`. This
8
+ * file is value-re-exported by ./index.ts, so that edge drags the whole
9
+ * @oxygen/shared root barrel into every browser bundle that touches the formula
10
+ * engine — packages/workflows/src/graph/browser-safety.test.ts fails on exactly
11
+ * that ("packages/formula/src/coerce.ts imports @oxygen/shared"). @oxygen/shared
12
+ * does export a `./type-guards` leaf, but importing it would still make this
13
+ * browser-safe package depend on @oxygen/shared, which the same guard forbids
14
+ * (packages/formula declares no @oxygen/* dependency at all); the duplicate body
15
+ * is deliberate.
6
16
  */
7
17
  /** Loose record check: any non-array object (class instances included). */
8
18
  export function isRecord(value) {
@@ -1,5 +1,7 @@
1
1
  import { OxygenError } from "@oxygen/shared/cli-result";
2
2
  import { walkJsonPath } from "@oxygen/shared/json-path";
3
+ import { resolveLinkedInCountry } from "@oxygen/shared/linkedin-countries";
4
+ import { md5Hex, sha256Hex } from "./hash.js";
3
5
  import { normalizeDomain, normalizeEmail, normalizeLinkedinUrl, } from "./value-normalizers.js";
4
6
  import { classifyEmailType, cleanJobTitle, normalizeCompanyName, splitDelimitedList, } from "./value-cleaners.js";
5
7
  // Pattern LENGTH is not a safety bound and never was -- catastrophic backtracking
@@ -578,6 +580,56 @@ const SPECS = [
578
580
  examples: [{ expression: "title_case(\"acme CORP\")", result: "Acme Corp" }],
579
581
  evaluate: (args) => asText(args[0]).replace(/([A-Za-z])([A-Za-z]*)/g, (_match, first, rest) => first.toUpperCase() + rest.toLowerCase()),
580
582
  }),
583
+ spec({
584
+ name: "sha256",
585
+ category: "string",
586
+ signature: "sha256(text)",
587
+ description: "Hex-encoded SHA-256 digest of the text (UTF-8). Computed in-process, so it costs no credits. Blank input returns null.",
588
+ minArgs: 1,
589
+ maxArgs: 1,
590
+ examples: [{ expression: "sha256(\"abc\")", result: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" }],
591
+ evaluate: (args) => blankToNull(args[0], (input) => sha256Hex(asText(input))),
592
+ }),
593
+ spec({
594
+ name: "md5",
595
+ category: "string",
596
+ signature: "md5(text)",
597
+ description: "Hex-encoded MD5 digest of the text (UTF-8). Computed in-process, so it costs no credits. Blank input returns null. MD5 exists here for ad-platform audience matching, never for anything security-bearing.",
598
+ minArgs: 1,
599
+ maxArgs: 1,
600
+ examples: [{ expression: "md5(\"abc\")", result: "900150983cd24fb0d6963f7d28e17f72" }],
601
+ evaluate: (args) => blankToNull(args[0], (input) => md5Hex(asText(input))),
602
+ }),
603
+ spec({
604
+ name: "email_hash",
605
+ category: "string",
606
+ signature: "email_hash(email, algorithm?)",
607
+ description: "Normalizes an email address and returns its hex digest — the hashed identifier ad platforms take for custom audiences. Algorithm is \"sha256\" (default) or \"md5\"; blank input returns null.",
608
+ minArgs: 1,
609
+ maxArgs: 2,
610
+ examples: [{ expression: "email_hash(\" Ada@Example.COM \")", result: "b5fc85e55755f9e0d030a10ab4429b6b2944855f9a0d60077fe832becbc41d72" }],
611
+ evaluate: (args) => {
612
+ // The algorithm is checked before the blank check on purpose: an unknown
613
+ // algorithm is an authoring mistake, not row data, so it must fail on
614
+ // every row rather than only on the rows that carry an address.
615
+ const algorithm = args.length > 1 && !isBlankFormulaValue(args[1])
616
+ ? asText(args[1]).trim().toLowerCase()
617
+ : "sha256";
618
+ if (algorithm !== "sha256" && algorithm !== "md5") {
619
+ throw formulaExpressionError("email_hash supports the \"sha256\" and \"md5\" algorithms only.", {
620
+ function: "email_hash",
621
+ algorithm: args[1],
622
+ });
623
+ }
624
+ return blankToNull(args[0], (input) => {
625
+ // Trim + lowercase and nothing else. Meta, Google, and LinkedIn all
626
+ // specify exactly that normalization, so stripping dots or plus-tags
627
+ // here would hash an address the platform never matches.
628
+ const normalized = normalizeEmail(asText(input));
629
+ return algorithm === "md5" ? md5Hex(normalized) : sha256Hex(normalized);
630
+ });
631
+ },
632
+ }),
581
633
  spec({
582
634
  name: "pad_start",
583
635
  category: "string",
@@ -943,6 +995,19 @@ const SPECS = [
943
995
  }
944
996
  }),
945
997
  }),
998
+ spec({
999
+ name: "linkedin_country",
1000
+ category: "url_email",
1001
+ signature: "linkedin_country(code_or_name)",
1002
+ description: "Canonical LinkedIn country name for an ISO alpha-2 code, a country name, or a known alias. Returns blank for anything unrecognized, so a select column never stores a value outside its own option list.",
1003
+ minArgs: 1,
1004
+ maxArgs: 1,
1005
+ examples: [
1006
+ { expression: "linkedin_country(\"US\")", result: "United States" },
1007
+ { expression: "linkedin_country(\"Congo - Kinshasa\")", result: "Congo (DRC)" },
1008
+ ],
1009
+ evaluate: (args) => blankToNull(args[0], (input) => resolveLinkedInCountry(asText(input))),
1010
+ }),
946
1011
  spec({
947
1012
  name: "normalize_linkedin_url",
948
1013
  category: "url_email",
@@ -0,0 +1,19 @@
1
+ /**
2
+ * SHA-256 and MD5, hand-rolled and dependency-free.
3
+ *
4
+ * WHY HAND-ROLLED: this package is bundled into the browser (formula editor
5
+ * previews, the visual workflow editor) and its tsconfig sets `types: []` on
6
+ * purpose, so `node:crypto` is not importable here; `crypto.subtle.digest` is
7
+ * async and formula evaluation is synchronous per row, so a Promise cannot be
8
+ * returned to a cell either. Both digests are therefore computed inline —
9
+ * the textbook algorithms (FIPS 180-4 / RFC 1321), UTF-8 in, lowercase hex out.
10
+ *
11
+ * NOT FOR SECRETS. These exist because the ad platforms (Meta, Google,
12
+ * LinkedIn) specify exactly SHA-256 or MD5 over a trimmed, lowercased address
13
+ * for customer-match audience uploads. Do not reach for them for anything that
14
+ * needs a password hash, a MAC, or collision resistance.
15
+ */
16
+ /** Hex-encoded SHA-256 of the UTF-8 encoding of `input`. */
17
+ export declare function sha256Hex(input: string): string;
18
+ /** Hex-encoded MD5 of the UTF-8 encoding of `input`. */
19
+ export declare function md5Hex(input: string): string;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * SHA-256 and MD5, hand-rolled and dependency-free.
3
+ *
4
+ * WHY HAND-ROLLED: this package is bundled into the browser (formula editor
5
+ * previews, the visual workflow editor) and its tsconfig sets `types: []` on
6
+ * purpose, so `node:crypto` is not importable here; `crypto.subtle.digest` is
7
+ * async and formula evaluation is synchronous per row, so a Promise cannot be
8
+ * returned to a cell either. Both digests are therefore computed inline —
9
+ * the textbook algorithms (FIPS 180-4 / RFC 1321), UTF-8 in, lowercase hex out.
10
+ *
11
+ * NOT FOR SECRETS. These exist because the ad platforms (Meta, Google,
12
+ * LinkedIn) specify exactly SHA-256 or MD5 over a trimmed, lowercased address
13
+ * for customer-match audience uploads. Do not reach for them for anything that
14
+ * needs a password hash, a MAC, or collision resistance.
15
+ */
16
+ // `noUncheckedIndexedAccess` types every element read as possibly undefined.
17
+ // Every index below is provably in range (fixed-size constant tables and a
18
+ // 64-word schedule), so the reads go through these accessors rather than
19
+ // scattering assertions through the rounds.
20
+ function wordAt(words, index) {
21
+ return words[index];
22
+ }
23
+ function shiftAt(shifts, index) {
24
+ return shifts[index];
25
+ }
26
+ function rotateRight(value, bits) {
27
+ return ((value >>> bits) | (value << (32 - bits))) >>> 0;
28
+ }
29
+ function rotateLeft(value, bits) {
30
+ return ((value << bits) | (value >>> (32 - bits))) >>> 0;
31
+ }
32
+ function hex32BigEndian(value) {
33
+ return (value >>> 0).toString(16).padStart(8, "0");
34
+ }
35
+ function hex32LittleEndian(value) {
36
+ let out = "";
37
+ for (let byte = 0; byte < 4; byte += 1) {
38
+ out += ((value >>> (byte * 8)) & 0xff).toString(16).padStart(2, "0");
39
+ }
40
+ return out;
41
+ }
42
+ /**
43
+ * Merkle-Damgård padding shared by both digests: append 0x80, zero-fill to a
44
+ * 64-byte boundary, then write the message length in bits into the last eight
45
+ * bytes (big-endian for SHA-256, little-endian for MD5).
46
+ */
47
+ function padMessage(bytes, littleEndianLength) {
48
+ const paddedLength = (((bytes.length + 8) >> 6) + 1) << 6;
49
+ const padded = new Uint8Array(paddedLength);
50
+ padded.set(bytes);
51
+ padded[bytes.length] = 0x80;
52
+ const view = new DataView(padded.buffer);
53
+ const bitLength = bytes.length * 8;
54
+ const low = bitLength >>> 0;
55
+ const high = Math.floor(bitLength / 0x1_0000_0000);
56
+ if (littleEndianLength) {
57
+ view.setUint32(paddedLength - 8, low, true);
58
+ view.setUint32(paddedLength - 4, high, true);
59
+ }
60
+ else {
61
+ view.setUint32(paddedLength - 8, high, false);
62
+ view.setUint32(paddedLength - 4, low, false);
63
+ }
64
+ return view;
65
+ }
66
+ function utf8Bytes(input) {
67
+ return new TextEncoder().encode(input);
68
+ }
69
+ const SHA256_K = new Uint32Array([
70
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
71
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
72
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
73
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
74
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
75
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
76
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
77
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
78
+ ]);
79
+ /** Hex-encoded SHA-256 of the UTF-8 encoding of `input`. */
80
+ export function sha256Hex(input) {
81
+ const view = padMessage(utf8Bytes(input), false);
82
+ const state = new Uint32Array([
83
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
84
+ 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
85
+ ]);
86
+ const schedule = new Uint32Array(64);
87
+ for (let offset = 0; offset < view.byteLength; offset += 64) {
88
+ for (let i = 0; i < 16; i += 1) {
89
+ schedule[i] = view.getUint32(offset + i * 4, false);
90
+ }
91
+ for (let i = 16; i < 64; i += 1) {
92
+ const previous = wordAt(schedule, i - 15);
93
+ const recent = wordAt(schedule, i - 2);
94
+ const s0 = rotateRight(previous, 7) ^ rotateRight(previous, 18) ^ (previous >>> 3);
95
+ const s1 = rotateRight(recent, 17) ^ rotateRight(recent, 19) ^ (recent >>> 10);
96
+ schedule[i] = (wordAt(schedule, i - 16) + s0 + wordAt(schedule, i - 7) + s1) >>> 0;
97
+ }
98
+ let a = wordAt(state, 0);
99
+ let b = wordAt(state, 1);
100
+ let c = wordAt(state, 2);
101
+ let d = wordAt(state, 3);
102
+ let e = wordAt(state, 4);
103
+ let f = wordAt(state, 5);
104
+ let g = wordAt(state, 6);
105
+ let h = wordAt(state, 7);
106
+ for (let i = 0; i < 64; i += 1) {
107
+ const s1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
108
+ const choice = (e & f) ^ (~e & g);
109
+ const temp1 = (h + s1 + choice + wordAt(SHA256_K, i) + wordAt(schedule, i)) >>> 0;
110
+ const s0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
111
+ const majority = (a & b) ^ (a & c) ^ (b & c);
112
+ const temp2 = (s0 + majority) >>> 0;
113
+ h = g;
114
+ g = f;
115
+ f = e;
116
+ e = (d + temp1) >>> 0;
117
+ d = c;
118
+ c = b;
119
+ b = a;
120
+ a = (temp1 + temp2) >>> 0;
121
+ }
122
+ state[0] = (wordAt(state, 0) + a) >>> 0;
123
+ state[1] = (wordAt(state, 1) + b) >>> 0;
124
+ state[2] = (wordAt(state, 2) + c) >>> 0;
125
+ state[3] = (wordAt(state, 3) + d) >>> 0;
126
+ state[4] = (wordAt(state, 4) + e) >>> 0;
127
+ state[5] = (wordAt(state, 5) + f) >>> 0;
128
+ state[6] = (wordAt(state, 6) + g) >>> 0;
129
+ state[7] = (wordAt(state, 7) + h) >>> 0;
130
+ }
131
+ let digest = "";
132
+ for (let i = 0; i < 8; i += 1)
133
+ digest += hex32BigEndian(wordAt(state, i));
134
+ return digest;
135
+ }
136
+ const MD5_K = new Uint32Array([
137
+ 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
138
+ 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
139
+ 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
140
+ 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
141
+ 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
142
+ 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
143
+ 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
144
+ 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
145
+ ]);
146
+ const MD5_SHIFTS = [
147
+ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
148
+ 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
149
+ 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
150
+ 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
151
+ ];
152
+ /** Hex-encoded MD5 of the UTF-8 encoding of `input`. */
153
+ export function md5Hex(input) {
154
+ const view = padMessage(utf8Bytes(input), true);
155
+ const state = new Uint32Array([0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]);
156
+ const block = new Uint32Array(16);
157
+ for (let offset = 0; offset < view.byteLength; offset += 64) {
158
+ for (let i = 0; i < 16; i += 1) {
159
+ block[i] = view.getUint32(offset + i * 4, true);
160
+ }
161
+ let a = wordAt(state, 0);
162
+ let b = wordAt(state, 1);
163
+ let c = wordAt(state, 2);
164
+ let d = wordAt(state, 3);
165
+ for (let i = 0; i < 64; i += 1) {
166
+ let mixed;
167
+ let wordIndex;
168
+ if (i < 16) {
169
+ mixed = (b & c) | (~b & d);
170
+ wordIndex = i;
171
+ }
172
+ else if (i < 32) {
173
+ mixed = (d & b) | (~d & c);
174
+ wordIndex = (5 * i + 1) % 16;
175
+ }
176
+ else if (i < 48) {
177
+ mixed = b ^ c ^ d;
178
+ wordIndex = (3 * i + 5) % 16;
179
+ }
180
+ else {
181
+ mixed = c ^ (b | ~d);
182
+ wordIndex = (7 * i) % 16;
183
+ }
184
+ const rotated = rotateLeft((mixed + a + wordAt(MD5_K, i) + wordAt(block, wordIndex)) >>> 0, shiftAt(MD5_SHIFTS, i));
185
+ a = d;
186
+ d = c;
187
+ c = b;
188
+ b = (b + rotated) >>> 0;
189
+ }
190
+ state[0] = (wordAt(state, 0) + a) >>> 0;
191
+ state[1] = (wordAt(state, 1) + b) >>> 0;
192
+ state[2] = (wordAt(state, 2) + c) >>> 0;
193
+ state[3] = (wordAt(state, 3) + d) >>> 0;
194
+ }
195
+ let digest = "";
196
+ for (let i = 0; i < 4; i += 1)
197
+ digest += hex32LittleEndian(wordAt(state, i));
198
+ return digest;
199
+ }
@@ -52,7 +52,12 @@ export declare function cleanJobTitle(value: unknown): string | null;
52
52
  * Blank input returns an empty array.
53
53
  */
54
54
  export declare function splitDelimitedList(value: unknown, separator?: string | null): string[];
55
- /** Consumer mailbox providers — a person, not a company domain. */
55
+ /**
56
+ * Consumer mailbox providers — a person, not a company domain. Re-exported from
57
+ * `@oxygen/shared/freemail-domains`, which is now the ONE list; this module used to
58
+ * carry its own 101-entry copy and was the best-maintained of eight divergent forks.
59
+ * Its entries survive as the bulk of the shared broad tier.
60
+ */
56
61
  export declare const FREE_MAIL_DOMAINS: ReadonlySet<string>;
57
62
  /** Throwaway inbox providers — never worth a send, never worth a credit. */
58
63
  export declare const DISPOSABLE_EMAIL_DOMAINS: ReadonlySet<string>;
@@ -9,6 +9,7 @@
9
9
  * model. When a deterministic list is not enough, the paid verifier columns
10
10
  * are the upgrade — these helpers are the free first pass.
11
11
  */
12
+ import { PERSONAL_EMAIL_DOMAINS } from "@oxygen/shared/freemail-domains";
12
13
  // ---------------------------------------------------------------------------
13
14
  // Shared text helpers
14
15
  // ---------------------------------------------------------------------------
@@ -279,32 +280,15 @@ export function splitDelimitedList(value, separator) {
279
280
  // ---------------------------------------------------------------------------
280
281
  // Email classification
281
282
  // ---------------------------------------------------------------------------
282
- /** Consumer mailbox providers — a person, not a company domain. */
283
- export const FREE_MAIL_DOMAINS = new Set([
284
- "gmail.com", "googlemail.com",
285
- "yahoo.com", "yahoo.co.uk", "yahoo.fr", "yahoo.de", "yahoo.co.jp", "yahoo.ca",
286
- "yahoo.com.au", "yahoo.es", "yahoo.it", "ymail.com", "rocketmail.com",
287
- "hotmail.com", "hotmail.co.uk", "hotmail.fr", "hotmail.de", "hotmail.it", "hotmail.es",
288
- "outlook.com", "outlook.fr", "outlook.de", "outlook.es", "outlook.co.uk",
289
- "live.com", "live.co.uk", "live.fr", "live.de", "msn.com",
290
- "aol.com", "icloud.com", "me.com", "mac.com",
291
- "protonmail.com", "proton.me", "pm.me",
292
- "gmx.com", "gmx.de", "gmx.net", "gmx.at", "gmx.ch",
293
- "web.de", "t-online.de", "freenet.de",
294
- "mail.com", "mail.ru", "yandex.ru", "yandex.com",
295
- "zoho.com", "fastmail.com", "hey.com", "tutanota.com", "tuta.io",
296
- "orange.fr", "wanadoo.fr", "free.fr", "laposte.net", "sfr.fr",
297
- "libero.it", "virgilio.it", "seznam.cz",
298
- "wp.pl", "o2.pl", "onet.pl", "interia.pl",
299
- "qq.com", "163.com", "126.com", "sina.com",
300
- "naver.com", "daum.net", "hanmail.net", "rediffmail.com",
301
- "bigpond.com", "optusnet.com.au", "telus.net", "shaw.ca", "sympatico.ca", "rogers.com",
302
- "btinternet.com", "sky.com", "talktalk.net", "virginmedia.com", "blueyonder.co.uk",
303
- "comcast.net", "verizon.net", "att.net", "sbcglobal.net", "cox.net",
304
- "charter.net", "earthlink.net", "bellsouth.net",
305
- "ziggo.nl", "kpnmail.nl", "home.nl", "telenet.be", "skynet.be", "bluewin.ch",
306
- "uol.com.br", "bol.com.br", "terra.com.br",
307
- ]);
283
+ /**
284
+ * Consumer mailbox providers — a person, not a company domain. Re-exported from
285
+ * `@oxygen/shared/freemail-domains`, which is now the ONE list; this module used to
286
+ * carry its own 101-entry copy and was the best-maintained of eight divergent forks.
287
+ * Its entries survive as the bulk of the shared broad tier.
288
+ */
289
+ // Aliased rather than `export … from`, because this module also USES the set locally
290
+ // (see classifyDomain below) and a re-export creates no local binding.
291
+ export const FREE_MAIL_DOMAINS = PERSONAL_EMAIL_DOMAINS;
308
292
  /** Throwaway inbox providers — never worth a send, never worth a credit. */
309
293
  export const DISPOSABLE_EMAIL_DOMAINS = new Set([
310
294
  "mailinator.com", "guerrillamail.com", "guerrillamail.net",
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Small array helpers shared across every OXYGEN package.
3
+ */
4
+ /** Split `values` into consecutive slices of at most `size` entries. */
5
+ export declare function chunk<T>(values: T[], size: number): T[][];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Small array helpers shared across every OXYGEN package.
3
+ */
4
+ /** Split `values` into consecutive slices of at most `size` entries. */
5
+ export function chunk(values, size) {
6
+ const chunks = [];
7
+ for (let index = 0; index < values.length; index += size) {
8
+ chunks.push(values.slice(index, index + size));
9
+ }
10
+ return chunks;
11
+ }