diff --git a/integrations/ipinfo/manifest.yaml b/integrations/ipinfo/manifest.yaml index e3c2b40..90685f7 100644 --- a/integrations/ipinfo/manifest.yaml +++ b/integrations/ipinfo/manifest.yaml @@ -1,15 +1,15 @@ id: ipinfo name: IPinfo -version: 1.0.1 -description: "IPinfo Core API — IP address enrichment: geolocation, ASN / network details, and privacy/network flags (hosting, mobile, anycast, satellite)." -changelog: "1.0.1 — Re-publish to regenerate form-based command scripts (fixes 'script not found' on install). 1.0.0 — Initial release: IP lookup, single-field lookup, own IP, batch, and connectivity test." +version: 1.1.0 +description: "IPinfo — IP address enrichment. Lite API (all plans, unlimited): ASN / AS name / AS domain / country / continent. Core API (paid Core plan): geolocation, network details and privacy flags (hosting, mobile, anycast, satellite)." +changelog: "1.1.0 — Lite API support for accounts on the free/Lite plan: lite_lookup_ip, lite_lookup_me and lite_batch (POST /batch/lite, chunked at 1000 IPs). test_connection now calls /lite/me, which every plan can reach, so the Test button no longer fails on Lite-only tokens. Core commands renamed in their description to state the Core plan requirement. 1.0.1 — Re-publish to regenerate form-based command scripts (fixes 'script not found' on install). 1.0.0 — Initial release: IP lookup, single-field lookup, own IP, batch, and connectivity test." category: enrichment config_schema: properties: base_url: type: string - description: IPinfo Core API base URL + description: "IPinfo API base URL (shared by the Lite and Core endpoints)" default: https://api.ipinfo.io token: type: string @@ -28,9 +28,50 @@ auth: secret_field: token commands: + # ── Lite API — available on every plan, no daily/monthly quota ───────────── + - id: lite_lookup_ip + name: ipinfo-lite-lookup-ip + description: "Lite enrichment for an IP — ASN, AS name, AS domain, country and continent. Works on every plan, including free/Lite." + risk: read + inputs_schema: + properties: + ip: { type: string, description: "IPv4 or IPv6 address" } + required: [ip] + outputs_schema: { properties: {} } + request: + method: GET + path: /lite/{ip} + auth_ref: apikey + + - id: lite_lookup_me + name: ipinfo-lite-lookup-me + description: "Lite details for the IP address making the request (ASN, country, continent)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + request: + method: GET + path: /lite/me + auth_ref: apikey + + # Code-first: POST /batch/lite takes a raw JSON array body, which the + # form-based generator (object body) cannot produce. See scripts/lite_batch.py. + - id: lite_batch + name: ipinfo-lite-batch + description: "Lite lookup for many IPs in one call. Chunked at the API limit of 1000 IPs per request; returns one object keyed by IP." + risk: read + inputs_schema: + properties: + ips: { type: string, description: "Comma-separated IP addresses, e.g. '8.8.8.8,1.1.1.1'" } + required: [ips] + outputs_schema: { properties: {} } + + # ── Core API — requires a paid Core plan (403 on Lite/free tokens) ───────── - id: lookup_ip name: ipinfo-lookup-ip - description: Full enrichment for an IP — geolocation, ASN/network, and privacy/network flags. + description: "Core plan required. Full enrichment for an IP — geolocation, ASN/network, and privacy/network flags." risk: read inputs_schema: properties: @@ -44,7 +85,7 @@ commands: - id: lookup_field name: ipinfo-lookup-field - description: A single field for an IP (e.g. city, country, hostname, as). + description: "Core plan required. A single field for an IP (e.g. city, country, hostname, as)." risk: read inputs_schema: properties: @@ -59,7 +100,7 @@ commands: - id: lookup_me name: ipinfo-lookup-me - description: Details for the IP address making the request. + description: "Core plan required. Details for the IP address making the request." risk: read inputs_schema: properties: {} @@ -74,7 +115,7 @@ commands: # form-based generator (object body) cannot produce. See scripts/batch.py. - id: batch name: ipinfo-batch - description: Look up multiple IPs (or IP/field paths) in a single request. + description: "Core plan required. Look up multiple IPs (or IP/field paths) in a single request." risk: read inputs_schema: properties: @@ -82,6 +123,8 @@ commands: required: [ips] outputs_schema: { properties: {} } + # Uses the Lite endpoint on purpose: it validates the token on every plan, + # whereas /lookup/me returns 403 for Lite-only accounts. - id: test_connection name: ipinfo-test-connection description: "Verify connectivity and credentials (used by the Test button)." @@ -92,5 +135,5 @@ commands: outputs_schema: { properties: {} } request: method: GET - path: /lookup/me + path: /lite/me auth_ref: apikey diff --git a/integrations/ipinfo/scripts/lite_batch.py b/integrations/ipinfo/scripts/lite_batch.py new file mode 100644 index 0000000..c5aa9d1 --- /dev/null +++ b/integrations/ipinfo/scripts/lite_batch.py @@ -0,0 +1,48 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + +SECRETS = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +INPUTS = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) +BASE = SECRETS.get("base_url", "https://api.ipinfo.io").rstrip("/") + +# IPinfo caps a single Lite batch call at 1000 addresses. +CHUNK = 1000 + + +def post_chunk(url, ips): + # POST /batch/lite expects a raw JSON array of IPs, not an object. + data = json.dumps(ips).encode("utf-8") + headers = {"Content-Type": "application/json", "Accept": "application/json"} + req = urllib.request.Request(url, data=data, headers=headers, method="POST") + with urllib.request.urlopen(req, timeout=30) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + token = SECRETS.get("token", "") + seen = set() + ips = [] + for x in str(INPUTS.get("ips", "")).split(","): + x = x.strip() + if x and x not in seen: + seen.add(x) + ips.append(x) + if not ips: + print(json.dumps({"error": "ips is required"})) + sys.exit(1) + + url = BASE + "/batch/lite?" + urllib.parse.urlencode({"token": token}) + result = {} + for i in range(0, len(ips), CHUNK): + result.update(post_chunk(url, ips[i:i + CHUNK])) + print(json.dumps(result)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1)