@oxygen-agent/cli 1.242.6 → 1.244.2

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 CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation, visit https://oxygen-agent.com/docs. For support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.242.6
37
+ Version: 1.244.2
package/dist/index.js CHANGED
@@ -1796,7 +1796,7 @@ export function createProgram() {
1796
1796
  }));
1797
1797
  }))
1798
1798
  .addCommand(new Command("configure")
1799
- .description("Configure a standing bidirectional sync for one CRM object pair. Defaults to dry-run, which returns a real preview of the next cycle.")
1799
+ .description("Configure a standing bidirectional sync for one CRM object pair. Defaults to dry-run, which returns a real preview of the next cycle; inbound and bidirectional previews perform bounded CRM provider reads and may use external API quota.")
1800
1800
  .requiredOption("--provider <provider>", "Connected CRM: hubspot or attio.")
1801
1801
  .requiredOption("--object <object>", "Provider object: hubspot contacts|companies, or attio people|companies.")
1802
1802
  .option("--direction <direction>", "Sync direction: inbound, outbound, or bidirectional. Defaults to bidirectional.")
@@ -1805,7 +1805,7 @@ export function createProgram() {
1805
1805
  .option("--max-rows <n>", "Per-cycle row cap. The real volume brake on provider writes (BYOK writes cost 0 Oxygen credits).")
1806
1806
  .option("--max-credits <n>", "Managed-credit + external-quota cap per cycle.")
1807
1807
  .option("--approved", "Confirm a live configuration after inspecting a dry run.")
1808
- .option("--dry-run", "Preview the configuration and next cycle without writing. Default.")
1808
+ .option("--dry-run", "Preview the configuration and next cycle without writing. Inbound/bidirectional previews perform bounded CRM reads and may use external API quota. Default.")
1809
1809
  .option("--live", "Write the configuration. Requires --approved and --max-credits.")
1810
1810
  .option("--json", "Print a JSON envelope.")
1811
1811
  .action(async (options) => {
@@ -13,6 +13,6 @@ export declare function assertCustomHttpPublicUrlSyntax(url: string | URL): URL;
13
13
  export declare function assertCustomHttpResolvedHostAllowed(input: {
14
14
  url: string | URL;
15
15
  resolveHostname?: CustomHttpResolveHostname;
16
- }): Promise<void>;
16
+ }): Promise<CustomHttpResolvedAddress[]>;
17
17
  export declare function normalizeCustomHttpUrlHost(hostname: string): string;
18
18
  export declare function isBlockedCustomHttpHost(host: string): boolean;
@@ -29,8 +29,10 @@ export function assertCustomHttpPublicUrlSyntax(url) {
29
29
  export async function assertCustomHttpResolvedHostAllowed(input) {
30
30
  const parsed = assertCustomHttpPublicUrlSyntax(input.url);
31
31
  const host = normalizeCustomHttpUrlHost(parsed.hostname);
32
+ // Literal IP hosts were already validated by assertCustomHttpPublicUrlSyntax and
33
+ // need no DNS resolution (nor connection pinning) — return an empty answer set.
32
34
  if (isIP(host))
33
- return;
35
+ return [];
34
36
  const resolveHostname = input.resolveHostname ?? defaultResolveHostname;
35
37
  let addresses;
36
38
  try {
@@ -53,6 +55,9 @@ export async function assertCustomHttpResolvedHostAllowed(input) {
53
55
  throw new CustomHttpUrlSafetyError("blocked_resolved_address", "Custom HTTP URL host resolved to a non-public address.", { host, address: normalizedAddress, family: address.family ?? null });
54
56
  }
55
57
  }
58
+ // Return the validated addresses so callers can pin the connection to a checked
59
+ // IP and close the DNS-rebinding TOCTOU (fetch would otherwise re-resolve).
60
+ return addresses;
56
61
  }
57
62
  export function normalizeCustomHttpUrlHost(hostname) {
58
63
  const host = hostname.toLowerCase().replace(/\.+$/, "");
@@ -137,20 +142,47 @@ function splitCustomHttpIpv6Part(part) {
137
142
  return [];
138
143
  const groups = part.split(":");
139
144
  const values = [];
140
- for (const group of groups) {
145
+ for (let index = 0; index < groups.length; index += 1) {
146
+ const group = groups[index] ?? "";
147
+ // A trailing dotted-decimal group (e.g. `8.8.8.8` in `::ffff:8.8.8.8`) is a
148
+ // valid IPv4-in-IPv6 literal; fold it into its two 16-bit words so mapped/
149
+ // compatible addresses parse instead of fail-closing as "blocked".
150
+ if (index === groups.length - 1 && group.includes(".")) {
151
+ const octets = customHttpDottedIpv4Octets(group);
152
+ if (!octets)
153
+ return null;
154
+ values.push((octets[0] << 8) | octets[1], (octets[2] << 8) | octets[3]);
155
+ continue;
156
+ }
141
157
  if (!/^[0-9a-f]{1,4}$/.test(group))
142
158
  return null;
143
159
  values.push(Number.parseInt(group, 16));
144
160
  }
145
161
  return values;
146
162
  }
163
+ function customHttpDottedIpv4Octets(value) {
164
+ if (isIP(value) !== 4)
165
+ return null;
166
+ const octets = value.split(".").map((octet) => Number(octet));
167
+ return [octets[0] ?? 0, octets[1] ?? 0, octets[2] ?? 0, octets[3] ?? 0];
168
+ }
147
169
  function customHttpIpv4FromIpv6(groups) {
148
170
  const mappedPrefix = groups.slice(0, 5).every((group) => group === 0) && groups[5] === 0xffff;
149
171
  const compatiblePrefix = groups.slice(0, 6).every((group) => group === 0);
150
- if (!mappedPrefix && !compatiblePrefix)
151
- return null;
152
- const high = groups[6] ?? 0;
153
- const low = groups[7] ?? 0;
172
+ // NAT64/DNS64 well-known prefix 64:ff9b::/96 embeds the IPv4 in the last 32 bits.
173
+ const nat64WellKnownPrefix = groups[0] === 0x0064
174
+ && groups[1] === 0xff9b
175
+ && groups.slice(2, 6).every((group) => group === 0);
176
+ if (mappedPrefix || compatiblePrefix || nat64WellKnownPrefix) {
177
+ return customHttpIpv4FromIpv6Pair(groups[6], groups[7]);
178
+ }
179
+ // 6to4 prefix 2002::/16 embeds the gateway IPv4 in the two groups after the prefix.
180
+ if (groups[0] === 0x2002) {
181
+ return customHttpIpv4FromIpv6Pair(groups[1], groups[2]);
182
+ }
183
+ return null;
184
+ }
185
+ function customHttpIpv4FromIpv6Pair(high = 0, low = 0) {
154
186
  if (high === 0 && low === 0)
155
187
  return null;
156
188
  return [
@@ -1,2 +1,2 @@
1
- export declare const OXYGEN_VERSION = "1.242.6";
1
+ export declare const OXYGEN_VERSION = "1.244.2";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.242.6";
1
+ export const OXYGEN_VERSION = "1.244.2";
2
2
  // Bump this only when deployed CLI/API contracts require a newer CLI.
3
3
  // 1.181.0: paid table action runs and background columns run require
4
4
  // approved=true in addition to max_credits; older CLIs cannot send the flag.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.242.6",
3
+ "version": "1.244.2",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",