pmdash-connectors
v0.1.0
Published
The PMDash connector manifest format and validator — the minimum-requirements gate for open, installable integrations.
Maintainers
Readme
pmdash-connectors
The connector format for PMDash, and the validator that gates it.
A connector teaches PMDash how to turn data from some tool into dashboard metrics. Anyone can author one. A connector can only be installed once it passes the minimum requirements in this package, which is what makes it safe to install a connector written by someone else, or by an AI agent.
The core idea
A connector is a declarative manifest (JSON). It is pure data: it describes where a metric's value comes from and how to read it. It contains:
- no executable code, and
- no secret values.
Those two rules are the whole safety model. A manifest can be read, diffed, and reasoned about by a human or an agent, and installing one can never run arbitrary code or leak a credential.
Two directions
| Direction | Who moves the data | Credential |
|-----------|--------------------|------------|
| push | Your tool (or a Zapier/Make zap, a cron job, pmdash push) posts to a PMDash webhook | None stored by PMDash |
| pull | PMDash polls the tool's API on a schedule | Entered once in the PMDash UI, encrypted at rest |
A credential is never written into a manifest. A pull manifest says where the credential goes (which header or query param) using the placeholder {{credential}}; PMDash injects the real value at run time.
Manifest shape
{
"manifestVersion": "1",
"id": "acme-crm",
"name": "Acme CRM",
"description": "What this connector does.",
"maintainer": "[email protected]",
"version": "0.1.0",
"homepage": "https://developer.acme.com",
"category": "leads",
"direction": "pull",
"auth": { "type": "api_key", "in": "header", "name": "Authorization", "scheme": "Bearer" },
"source": {
"baseUrl": "https://api.acme.com",
"request": { "method": "GET", "path": "/v1/summary", "headers": { "Authorization": "Bearer {{credential}}" } },
"cadence": "daily"
},
"metrics": [
{ "name": "New Leads (30d)", "metricType": "trend", "aggregation": "sum", "valuePath": "$.leads.last30Days", "displayFormat": "number" }
]
}push connectors omit auth and source; their valuePath reads from the webhook payload instead of an API response. See examples/.
Fields
metricType:trendorcountdownaggregation(optional):sumcountaveragepercentage. Omit it when the field already holds the final number (push metrics, countdown metrics, or a pre-computed pull field).displayFormat:numbercurrencypercentagevaluePath: a JSONPath (starts with$) into the API response (pull) or the posted payload (push)benchmark(optional):{ type: count|days|percentage|rating, value, warning, critical }cadence(pull):every_runhourlydaily
Minimum requirements (the install gate)
validateManifest(manifest) returns { valid, errors, warnings }. A connector is installable only when valid is true. It enforces:
- Declarative only. No
code/script/eval/function/exec/… keys anywhere. - No secret literals. Bearer tokens,
sk_…/pmd_…-style keys, AWS/GitHub tokens, etc. are rejected. Use{{credential}}. - Only
{{credential}}. No other template placeholder is allowed. - HTTPS + no internal targets. A pull
baseUrl(and any URL in the request) must behttps://and must not point atlocalhost, loopback, private ranges, or cloud-metadata (169.254.169.254). This blocks server-side request forgery. - Well-formed. Valid slug
id, semverversion, amaintainer, a knowndirection, at least one metric, and a valid vocabulary for every metric field. - Direction integrity.
pushmust not declare asource;pullmust declaresource+auth+cadence.
The gate is static. At install time PMDash also runs a live dry-run against your credential to confirm the connector actually returns the metrics it declares. That step lives in the app, not this package.
Executing a connector
The same core powers the install-time dry-run and the scheduled runner:
import { executeConnector, extractMetrics } from 'pmdash-connectors';
// pull: fetch the source once with the user's credential and read the metrics
const result = await executeConnector(manifest, credential, { fetchImpl, maxBytes, timeoutMs });
// { ok: true, metrics: [{ name, value, found, ... }] } or { ok: false, code, error }
// push: given a sample/real payload, show what would be extracted
const rows = extractMetrics(manifest, payload);executeConnector re-applies the SSRF gate on the resolved URL, refuses redirects, injects the credential only at request time, times out the whole request (headers and body), and rejects an over-budget response by byte length after reading it. It never returns or logs the credential (error messages are scrubbed of it). A true streaming size cap that stops the download mid-flight is the runner's job (below). readPath resolves a metric's valuePath over own properties only (no prototype traversal). The remaining defenses below still belong to the deployment that calls it.
Required runtime-runner controls
The static gate cannot cover threats that only exist at fetch time. The engine that executes a pull connector must also:
- Re-resolve DNS and block private IPs on the resolved address, and pin that IP for the actual request (a hostname like
127.0.0.1.nip.iopasses the static gate but resolves to loopback; guards against DNS rebinding). - Not follow redirects blindly — re-run the host check on every hop, or disable redirects (a clean host can 302 to an internal one).
- Cap response size and wall-clock time on every poll.
- Evaluate
valuePathwith a non-evalJSONPath reader (novm/script expressions). - Parse/merge the manifest with a null-prototype object so a hostile key can't pollute prototypes.
- Never log the full request URL when
auth.inisquery(the credential rides in the query string) — prefer header auth. - Inject the credential only at request time, never by templating it into the stored manifest.
Usage
import { validateManifest } from 'pmdash-connectors';
const result = validateManifest(manifest);
if (!result.valid) {
for (const e of result.errors) console.error(`${e.path}: ${e.message} (${e.code})`);
}License
MIT
