ToolDescriptor tools.tool@1

Generated at from openvibe-contracts v0.114.0 and openvibe-sdk v0.35.1.

Version
1.1.0
Owner
tools
Visibility
public
Status
active
Compatibility
backward
Decision
ADR-027
Schema
https://openvibe.network/contracts/tools/tool.v1.json

One tool on openvibe.tools as its registry describes it (GET /api/v1/tools/:id, and each item of GET /api/v1/tools; capability tools.tool.read; ADR-027). There is one descriptor per catalogue tool (GET /api/catalog.json tools[].id), built from the tool's own code, so its page, the run API, openvibe-sdk/tools, the OpenAPI document and the docs all read the same facts. EXECUTION is where the tool's engine runs for its page: client (in the browser; nothing leaves it), sync (a server request answered inline) or job (an asynchronous job on a satellite, tools.job@1). API says whether POST /api/v1/tools/{id}/run exposes the tool. A client tool has api true only when it also has a server engine (a pure transform that runs in Node), and its page stays browser-only. A page-only tool (yt) has api false and run null. A job tool's run creates a job of run.job.type whose input is { ...input, ...run.job.preset, tool: run.job.operation } (the preset and the operation always win). INPUT is the JSON Schema (2020-12) that a run request's input must match, embedded or as { $ref: https://openvibe.tools/api/v1/tools/{id}/schema#/$defs/input }. GET /api/v1/tools/:id/schema answers { $schema, $id, $defs: { input, output } }, and the list uses the $ref form. When api is false, $defs.input is false (the schema that accepts nothing), since there is no run to take an input. OUTPUT.SCHEMA describes result.data for every execution: inline and job tools alike. KEYWORDS (the catalogue's search terms) and EXAMPLES (sample runs for the docs and the OpenAPI document) are optional. NOT A TOOL: there is no planned status. A planned catalogue entry has no descriptor and is not listed, and neither is a mirror (a second build of another tool). GET /api/v1/tools/:id and /schema answer 404 tools.tool.not_found for either, with a detail that says the id is planned or names the tool it mirrors, as for an id that does not exist. RULES this schema enforces: api false has no run and no examples, and api true has a run and an input; an API job tool names its job, and only job tools name one; an API tool whose output is a file runs as a job (its results are served as job files); a tools.net.probe tool fetches (egress) and is never anonymous; an anonymous egress tool has a per-target throttle (limits.perTargetPerMinute); a client tool never fetches; an unavailable tool says why (statusReason); JSON output has a schema. contracts.tools.checkDescriptor(d) also checks what depends on the id (run.path, $ref targets), files.min <= files.max, and each example: its input against an embedded input schema and limits.maxInputBytes, and its files against files (count and accept).

Fields

FieldTypeRequiredDescriptionConstraints
idstringyesThe tool's catalogue id, which is its subdomain label (png, jsminify, dns, pdf2jpg). It never changes.
  • pattern ^[a-z][a-z0-9-]{0,39}$
familystringyesThe catalogue family: net, dev, img, audio, docs, text, media, places, pastes…
  • pattern ^[a-z][a-z0-9-]{1,39}$
namestringyes
  • minLength 1
  • maxLength 80
summarystringyesOne plain sentence: what the tool does.
  • minLength 1
  • maxLength 200
keywordsarray of stringOptional. The catalogue's search terms for the tool (GET /api/catalog.json tools[].keywords): phrases people search for, such as 'heic to png' or 'yaml validator'. GET /api/v1/tools?q= matches every word of the query against the id, name, summary and these.
  • maxItems 50
  • unique items
  • items: pattern ^\S(.*\S)?$
  • items: minLength 1
  • items: maxLength 100
statusenumyesstable: works, and its input and output only grow. beta: works; input or output may still change in a minor. preview: works with known gaps (limits or output not final). unavailable: listed, but runs are refused with 503 tools.tool.unavailable and the page says so (statusReason); a program or upstream the engine needs is missing (qpdf, pdftoppm, heif-dec, ffmpeg…), and the engine's own routes answer 503 tools.unavailable while it is. There is no planned status: a planned catalogue entry is not a tool, so it has no descriptor and is not listed.
  • one of "stable", "beta", "preview", "unavailable"
statusReasonstringWhy the status is what it is, for people (e.g. 'needs qpdf on the host'). Required when unavailable.
  • minLength 1
  • maxLength 300
executionenumyesWhere the engine runs for the tool's page: client (the browser), sync (a server request answered inline), job (an asynchronous job).
  • one of "client", "sync", "job"
apibooleanyesPOST /api/v1/tools/{id}/run exposes the tool. false for a client tool without a server engine and for page-only tools (yt).
runobject | nullyesHow to call it; null when api is false.
inputone ofyesJSON Schema (2020-12) of the run request's input: embedded (an object schema), or { $ref } to $defs/input of GET /api/v1/tools/:id/schema. null only for a tool without an API, whose $defs/input is false (it accepts nothing).
filesobject | nullyesThe files a run takes (multipart parts, or tools.run-request@1 files references); null when it takes none.
examplesarray of objectOptional. Sample runs, which the docs and the generated OpenAPI document publish. Each example's input is valid against the tool's input schema and fits limits.maxInputBytes, and its files fit files (count and accept); contracts.tools.checkDescriptor checks this whenever input is embedded. Only a tool with an API has examples.
  • maxItems 50
  • items: no other fields
examples[].titlestringWhat the example shows, for people.
  • minLength 1
  • maxLength 120
examples[].inputobjectyesA run request's input (tools.run-request@1 input), valid against the tool's input schema.
examples[].filesarray of objectThe files the sample run takes (uploaded parts or files references), in order: what each one is, and where to download a sample.
  • maxItems 100
  • items: no other fields
examples[].files[].namestringA file name for the sample (photo.jpg).
  • minLength 1
  • maxLength 200
examples[].files[].mimestringyesIts media type, one that files.accept allows.
  • pattern ^[a-z]+/[a-z0-9][a-z0-9.+-]*$
examples[].files[].urlstringA sample file to download.
  • pattern ^https://
  • format uri
outputobjectyes
  • no other fields
output.kindenumyesjson: result.data; text: result.text; file or files: result.files (job result files).
  • one of "json", "file", "files", "text"
output.schemaone ofJSON Schema of result.data, whatever the execution: for a job tool too, where it describes the data of the run's result and of its job's result (tools.job@1 result.data), which are the same object. Required when kind is json. Embedded, or { $ref } to $defs/output of GET /api/v1/tools/:id/schema.
output.mimearray of stringMedia types the result files can have (kind file or files).
  • minItems 1
  • unique items
  • items: pattern ^[a-z]+/([a-z0-9][a-z0-9.+-]*|\*)$
limitsobjectyes
  • no other fields
limits.timeoutMsintegeryesA run (or its job) longer than this fails with tools.job.timeout or 504.
  • minimum 1
  • maximum 3600000
limits.maxDurationSecintegerLongest audio or video input.
  • minimum 1
limits.maxPixelsintegerLargest image input (width × height).
  • minimum 1
limits.maxPagesintegerMost PDF pages. A longer document fails with 413 tools.pdf.too_many_pages.
  • minimum 1
limits.maxInputBytesintegerLargest input as JSON (text to transform, lists of hosts…).
  • minimum 1
limits.perTargetPerMinuteintegerEgress tools: most runs per minute against one target host or address, across all callers.
  • minimum 1
authobjectyesCaller tiers: anonymous < session < user < app/service.
  • no other fields
auth.anonymousbooleanyestrue: a caller with no token and no browser session may run it (keyed by IP, IPv6 by /64). false: a browser session (the ov_tools_jobs cookie), a signed-in person or a token is needed.
auth.capabilityenumyesWhat an app or service token must hold. tools.tool.run is public. tools.net.probe (partner) covers network probes: only principals holding it run these through the API; people use their pages, which have a per-target throttle.
  • one of "tools.tool.run", "tools.net.probe"
quotaClassstringyesThe quota bucket a run counts against, with allowances per caller tier. Names describe the work, never a tier or a price (tools-run, tools-job, tools-probe, tools-fetch…), so a paid tier later adds allowances, not names. A caller whose allowance is spent gets 429 tools.quota.exceeded with Retry-After.
  • pattern ^tools-[a-z0-9]+(-[a-z0-9]+)*$
costintegeryesRelative weight of one run within its quota class (1 = a cheap lookup or transform). Quotas count weight, not requests.
  • minimum 1
  • maximum 1000
egressbooleanyesThe server fetches a host or URL the caller chose (DNS, WHOIS, headers, Open Graph, ports…), always through the SSRF guard.
hostsarray of stringyesThe tool's public hostnames, canonical first (png.openvibe.tools, a custom domain…); empty for a tool that lives on another service.
  • maxItems 50
  • unique items
  • items: pattern ^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$
docsstringyesWhere a person reads about the tool.
  • pattern ^https://
  • format uri

Examples

From the contract's own test fixtures: valid ones validate, rejected ones must fail.

Valid: dns-sync-egress
{
  "id": "dns",
  "family": "net",
  "name": "DNS Lookup",
  "summary": "Look up A, AAAA, MX, TXT, NS, CNAME and other records for a domain.",
  "keywords": [
    "dns lookup",
    "mx record lookup",
    "txt record check",
    "nslookup online"
  ],
  "status": "stable",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/dns/run",
    "job": null,
    "legacy": [
      "GET /api/net/dns/:target"
    ]
  },
  "input": {
    "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/input"
  },
  "files": null,
  "examples": [
    {
      "title": "MX records of a domain",
      "input": {
        "target": "example.com",
        "type": "MX"
      }
    }
  ],
  "output": {
    "kind": "json",
    "schema": {
      "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/output"
    }
  },
  "limits": {
    "timeoutMs": 10000,
    "maxInputBytes": 1024,
    "perTargetPerMinute": 30
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-fetch",
  "cost": 1,
  "egress": true,
  "hosts": [
    "dns.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/dns"
}
Valid: jsonminify-client-engine
{
  "id": "jsonminify",
  "family": "dev",
  "name": "JSON Minifier",
  "summary": "Strip the whitespace out of JSON.",
  "keywords": [
    "json minifier",
    "minify json",
    "compact json"
  ],
  "status": "stable",
  "execution": "client",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/jsonminify/run",
    "job": null
  },
  "input": {
    "type": "object",
    "required": [
      "text"
    ],
    "additionalProperties": false,
    "properties": {
      "text": {
        "type": "string",
        "maxLength": 1048576
      }
    }
  },
  "files": null,
  "examples": [
    {
      "title": "Minify a small object",
      "input": {
        "text": "{\n  \"name\": \"OpenVibe\",\n  \"tools\": [1, 2, 3]\n}"
      }
    }
  ],
  "output": {
    "kind": "text"
  },
  "limits": {
    "timeoutMs": 5000,
    "maxInputBytes": 1048576
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-run",
  "cost": 1,
  "egress": false,
  "hosts": [
    "json-minifier.openvibe.tools",
    "jsonminify.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/jsonminify"
}
Valid: logo-client-only
{
  "id": "logo",
  "family": "text",
  "name": "Logo Maker",
  "summary": "Make a text logo in the browser.",
  "status": "stable",
  "execution": "client",
  "api": false,
  "run": null,
  "input": null,
  "files": null,
  "output": {
    "kind": "file",
    "mime": [
      "image/png",
      "image/svg+xml"
    ]
  },
  "limits": {
    "timeoutMs": 1000
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-run",
  "cost": 1,
  "egress": false,
  "hosts": [
    "logo.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/logo"
}
Valid: png-job
{
  "id": "png",
  "family": "img",
  "name": "PNG Converter",
  "summary": "Convert JPG, WebP, AVIF, HEIC, GIF, BMP, TIFF and more to PNG.",
  "keywords": [
    "jpg to png",
    "webp to png",
    "heic to png",
    "convert image to png"
  ],
  "status": "stable",
  "execution": "job",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/png/run",
    "job": {
      "type": "img.process",
      "operation": "convert",
      "preset": {
        "format": "png"
      }
    },
    "legacy": [
      "POST /api/process",
      "POST /api/process/direct"
    ]
  },
  "input": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "compressionLevel": {
        "type": "integer",
        "minimum": 0,
        "maximum": 9
      },
      "width": {
        "type": "integer",
        "minimum": 1,
        "maximum": 16384
      }
    }
  },
  "files": {
    "min": 1,
    "max": 1,
    "accept": [
      "image/*"
    ],
    "maxBytes": 52428800
  },
  "examples": [
    {
      "title": "Convert a JPEG photo to PNG",
      "input": {},
      "files": [
        {
          "name": "photo.jpg",
          "mime": "image/jpeg"
        }
      ]
    },
    {
      "title": "512 px wide, at the highest compression level",
      "input": {
        "width": 512,
        "compressionLevel": 9
      },
      "files": [
        {
          "name": "screenshot.webp",
          "mime": "image/webp"
        }
      ]
    }
  ],
  "output": {
    "kind": "file",
    "mime": [
      "image/png"
    ]
  },
  "limits": {
    "timeoutMs": 120000,
    "maxPixels": 100000000
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-job",
  "cost": 5,
  "egress": false,
  "hosts": [
    "png.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/png"
}
Valid: port-probe
{
  "id": "port",
  "family": "net",
  "name": "Port Checker",
  "summary": "Check whether TCP ports on a host are open.",
  "keywords": [
    "open port checker",
    "port scanner"
  ],
  "status": "beta",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/port/run",
    "job": null,
    "legacy": [
      "GET /api/net/port/:target"
    ]
  },
  "input": {
    "type": "object",
    "required": [
      "host",
      "ports"
    ],
    "additionalProperties": false,
    "properties": {
      "host": {
        "type": "string",
        "maxLength": 253
      },
      "ports": {
        "type": "array",
        "minItems": 1,
        "maxItems": 20,
        "items": {
          "type": "integer",
          "minimum": 1,
          "maximum": 65535
        }
      }
    }
  },
  "files": null,
  "examples": [
    {
      "title": "Is HTTPS open?",
      "input": {
        "host": "example.com",
        "ports": [
          443
        ]
      }
    }
  ],
  "output": {
    "kind": "json",
    "schema": {
      "type": "object",
      "properties": {
        "results": {
          "type": "array"
        }
      }
    }
  },
  "limits": {
    "timeoutMs": 15000,
    "perTargetPerMinute": 6
  },
  "auth": {
    "anonymous": false,
    "capability": "tools.net.probe"
  },
  "quotaClass": "tools-probe",
  "cost": 5,
  "egress": true,
  "hosts": [
    "port.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/port"
}
Valid: protectpdf-unavailable
{
  "id": "protectpdf",
  "family": "docs",
  "name": "Protect PDF",
  "summary": "Put a password on a PDF (AES-256).",
  "status": "unavailable",
  "statusReason": "Needs qpdf on the host; not deployed yet.",
  "execution": "job",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/protectpdf/run",
    "job": {
      "type": "docs.process",
      "operation": "protect"
    }
  },
  "input": {
    "type": "object",
    "required": [
      "password"
    ],
    "properties": {
      "password": {
        "type": "string",
        "minLength": 1,
        "maxLength": 128
      }
    }
  },
  "files": {
    "min": 1,
    "max": 1,
    "accept": [
      "application/pdf"
    ],
    "maxBytes": 104857600
  },
  "output": {
    "kind": "file",
    "mime": [
      "application/pdf"
    ]
  },
  "limits": {
    "timeoutMs": 300000,
    "maxPages": 2000
  },
  "auth": {
    "anonymous": false,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-job",
  "cost": 10,
  "egress": false,
  "hosts": [
    "protectpdf.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/protectpdf"
}
Valid: yt-page-only
{
  "id": "yt",
  "family": "media",
  "name": "YouTube Downloader",
  "summary": "Save a YouTube video or its audio.",
  "status": "stable",
  "execution": "job",
  "api": false,
  "run": null,
  "input": null,
  "files": null,
  "output": {
    "kind": "file",
    "mime": [
      "video/mp4",
      "audio/mpeg"
    ]
  },
  "limits": {
    "timeoutMs": 900000,
    "maxDurationSec": 3600,
    "perTargetPerMinute": 20
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-download",
  "cost": 50,
  "egress": true,
  "hosts": [
    "youtube-downloader.openvibe.tools",
    "yt.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/yt"
}
Rejected: anonymous-egress-without-throttle
{
  "id": "dns",
  "family": "net",
  "name": "DNS Lookup",
  "summary": "Look up A, AAAA, MX, TXT, NS, CNAME and other records for a domain.",
  "status": "stable",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/dns/run",
    "job": null,
    "legacy": [
      "GET /api/net/dns/:target"
    ]
  },
  "input": {
    "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/input"
  },
  "files": null,
  "output": {
    "kind": "json",
    "schema": {
      "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/output"
    }
  },
  "limits": {
    "timeoutMs": 10000,
    "maxInputBytes": 1024
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-fetch",
  "cost": 1,
  "egress": true,
  "hosts": [
    "dns.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/dns"
}
Rejected: api-false-with-run
{
  "id": "png",
  "family": "img",
  "name": "PNG Converter",
  "summary": "Convert JPG, WebP, AVIF, HEIC, GIF, BMP, TIFF and more to PNG.",
  "status": "stable",
  "execution": "job",
  "api": false,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/png/run",
    "job": {
      "type": "img.process",
      "operation": "convert",
      "preset": {
        "format": "png"
      }
    },
    "legacy": [
      "POST /api/process",
      "POST /api/process/direct"
    ]
  },
  "input": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "compressionLevel": {
        "type": "integer",
        "minimum": 0,
        "maximum": 9
      },
      "width": {
        "type": "integer",
        "minimum": 1,
        "maximum": 16384
      }
    }
  },
  "files": {
    "min": 1,
    "max": 1,
    "accept": [
      "image/*"
    ],
    "maxBytes": 52428800
  },
  "output": {
    "kind": "file",
    "mime": [
      "image/png"
    ]
  },
  "limits": {
    "timeoutMs": 120000,
    "maxPixels": 100000000
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-job",
  "cost": 5,
  "egress": false,
  "hosts": [
    "png.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/png"
}
Rejected: api-job-tool-without-job
{
  "id": "png",
  "family": "img",
  "name": "PNG Converter",
  "summary": "Convert JPG, WebP, AVIF, HEIC, GIF, BMP, TIFF and more to PNG.",
  "status": "stable",
  "execution": "job",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/png/run",
    "job": null,
    "legacy": [
      "POST /api/process",
      "POST /api/process/direct"
    ]
  },
  "input": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "compressionLevel": {
        "type": "integer",
        "minimum": 0,
        "maximum": 9
      },
      "width": {
        "type": "integer",
        "minimum": 1,
        "maximum": 16384
      }
    }
  },
  "files": {
    "min": 1,
    "max": 1,
    "accept": [
      "image/*"
    ],
    "maxBytes": 52428800
  },
  "output": {
    "kind": "file",
    "mime": [
      "image/png"
    ]
  },
  "limits": {
    "timeoutMs": 120000,
    "maxPixels": 100000000
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-job",
  "cost": 5,
  "egress": false,
  "hosts": [
    "png.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/png"
}
Rejected: api-true-without-input
{
  "id": "dns",
  "family": "net",
  "name": "DNS Lookup",
  "summary": "Look up A, AAAA, MX, TXT, NS, CNAME and other records for a domain.",
  "status": "stable",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/dns/run",
    "job": null,
    "legacy": [
      "GET /api/net/dns/:target"
    ]
  },
  "input": null,
  "files": null,
  "output": {
    "kind": "json",
    "schema": {
      "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/output"
    }
  },
  "limits": {
    "timeoutMs": 10000,
    "maxInputBytes": 1024,
    "perTargetPerMinute": 30
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-fetch",
  "cost": 1,
  "egress": true,
  "hosts": [
    "dns.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/dns"
}
Rejected: api-true-without-run
{
  "id": "png",
  "family": "img",
  "name": "PNG Converter",
  "summary": "Convert JPG, WebP, AVIF, HEIC, GIF, BMP, TIFF and more to PNG.",
  "status": "stable",
  "execution": "job",
  "api": true,
  "run": null,
  "input": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "compressionLevel": {
        "type": "integer",
        "minimum": 0,
        "maximum": 9
      },
      "width": {
        "type": "integer",
        "minimum": 1,
        "maximum": 16384
      }
    }
  },
  "files": {
    "min": 1,
    "max": 1,
    "accept": [
      "image/*"
    ],
    "maxBytes": 52428800
  },
  "output": {
    "kind": "file",
    "mime": [
      "image/png"
    ]
  },
  "limits": {
    "timeoutMs": 120000,
    "maxPixels": 100000000
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-job",
  "cost": 5,
  "egress": false,
  "hosts": [
    "png.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/png"
}
Rejected: client-tool-fetches
{
  "id": "jsonminify",
  "family": "dev",
  "name": "JSON Minifier",
  "summary": "Strip the whitespace out of JSON.",
  "status": "stable",
  "execution": "client",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/jsonminify/run",
    "job": null
  },
  "input": {
    "type": "object",
    "required": [
      "text"
    ],
    "additionalProperties": false,
    "properties": {
      "text": {
        "type": "string",
        "maxLength": 1048576
      }
    }
  },
  "files": null,
  "output": {
    "kind": "text"
  },
  "limits": {
    "timeoutMs": 5000,
    "maxInputBytes": 1048576,
    "perTargetPerMinute": 10
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-run",
  "cost": 1,
  "egress": true,
  "hosts": [
    "json-minifier.openvibe.tools",
    "jsonminify.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/jsonminify"
}
Rejected: example-file-not-accepted
{
  "id": "png",
  "family": "img",
  "name": "PNG Converter",
  "summary": "Convert JPG, WebP, AVIF, HEIC, GIF, BMP, TIFF and more to PNG.",
  "keywords": [
    "jpg to png",
    "webp to png",
    "heic to png",
    "convert image to png"
  ],
  "status": "stable",
  "execution": "job",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/png/run",
    "job": {
      "type": "img.process",
      "operation": "convert",
      "preset": {
        "format": "png"
      }
    },
    "legacy": [
      "POST /api/process",
      "POST /api/process/direct"
    ]
  },
  "input": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "compressionLevel": {
        "type": "integer",
        "minimum": 0,
        "maximum": 9
      },
      "width": {
        "type": "integer",
        "minimum": 1,
        "maximum": 16384
      }
    }
  },
  "files": {
    "min": 1,
    "max": 1,
    "accept": [
      "image/*"
    ],
    "maxBytes": 52428800
  },
  "examples": [
    {
      "title": "A PDF is not an image",
      "input": {},
      "files": [
        {
          "name": "scan.pdf",
          "mime": "application/pdf"
        }
      ]
    }
  ],
  "output": {
    "kind": "file",
    "mime": [
      "image/png"
    ]
  },
  "limits": {
    "timeoutMs": 120000,
    "maxPixels": 100000000
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-job",
  "cost": 5,
  "egress": false,
  "hosts": [
    "png.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/png"
}
Rejected: example-input-violates-schema
{
  "id": "png",
  "family": "img",
  "name": "PNG Converter",
  "summary": "Convert JPG, WebP, AVIF, HEIC, GIF, BMP, TIFF and more to PNG.",
  "keywords": [
    "jpg to png",
    "webp to png",
    "heic to png",
    "convert image to png"
  ],
  "status": "stable",
  "execution": "job",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/png/run",
    "job": {
      "type": "img.process",
      "operation": "convert",
      "preset": {
        "format": "png"
      }
    },
    "legacy": [
      "POST /api/process",
      "POST /api/process/direct"
    ]
  },
  "input": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "compressionLevel": {
        "type": "integer",
        "minimum": 0,
        "maximum": 9
      },
      "width": {
        "type": "integer",
        "minimum": 1,
        "maximum": 16384
      }
    }
  },
  "files": {
    "min": 1,
    "max": 1,
    "accept": [
      "image/*"
    ],
    "maxBytes": 52428800
  },
  "examples": [
    {
      "title": "Compression level 12 (the schema allows 0 to 9)",
      "input": {
        "compressionLevel": 12
      },
      "files": [
        {
          "name": "photo.jpg",
          "mime": "image/jpeg"
        }
      ]
    }
  ],
  "output": {
    "kind": "file",
    "mime": [
      "image/png"
    ]
  },
  "limits": {
    "timeoutMs": 120000,
    "maxPixels": 100000000
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-job",
  "cost": 5,
  "egress": false,
  "hosts": [
    "png.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/png"
}
Rejected: examples-on-page-only-tool
{
  "id": "yt",
  "family": "media",
  "name": "YouTube Downloader",
  "summary": "Save a YouTube video or its audio.",
  "status": "stable",
  "execution": "job",
  "api": false,
  "run": null,
  "input": null,
  "files": null,
  "examples": [
    {
      "input": {
        "url": "https://www.youtube.com/watch?v=aqz-KE-bpKQ"
      }
    }
  ],
  "output": {
    "kind": "file",
    "mime": [
      "video/mp4",
      "audio/mpeg"
    ]
  },
  "limits": {
    "timeoutMs": 900000,
    "maxDurationSec": 3600,
    "perTargetPerMinute": 20
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-download",
  "cost": 50,
  "egress": true,
  "hosts": [
    "youtube-downloader.openvibe.tools",
    "yt.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/yt"
}
Rejected: file-output-answered-inline
{
  "id": "png",
  "family": "img",
  "name": "PNG Converter",
  "summary": "Convert JPG, WebP, AVIF, HEIC, GIF, BMP, TIFF and more to PNG.",
  "status": "stable",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/png/run",
    "job": null,
    "legacy": [
      "POST /api/process",
      "POST /api/process/direct"
    ]
  },
  "input": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "compressionLevel": {
        "type": "integer",
        "minimum": 0,
        "maximum": 9
      },
      "width": {
        "type": "integer",
        "minimum": 1,
        "maximum": 16384
      }
    }
  },
  "files": {
    "min": 1,
    "max": 1,
    "accept": [
      "image/*"
    ],
    "maxBytes": 52428800
  },
  "output": {
    "kind": "file",
    "mime": [
      "image/png"
    ]
  },
  "limits": {
    "timeoutMs": 120000,
    "maxPixels": 100000000
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-job",
  "cost": 5,
  "egress": false,
  "hosts": [
    "png.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/png"
}
Rejected: input-schema-not-an-object
{
  "id": "jsonminify",
  "family": "dev",
  "name": "JSON Minifier",
  "summary": "Strip the whitespace out of JSON.",
  "status": "stable",
  "execution": "client",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/jsonminify/run",
    "job": null
  },
  "input": {
    "type": "string"
  },
  "files": null,
  "output": {
    "kind": "text"
  },
  "limits": {
    "timeoutMs": 5000,
    "maxInputBytes": 1048576
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-run",
  "cost": 1,
  "egress": false,
  "hosts": [
    "json-minifier.openvibe.tools",
    "jsonminify.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/jsonminify"
}
Rejected: json-output-without-schema
{
  "id": "dns",
  "family": "net",
  "name": "DNS Lookup",
  "summary": "Look up A, AAAA, MX, TXT, NS, CNAME and other records for a domain.",
  "status": "stable",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/dns/run",
    "job": null,
    "legacy": [
      "GET /api/net/dns/:target"
    ]
  },
  "input": {
    "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/input"
  },
  "files": null,
  "output": {
    "kind": "json"
  },
  "limits": {
    "timeoutMs": 10000,
    "maxInputBytes": 1024,
    "perTargetPerMinute": 30
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-fetch",
  "cost": 1,
  "egress": true,
  "hosts": [
    "dns.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/dns"
}
Rejected: keyword-blank
{
  "id": "png",
  "family": "img",
  "name": "PNG Converter",
  "summary": "Convert JPG, WebP, AVIF, HEIC, GIF, BMP, TIFF and more to PNG.",
  "keywords": [
    "jpg to png",
    " "
  ],
  "status": "stable",
  "execution": "job",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/png/run",
    "job": {
      "type": "img.process",
      "operation": "convert",
      "preset": {
        "format": "png"
      }
    },
    "legacy": [
      "POST /api/process",
      "POST /api/process/direct"
    ]
  },
  "input": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "compressionLevel": {
        "type": "integer",
        "minimum": 0,
        "maximum": 9
      },
      "width": {
        "type": "integer",
        "minimum": 1,
        "maximum": 16384
      }
    }
  },
  "files": {
    "min": 1,
    "max": 1,
    "accept": [
      "image/*"
    ],
    "maxBytes": 52428800
  },
  "examples": [
    {
      "title": "Convert a JPEG photo to PNG",
      "input": {},
      "files": [
        {
          "name": "photo.jpg",
          "mime": "image/jpeg"
        }
      ]
    },
    {
      "title": "Convert and shrink to 512 px wide, smallest file",
      "input": {
        "width": 512,
        "compressionLevel": 9
      },
      "files": [
        {
          "name": "screenshot.webp",
          "mime": "image/webp"
        }
      ]
    }
  ],
  "output": {
    "kind": "file",
    "mime": [
      "image/png"
    ]
  },
  "limits": {
    "timeoutMs": 120000,
    "maxPixels": 100000000
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-job",
  "cost": 5,
  "egress": false,
  "hosts": [
    "png.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/png"
}
Rejected: mime-with-parameters
{
  "id": "png",
  "family": "img",
  "name": "PNG Converter",
  "summary": "Convert JPG, WebP, AVIF, HEIC, GIF, BMP, TIFF and more to PNG.",
  "status": "stable",
  "execution": "job",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/png/run",
    "job": {
      "type": "img.process",
      "operation": "convert",
      "preset": {
        "format": "png"
      }
    },
    "legacy": [
      "POST /api/process",
      "POST /api/process/direct"
    ]
  },
  "input": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "compressionLevel": {
        "type": "integer",
        "minimum": 0,
        "maximum": 9
      },
      "width": {
        "type": "integer",
        "minimum": 1,
        "maximum": 16384
      }
    }
  },
  "files": {
    "min": 1,
    "max": 1,
    "accept": [
      "image/png; charset=binary"
    ],
    "maxBytes": 52428800
  },
  "output": {
    "kind": "file",
    "mime": [
      "image/png"
    ]
  },
  "limits": {
    "timeoutMs": 120000,
    "maxPixels": 100000000
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-job",
  "cost": 5,
  "egress": false,
  "hosts": [
    "png.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/png"
}
Rejected: probe-anonymous
{
  "id": "port",
  "family": "net",
  "name": "Port Checker",
  "summary": "Check whether TCP ports on a host are open.",
  "status": "beta",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/port/run",
    "job": null,
    "legacy": [
      "GET /api/net/port/:target"
    ]
  },
  "input": {
    "type": "object",
    "required": [
      "host",
      "ports"
    ],
    "additionalProperties": false,
    "properties": {
      "host": {
        "type": "string",
        "maxLength": 253
      },
      "ports": {
        "type": "array",
        "minItems": 1,
        "maxItems": 20,
        "items": {
          "type": "integer",
          "minimum": 1,
          "maximum": 65535
        }
      }
    }
  },
  "files": null,
  "output": {
    "kind": "json",
    "schema": {
      "type": "object",
      "properties": {
        "results": {
          "type": "array"
        }
      }
    }
  },
  "limits": {
    "timeoutMs": 15000,
    "perTargetPerMinute": 6
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.net.probe"
  },
  "quotaClass": "tools-probe",
  "cost": 5,
  "egress": true,
  "hosts": [
    "port.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/port"
}
Rejected: probe-without-egress
{
  "id": "port",
  "family": "net",
  "name": "Port Checker",
  "summary": "Check whether TCP ports on a host are open.",
  "status": "beta",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/port/run",
    "job": null,
    "legacy": [
      "GET /api/net/port/:target"
    ]
  },
  "input": {
    "type": "object",
    "required": [
      "host",
      "ports"
    ],
    "additionalProperties": false,
    "properties": {
      "host": {
        "type": "string",
        "maxLength": 253
      },
      "ports": {
        "type": "array",
        "minItems": 1,
        "maxItems": 20,
        "items": {
          "type": "integer",
          "minimum": 1,
          "maximum": 65535
        }
      }
    }
  },
  "files": null,
  "output": {
    "kind": "json",
    "schema": {
      "type": "object",
      "properties": {
        "results": {
          "type": "array"
        }
      }
    }
  },
  "limits": {
    "timeoutMs": 15000,
    "perTargetPerMinute": 6
  },
  "auth": {
    "anonymous": false,
    "capability": "tools.net.probe"
  },
  "quotaClass": "tools-probe",
  "cost": 5,
  "egress": false,
  "hosts": [
    "port.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/port"
}
Rejected: quota-class-without-prefix
{
  "id": "dns",
  "family": "net",
  "name": "DNS Lookup",
  "summary": "Look up A, AAAA, MX, TXT, NS, CNAME and other records for a domain.",
  "status": "stable",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/dns/run",
    "job": null,
    "legacy": [
      "GET /api/net/dns/:target"
    ]
  },
  "input": {
    "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/input"
  },
  "files": null,
  "output": {
    "kind": "json",
    "schema": {
      "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/output"
    }
  },
  "limits": {
    "timeoutMs": 10000,
    "maxInputBytes": 1024,
    "perTargetPerMinute": 30
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "fetch",
  "cost": 1,
  "egress": true,
  "hosts": [
    "dns.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/dns"
}
Rejected: ref-not-a-string
{
  "id": "dns",
  "family": "net",
  "name": "DNS Lookup",
  "summary": "Look up A, AAAA, MX, TXT, NS, CNAME and other records for a domain.",
  "status": "stable",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/dns/run",
    "job": null,
    "legacy": [
      "GET /api/net/dns/:target"
    ]
  },
  "input": {
    "$ref": 42
  },
  "files": null,
  "output": {
    "kind": "json",
    "schema": {
      "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/output"
    }
  },
  "limits": {
    "timeoutMs": 10000,
    "maxInputBytes": 1024,
    "perTargetPerMinute": 30
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-fetch",
  "cost": 1,
  "egress": true,
  "hosts": [
    "dns.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/dns"
}
Rejected: run-get-method
{
  "id": "dns",
  "family": "net",
  "name": "DNS Lookup",
  "summary": "Look up A, AAAA, MX, TXT, NS, CNAME and other records for a domain.",
  "status": "stable",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "GET",
    "path": "/api/v1/tools/dns/run",
    "job": null,
    "legacy": [
      "GET /api/net/dns/:target"
    ]
  },
  "input": {
    "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/input"
  },
  "files": null,
  "output": {
    "kind": "json",
    "schema": {
      "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/output"
    }
  },
  "limits": {
    "timeoutMs": 10000,
    "maxInputBytes": 1024,
    "perTargetPerMinute": 30
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-fetch",
  "cost": 1,
  "egress": true,
  "hosts": [
    "dns.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/dns"
}
Rejected: sync-tool-with-job
{
  "id": "dns",
  "family": "net",
  "name": "DNS Lookup",
  "summary": "Look up A, AAAA, MX, TXT, NS, CNAME and other records for a domain.",
  "status": "stable",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/dns/run",
    "job": {
      "type": "net.lookup",
      "operation": "dns"
    },
    "legacy": [
      "GET /api/net/dns/:target"
    ]
  },
  "input": {
    "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/input"
  },
  "files": null,
  "output": {
    "kind": "json",
    "schema": {
      "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/output"
    }
  },
  "limits": {
    "timeoutMs": 10000,
    "maxInputBytes": 1024,
    "perTargetPerMinute": 30
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-fetch",
  "cost": 1,
  "egress": true,
  "hosts": [
    "dns.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/dns"
}
Rejected: unavailable-without-reason
{
  "id": "protectpdf",
  "family": "docs",
  "name": "Protect PDF",
  "summary": "Put a password on a PDF (AES-256).",
  "status": "unavailable",
  "execution": "job",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/protectpdf/run",
    "job": {
      "type": "docs.process",
      "operation": "protect"
    }
  },
  "input": {
    "type": "object",
    "required": [
      "password"
    ],
    "properties": {
      "password": {
        "type": "string",
        "minLength": 1,
        "maxLength": 128
      }
    }
  },
  "files": {
    "min": 1,
    "max": 1,
    "accept": [
      "application/pdf"
    ],
    "maxBytes": 104857600
  },
  "output": {
    "kind": "file",
    "mime": [
      "application/pdf"
    ]
  },
  "limits": {
    "timeoutMs": 300000,
    "maxPages": 2000
  },
  "auth": {
    "anonymous": false,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-job",
  "cost": 10,
  "egress": false,
  "hosts": [
    "protectpdf.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/protectpdf"
}
Rejected: unknown-capability
{
  "id": "dns",
  "family": "net",
  "name": "DNS Lookup",
  "summary": "Look up A, AAAA, MX, TXT, NS, CNAME and other records for a domain.",
  "status": "stable",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/dns/run",
    "job": null,
    "legacy": [
      "GET /api/net/dns/:target"
    ]
  },
  "input": {
    "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/input"
  },
  "files": null,
  "output": {
    "kind": "json",
    "schema": {
      "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/output"
    }
  },
  "limits": {
    "timeoutMs": 10000,
    "maxInputBytes": 1024,
    "perTargetPerMinute": 30
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.job.create"
  },
  "quotaClass": "tools-fetch",
  "cost": 1,
  "egress": true,
  "hosts": [
    "dns.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/dns"
}
Rejected: unknown-status
{
  "id": "dns",
  "family": "net",
  "name": "DNS Lookup",
  "summary": "Look up A, AAAA, MX, TXT, NS, CNAME and other records for a domain.",
  "status": "deprecated",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/dns/run",
    "job": null,
    "legacy": [
      "GET /api/net/dns/:target"
    ]
  },
  "input": {
    "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/input"
  },
  "files": null,
  "output": {
    "kind": "json",
    "schema": {
      "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/output"
    }
  },
  "limits": {
    "timeoutMs": 10000,
    "maxInputBytes": 1024,
    "perTargetPerMinute": 30
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-fetch",
  "cost": 1,
  "egress": true,
  "hosts": [
    "dns.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/dns"
}
Rejected: uppercase-id
{
  "id": "DNS",
  "family": "net",
  "name": "DNS Lookup",
  "summary": "Look up A, AAAA, MX, TXT, NS, CNAME and other records for a domain.",
  "status": "stable",
  "execution": "sync",
  "api": true,
  "run": {
    "method": "POST",
    "path": "/api/v1/tools/dns/run",
    "job": null,
    "legacy": [
      "GET /api/net/dns/:target"
    ]
  },
  "input": {
    "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/input"
  },
  "files": null,
  "output": {
    "kind": "json",
    "schema": {
      "$ref": "https://openvibe.tools/api/v1/tools/dns/schema#/$defs/output"
    }
  },
  "limits": {
    "timeoutMs": 10000,
    "maxInputBytes": 1024,
    "perTargetPerMinute": 30
  },
  "auth": {
    "anonymous": true,
    "capability": "tools.tool.run"
  },
  "quotaClass": "tools-fetch",
  "cost": 1,
  "egress": true,
  "hosts": [
    "dns.openvibe.tools"
  ],
  "docs": "https://openvibe.tools/tool/dns"
}

Validate

const contracts = require('openvibe-contracts');
contracts.validate('tools.tool@1', value);   // { valid, errors: [{ path, message }] }