@hasna/domains 0.0.43 → 0.0.45

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/dist/cli/index.js CHANGED
@@ -41360,8 +41360,12 @@ import { existsSync as existsSync4 } from "fs";
41360
41360
  import { homedir as homedir5 } from "os";
41361
41361
  import { join as join6 } from "path";
41362
41362
  import { createHmac, timingSafeEqual } from "crypto";
41363
+ import { lookup as dnsLookup } from "dns/promises";
41364
+ import { isIP as isIP2 } from "net";
41363
41365
  import { randomUUID } from "crypto";
41364
41366
  import { spawn } from "child_process";
41367
+ import { request as nodeHttpRequest } from "http";
41368
+ import { request as nodeHttpsRequest } from "https";
41365
41369
  import { randomUUID as randomUUID2 } from "crypto";
41366
41370
  function getPathValue(input, path) {
41367
41371
  return path.split(".").reduce((value, part) => {
@@ -41758,6 +41762,177 @@ function signPayload(secret, timestamp, body) {
41758
41762
  const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
41759
41763
  return `sha256=${digest}`;
41760
41764
  }
41765
+ function isPrivateAddress(address) {
41766
+ const normalized = stripZoneId(address);
41767
+ const version2 = isIP2(normalized);
41768
+ if (version2 === 4) {
41769
+ const integer = ipv4ToInt(normalized);
41770
+ if (integer === undefined)
41771
+ return true;
41772
+ return IPV4_PRIVATE_RANGES.some(([low, high]) => integer >= low && integer <= high);
41773
+ }
41774
+ if (version2 === 6) {
41775
+ const groups = ipv6Groups(normalized);
41776
+ if (!groups)
41777
+ return true;
41778
+ for (const prefix of IPV6_SPECIAL_PREFIXES) {
41779
+ if (!ipv6MatchesPrefix(groups, prefix.groups, prefix.bits))
41780
+ continue;
41781
+ if (prefix.bits === 96 && groups[5] === 65535) {
41782
+ return isPrivateAddress(ipv4IntToString(groups[6] << 16 | groups[7]));
41783
+ }
41784
+ if (prefix.bits === 16 && groups[0] === 8194) {
41785
+ return isPrivateAddress(ipv4IntToString(groups[1] << 16 | groups[2]));
41786
+ }
41787
+ return true;
41788
+ }
41789
+ return false;
41790
+ }
41791
+ return true;
41792
+ }
41793
+ async function resolveWebhookTarget(url, policy = {}) {
41794
+ const hostname = normalizeHostname(url.hostname);
41795
+ const allowlist = (policy.allowPrivateHosts ?? []).map((entry) => normalizeHostname(entry.toLowerCase()));
41796
+ if (allowlist.includes(hostname)) {
41797
+ const version22 = isIP2(hostname);
41798
+ if (version22 === 4 || version22 === 6) {
41799
+ return { hostname, addresses: [hostname] };
41800
+ }
41801
+ const lookup2 = policy.lookup ?? defaultTargetLookup;
41802
+ let resolved2;
41803
+ try {
41804
+ resolved2 = await lookup2(hostname);
41805
+ } catch {
41806
+ throw new Error(`Webhook target ${hostname} could not be resolved`);
41807
+ }
41808
+ if (!Array.isArray(resolved2) || resolved2.length === 0) {
41809
+ throw new Error(`Webhook target ${hostname} resolved to no addresses`);
41810
+ }
41811
+ const addresses = resolved2.map((entry) => normalizeHostname(entry.address));
41812
+ return { hostname, addresses };
41813
+ }
41814
+ const version2 = isIP2(hostname);
41815
+ if (version2 === 4 || version2 === 6) {
41816
+ if (isPrivateAddress(hostname)) {
41817
+ throw new Error(`Webhook target ${hostname} is a private or special-use address`);
41818
+ }
41819
+ return { hostname, addresses: [hostname] };
41820
+ }
41821
+ const lookup = policy.lookup ?? defaultTargetLookup;
41822
+ let resolved;
41823
+ try {
41824
+ resolved = await lookup(hostname);
41825
+ } catch {
41826
+ throw new Error(`Webhook target ${hostname} could not be resolved`);
41827
+ }
41828
+ if (!Array.isArray(resolved) || resolved.length === 0) {
41829
+ throw new Error(`Webhook target ${hostname} resolved to no addresses`);
41830
+ }
41831
+ const allowed = [];
41832
+ for (const entry of resolved) {
41833
+ const address = normalizeHostname(entry.address);
41834
+ if (isPrivateAddress(address)) {
41835
+ if (allowlist.includes(address)) {
41836
+ allowed.push(address);
41837
+ continue;
41838
+ }
41839
+ throw new Error(`Webhook target ${hostname} resolves to private or special-use address ${address}`);
41840
+ }
41841
+ allowed.push(address);
41842
+ }
41843
+ if (allowed.length === 0) {
41844
+ throw new Error(`Webhook target ${hostname} resolved to no public addresses`);
41845
+ }
41846
+ return { hostname, addresses: allowed };
41847
+ }
41848
+ function normalizeMaxRedirects(value) {
41849
+ if (value === undefined)
41850
+ return DEFAULT_MAX_REDIRECTS;
41851
+ if (!Number.isInteger(value) || value < 0)
41852
+ throw new Error("webhookTargetPolicy.maxRedirects must be a non-negative integer");
41853
+ return value;
41854
+ }
41855
+ function normalizeHostname(hostname) {
41856
+ const lower = hostname.toLowerCase();
41857
+ if (lower.startsWith("[") && lower.endsWith("]"))
41858
+ return lower.slice(1, -1);
41859
+ return lower;
41860
+ }
41861
+ function stripZoneId(address) {
41862
+ const percent = address.indexOf("%");
41863
+ return percent === -1 ? address : address.slice(0, percent);
41864
+ }
41865
+ function ipv4ToInt(address) {
41866
+ const parts = address.split(".");
41867
+ if (parts.length !== 4)
41868
+ return;
41869
+ let value = 0;
41870
+ for (const part of parts) {
41871
+ if (!/^\d{1,3}$/.test(part))
41872
+ return;
41873
+ const octet = Number(part);
41874
+ if (octet > 255)
41875
+ return;
41876
+ value = value << 8 | octet;
41877
+ }
41878
+ return value >>> 0;
41879
+ }
41880
+ function ipv4IntToString(integer) {
41881
+ return [
41882
+ integer >>> 24 & 255,
41883
+ integer >>> 16 & 255,
41884
+ integer >>> 8 & 255,
41885
+ integer & 255
41886
+ ].join(".");
41887
+ }
41888
+ function ipv6Groups(address) {
41889
+ const raw = stripZoneId(address);
41890
+ const doubleColon = raw.indexOf("::");
41891
+ const headText = doubleColon === -1 ? raw : raw.slice(0, doubleColon);
41892
+ const tailText = doubleColon === -1 ? "" : raw.slice(doubleColon + 2);
41893
+ const parseGroups = (text) => {
41894
+ if (text === "")
41895
+ return [];
41896
+ const out = [];
41897
+ for (const part of text.split(":")) {
41898
+ if (part.includes(".")) {
41899
+ const v4 = ipv4ToInt(part);
41900
+ if (v4 === undefined)
41901
+ return;
41902
+ out.push(v4 >>> 16 & 65535, v4 & 65535);
41903
+ } else {
41904
+ if (!/^[0-9a-fA-F]{1,4}$/.test(part))
41905
+ return;
41906
+ out.push(parseInt(part, 16));
41907
+ }
41908
+ }
41909
+ return out;
41910
+ };
41911
+ const head = parseGroups(headText);
41912
+ if (!head)
41913
+ return;
41914
+ const tail = parseGroups(tailText);
41915
+ if (!tail)
41916
+ return;
41917
+ const total = head.length + tail.length;
41918
+ if (doubleColon === -1) {
41919
+ return total === 8 ? head : undefined;
41920
+ }
41921
+ if (total >= 8)
41922
+ return;
41923
+ return [...head, ...new Array(8 - total).fill(0), ...tail];
41924
+ }
41925
+ function ipv6MatchesPrefix(groups, prefixGroups, prefixBits) {
41926
+ let remaining = prefixBits;
41927
+ for (let index = 0;index < prefixGroups.length && remaining > 0; index += 1) {
41928
+ const take = Math.min(16, remaining);
41929
+ const mask = 65535 << 16 - take & 65535;
41930
+ if ((groups[index] & mask) !== (prefixGroups[index] & mask))
41931
+ return false;
41932
+ remaining -= take;
41933
+ }
41934
+ return true;
41935
+ }
41761
41936
  function now() {
41762
41937
  return new Date().toISOString();
41763
41938
  }
@@ -41788,9 +41963,18 @@ function buildWebhookRequest(event, channel, options = {}) {
41788
41963
  }
41789
41964
  return { body, headers };
41790
41965
  }
41966
+ function normalizeWebhookUrl(raw) {
41967
+ const url = new URL(raw);
41968
+ if (url.username !== "" || url.password !== "") {
41969
+ url.username = "";
41970
+ url.password = "";
41971
+ }
41972
+ return url.toString();
41973
+ }
41791
41974
  async function dispatchWebhook(event, channel, options = {}) {
41792
41975
  if (!channel.webhook)
41793
41976
  throw new Error(`Channel ${channel.id} has no webhook config`);
41977
+ const webhookUrl = normalizeWebhookUrl(channel.webhook.url);
41794
41978
  const startedAt = now();
41795
41979
  let secret = channel.webhook.secret;
41796
41980
  if (channel.webhook.secretRef) {
@@ -41807,10 +41991,14 @@ async function dispatchWebhook(event, channel, options = {}) {
41807
41991
  }
41808
41992
  const timestamp = (options.now?.() ?? new Date).toISOString();
41809
41993
  const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp });
41994
+ const validateTargets = options.webhookTargetPolicy !== undefined || options.fetchImpl === undefined;
41995
+ if (validateTargets) {
41996
+ return dispatchValidatedWebhook(event, channel, { body, headers, startedAt, options });
41997
+ }
41810
41998
  const controller = new AbortController;
41811
41999
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
41812
42000
  try {
41813
- const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
42001
+ const response = await (options.fetchImpl ?? fetch)(webhookUrl, {
41814
42002
  method: "POST",
41815
42003
  headers,
41816
42004
  body,
@@ -41838,6 +42026,130 @@ async function dispatchWebhook(event, channel, options = {}) {
41838
42026
  clearTimeout(timeout);
41839
42027
  }
41840
42028
  }
42029
+ function isRedirectStatus(status) {
42030
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
42031
+ }
42032
+ function redirectKeepsBody(status) {
42033
+ return status === 307 || status === 308;
42034
+ }
42035
+ async function pinnedNativeRequest(target, addresses, method, headers, body, signal, tls2) {
42036
+ const isHttps = target.protocol === "https:";
42037
+ if (!isHttps && target.protocol !== "http:") {
42038
+ throw new Error(`Webhook target uses unsupported protocol ${target.protocol}`);
42039
+ }
42040
+ const defaultPort = isHttps ? 443 : 80;
42041
+ const port = target.port ? Number(target.port) : defaultPort;
42042
+ const requestOptions = {
42043
+ hostname: target.hostname,
42044
+ port,
42045
+ path: `${target.pathname}${target.search}`,
42046
+ method,
42047
+ headers,
42048
+ ...tls2?.ca ? { ca: tls2.ca } : {},
42049
+ lookup: (hostname, _options, callback) => {
42050
+ const entries = addresses.map((address) => ({
42051
+ address,
42052
+ family: address.includes(":") ? 6 : 4
42053
+ }));
42054
+ callback(null, entries);
42055
+ }
42056
+ };
42057
+ return new Promise((resolve3, reject) => {
42058
+ const request = isHttps ? nodeHttpsRequest(requestOptions, onResponse) : nodeHttpRequest(requestOptions, onResponse);
42059
+ const onAbort = () => {
42060
+ const error = new Error("The operation was aborted.");
42061
+ error.name = "AbortError";
42062
+ request.destroy(error);
42063
+ };
42064
+ if (signal.aborted)
42065
+ onAbort();
42066
+ else
42067
+ signal.addEventListener("abort", onAbort, { once: true });
42068
+ request.on("error", reject);
42069
+ if (body !== undefined)
42070
+ request.write(body);
42071
+ request.end();
42072
+ function onResponse(response) {
42073
+ const chunks = [];
42074
+ response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
42075
+ response.on("error", reject);
42076
+ response.on("end", () => {
42077
+ const headersRecord = {};
42078
+ for (const [name, value] of Object.entries(response.headers)) {
42079
+ if (typeof value === "string")
42080
+ headersRecord[name] = value;
42081
+ else if (Array.isArray(value))
42082
+ headersRecord[name] = value.join(", ");
42083
+ }
42084
+ resolve3(new Response(Buffer.concat(chunks), { status: response.statusCode ?? 200, headers: headersRecord }));
42085
+ });
42086
+ }
42087
+ });
42088
+ }
42089
+ async function dispatchValidatedWebhook(event, channel, input) {
42090
+ const { body, headers, startedAt, options } = input;
42091
+ const webhook = channel.webhook;
42092
+ if (!webhook)
42093
+ throw new Error(`Channel ${channel.id} has no webhook config`);
42094
+ const policy = options.webhookTargetPolicy ?? {};
42095
+ const maxRedirects = normalizeMaxRedirects(policy.maxRedirects);
42096
+ const controller = new AbortController;
42097
+ const timeout = setTimeout(() => controller.abort(), webhook.timeoutMs ?? 15000);
42098
+ try {
42099
+ let target = new URL(normalizeWebhookUrl(webhook.url));
42100
+ let requestHeaders = headers;
42101
+ let method = "POST";
42102
+ let requestBody = body;
42103
+ let redirectsFollowed = 0;
42104
+ for (;; ) {
42105
+ const resolved = await resolveWebhookTarget(target, policy).catch((error) => {
42106
+ throw new Error(`Webhook target rejected by SSRF guard: ${error.message}`);
42107
+ });
42108
+ const response = options.fetchImpl ? await options.fetchImpl(target, {
42109
+ method,
42110
+ headers: requestHeaders,
42111
+ body: requestBody,
42112
+ signal: controller.signal,
42113
+ redirect: "manual"
42114
+ }) : await pinnedNativeRequest(target, resolved.addresses, method, requestHeaders, requestBody, controller.signal, options.tls);
42115
+ const location = response.headers.get("location");
42116
+ if (isRedirectStatus(response.status) && location) {
42117
+ if (redirectsFollowed >= maxRedirects) {
42118
+ return failedAttempt(startedAt, `Webhook target exceeded ${maxRedirects} redirects`);
42119
+ }
42120
+ redirectsFollowed += 1;
42121
+ const next = new URL(location, target);
42122
+ target = next;
42123
+ if (!redirectKeepsBody(response.status)) {
42124
+ method = "GET";
42125
+ requestBody = undefined;
42126
+ requestHeaders = Object.fromEntries(Object.entries(requestHeaders).filter(([name]) => name.toLowerCase() !== "content-type" && name.toLowerCase() !== "content-length"));
42127
+ }
42128
+ continue;
42129
+ }
42130
+ const responseBody = truncate(await response.text());
42131
+ return {
42132
+ attempt: 1,
42133
+ status: response.ok ? "success" : "failed",
42134
+ startedAt,
42135
+ completedAt: now(),
42136
+ responseStatus: response.status,
42137
+ responseBody,
42138
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
42139
+ };
42140
+ }
42141
+ } catch (error) {
42142
+ return {
42143
+ attempt: 1,
42144
+ status: "failed",
42145
+ startedAt,
42146
+ completedAt: now(),
42147
+ error: error instanceof Error ? error.message : String(error)
42148
+ };
42149
+ } finally {
42150
+ clearTimeout(timeout);
42151
+ }
42152
+ }
41841
42153
  function failedAttempt(startedAt, error) {
41842
42154
  return {
41843
42155
  attempt: 1,
@@ -42033,7 +42345,9 @@ class EventsClient {
42033
42345
  this.transportOptions = {
42034
42346
  fetchImpl: options.fetchImpl,
42035
42347
  secretResolver: options.secretResolver,
42036
- now: options.now
42348
+ now: options.now,
42349
+ tls: options.tls,
42350
+ webhookTargetPolicy: options.webhookTargetPolicy
42037
42351
  };
42038
42352
  this.catalog = options.catalog ?? defaultEventTypeCatalog;
42039
42353
  this.validateCatalogTypes = options.validateCatalogTypes ?? false;
@@ -42513,9 +42827,44 @@ function replaySummary(events, deliveries, nextCursor) {
42513
42827
  const suffix = nextCursor ? `, next cursor: ${nextCursor}` : "";
42514
42828
  return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
42515
42829
  }
42516
- var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:", DEFAULT_EVENT_PAGE_LIMIT = 100, MAX_EVENT_PAGE_LIMIT = 1000, DEFAULT_SIGNATURE_TOLERANCE_MS, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES, DEFAULT_EVENT_LIST_LIMIT = 100;
42830
+ var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:", DEFAULT_EVENT_PAGE_LIMIT = 100, MAX_EVENT_PAGE_LIMIT = 1000, DEFAULT_SIGNATURE_TOLERANCE_MS, DEFAULT_MAX_REDIRECTS = 5, IPV4_PRIVATE_RANGES, IPV6_SPECIAL_PREFIXES, defaultTargetLookup = async (hostname) => {
42831
+ return dnsLookup(hostname, { all: true, verbatim: false });
42832
+ }, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES, DEFAULT_EVENT_LIST_LIMIT = 100;
42517
42833
  var init_commander = __esm(() => {
42518
42834
  DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
42835
+ IPV4_PRIVATE_RANGES = [
42836
+ [0, 16777215],
42837
+ [167772160, 184549375],
42838
+ [1681915904, 1686110207],
42839
+ [2130706432, 2147483647],
42840
+ [2851995648, 2852061183],
42841
+ [2886729728, 2887778303],
42842
+ [3221225472, 3221225727],
42843
+ [3221225984, 3221226239],
42844
+ [3227017984, 3227018239],
42845
+ [3232235520, 3232301055],
42846
+ [3323068416, 3323199487],
42847
+ [3325256704, 3325256959],
42848
+ [3405803776, 3405804031],
42849
+ [3758096384, 4294967295]
42850
+ ];
42851
+ IPV6_SPECIAL_PREFIXES = [
42852
+ { groups: [0, 0, 0, 0, 0, 0, 0, 0], bits: 128 },
42853
+ { groups: [0, 0, 0, 0, 0, 0, 0, 1], bits: 128 },
42854
+ { groups: [0, 0, 0, 0, 0, 65535, 0, 0], bits: 96 },
42855
+ { groups: [100, 65435, 0, 0, 0, 0, 0, 0], bits: 96 },
42856
+ { groups: [256, 0, 0, 0, 0, 0, 0, 0], bits: 64 },
42857
+ { groups: [8193, 0, 0, 0, 0, 0, 0, 0], bits: 32 },
42858
+ { groups: [8193, 2, 0, 0, 0, 0, 0, 0], bits: 48 },
42859
+ { groups: [8193, 16, 0, 0, 0, 0, 0, 0], bits: 28 },
42860
+ { groups: [8193, 3512, 0, 0, 0, 0, 0, 0], bits: 32 },
42861
+ { groups: [8194, 0, 0, 0, 0, 0, 0, 0], bits: 16 },
42862
+ { groups: [16383, 0, 0, 0, 0, 0, 0, 0], bits: 20 },
42863
+ { groups: [64512, 0, 0, 0, 0, 0, 0, 0], bits: 7 },
42864
+ { groups: [65152, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
42865
+ { groups: [65216, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
42866
+ { groups: [65280, 0, 0, 0, 0, 0, 0, 0], bits: 8 }
42867
+ ];
42519
42868
  EventValidationError = class EventValidationError extends Error {
42520
42869
  eventType;
42521
42870
  issues;
package/dist/mcp/index.js CHANGED
@@ -41082,8 +41082,28 @@ function buildServer() {
41082
41082
  });
41083
41083
  return server;
41084
41084
  }
41085
+ function printHelp() {
41086
+ console.log(`Usage: domains-mcp [options]
41087
+
41088
+ MCP server for @hasna/domains (Streamable HTTP by default, stdio with --stdio)
41089
+
41090
+ Options:
41091
+ --stdio Serve MCP over stdio (env: MCP_STDIO=1)
41092
+ --http Serve MCP over Streamable HTTP (127.0.0.1; env: MCP_HTTP=1)
41093
+ --port <number> HTTP port (default: 8859, env: MCP_HTTP_PORT)
41094
+ -V, --version output the version number
41095
+ -h, --help display help for command`);
41096
+ }
41085
41097
  async function main() {
41086
41098
  const argv = process.argv.slice(2);
41099
+ if (argv.includes("--help") || argv.includes("-h")) {
41100
+ printHelp();
41101
+ process.exit(0);
41102
+ }
41103
+ if (argv.includes("--version") || argv.includes("-V")) {
41104
+ console.log(getPackageVersion());
41105
+ process.exit(0);
41106
+ }
41087
41107
  const { isStdioMode: isStdioMode3 } = await Promise.resolve().then(() => (init_http(), exports_http));
41088
41108
  if (isStdioMode3(argv)) {
41089
41109
  const transport = new StdioServerTransport2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/domains",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "description": "Domain portfolio, registrar, marketplace, and DNS management for AI agents — CLI + MCP + HTTP API + SDK, local SQLite or cloud Postgres",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -97,7 +97,7 @@
97
97
  "@aws-sdk/client-route-53": "^3.1067.0",
98
98
  "@aws-sdk/client-route-53-domains": "^3.1067.0",
99
99
  "@aws-sdk/credential-provider-ini": "3.972.53",
100
- "@hasna/contracts": "0.13.4",
100
+ "@hasna/contracts": "0.14.0",
101
101
  "@modelcontextprotocol/sdk": "^1.29.0",
102
102
  "chalk": "^5.4.1",
103
103
  "commander": "^13.1.0",