Signet — REST API v1 Contract (v0.1)
The public API surface. This document is the contract for the core record/coverage surface; endpoints added since (targets queue, identities leaderboard, subscriptions, AI verifications, dumps, jobs, badges, Atom feeds) are documented authoritatively by the OpenAPI 3.1 spec generated from the live handler mappings and published at/v1/openapi.json. Base URL:https://api.signet.dev. Related: backend.md, architecture.md §5
Conventions
- Reads are anonymous. Record writes authenticate via the **bundle
signature — no account signup. Three narrow token surfaces exist beyond that: operator** endpoints (AI runs, job replay, dump export, grants) take the bootstrap Authorization: Bearer <signet.admin.token> or a PD-8 operator-signed request; subscription delete/test take the per-subscription manage token; target lease closes take the lease token from POST /v1/targets/lease.
- Errors: RFC 9457
application/problem+jsonwith stabletypeURIs
(backend.md §6).
- All timestamps RFC 3339 UTC. All IDs are content-addressed URNs.
- Only submission (write) endpoints are rate-limited — a coarse per-IP
bucket plus a tier-aware per-identity bucket (NFR-4); reads are not metered. A throttled write returns 429 rate-limited with the standard Retry-After header (seconds) and a matching retryAfter body field.
- Every response carries an
X-Request-Idcorrelation id (a valid inbound one
is honoured, otherwise the server mints one) — quote it in bug reports so an operator can find the matching server log line.
- Every response also carries baseline security headers
(X-Content-Type-Options: nosniff, X-Frame-Options: DENY, a locked Content-Security-Policy, Referrer-Policy); no server/framework version is advertised.
- Immutable resources (
/v1/records/{id}, schemas) return
Cache-Control: public, max-age=31536000, immutable.
Records
POST /v1/records
Submit any signed record (audit, verification, flag, vouch). Body: the signed bundle (DSSE envelope; Sigstore verification material may accompany it in the bundle wrapper).
201 Created→{"id": "urn:signet:att:sha256:…", "logIndex": 123456}409 duplicate-record→ body includes the existingid; clients treat as
success (content addressing makes resubmission idempotent).
422 validation-failed,401 signature-invalid,429 rate-limited,
403 read-only-mirror.
Coverage visibility is eventually consistent (seconds); the record itself is readable by ID immediately.
GET /v1/records/{id}
Returns the stored bundle verbatim plus registry annotations:
{
"id": "urn:signet:att:sha256:…",
"bundle": { "payloadType": "…", "payload": "…", "signatures": [ … ] },
"log": { "index": 123456, "inclusionProof": { … }, "integratedTime": "…" },
"registry": {
"recordType": "audit",
"signer": { "id": "sha256:…", "tier": 3, "reputation": 0.82 },
"verifications": [ { "id": "…", "depth": "reviewed", "signer": "…" } ],
"flags": [],
"disputeStatus": "none"
}
}bundle + log are verifiable offline; everything under registry is this mirror's derived view (trust-root-dependent, not signed).
GET /v1/records?repo=…&commit=…&type=audit&cursor=…
List records by subject. Cursor pagination (nextCursor in the response; opaque). Also accepts purl=, signer=, fileSha256=.
Coverage (the skip-scan read path)
GET /v1/coverage?repo=…&commit=…[&path=lib/router/index.js][&minConfidence=0.8][&cwe=CWE-79,CWE-89]
{
"status": "partially-audited",
"computedAt": "2026-07-14T10:02:11Z",
"paramsVersion": "2026.1",
"files": [
{
"path": "lib/router/index.js",
"fileSha256": "e3b0c4…",
"confidence": 0.93,
"status": "fully-audited",
"disputed": false,
"vulnClasses": ["CWE-79", "CWE-89"],
"lines": [{ "start": 1, "end": 150 }],
"attestations": ["urn:signet:att:sha256:…"],
"falsePositives": [
{
"ruleId": "javascript.express.security.audit.express-open-redirect",
"tool": "semgrep",
"verdict": "false-positive",
"category": "sanitized-elsewhere",
"justification": "Redirect target is validated against an allowlist before this sink.",
"fileSha256": "e3b0c4…",
"lines": { "start": 41, "end": 45 }
}
]
}
]
}- Top-level
status: `fully-audited | partially-audited | no-audit-history |
disputed` (disputed wins if any covering attestation has an open flag).
cwe=filters the *coverage requirement*: a file only counts as covered if
every requested class is claimed (per-CWE semantics, trust model §6.2).
minConfidencedrops files below the caller's threshold. The default (no
param) returns everything with confidences — filtering is the caller's risk decision, not the server's.
lines(per file) lists the audited line ranges (FR-2 "line range"). An empty
list means the whole file was in scope (the status reflects that); a non-empty list means coverage is scoped to those ranges, so a consumer can test its own range for overlap before deciding to skip.
falsePositives(per file) is the FR-1 false-positive registry (schema §5.2):
scanner findings the auditor reviewed and dismissed, each with the ruleId, triage category, and justification. A tool suppresses a finding when its rule and the file's fileSha256 match an entry — the hash binding means the suppression dies the moment the bytes change. Index-backed; no bundle parsing.
pathnarrows the query to a single file (FR-2 "specific file path"). It is a
primary-key point lookup, so a per-file CI check does not download the repo's whole covered file set; and because one file is a complete unit (like the by-hash query), the top-level status can be fully-audited for a whole-file audit — which the repo-wide query, lacking the full file inventory, cannot assert. The same path field is accepted in POST /v1/coverage/bulk queries.
POST /v1/coverage/bulk
Body: {"queries": [{"purl": "pkg:github/expressjs/express@e56994…"}, …]} — up to 5,000 entries; same per-item response shape, keyed by input index. Partial results stream as NDJSON when Accept: application/x-ndjson. This endpoint has the high-concurrency rate class (NFR-2).
Mapping confidence (PD-3). A catalog-bridged purl answer carries resolveConfidence — verified (publisher-asserted publish commit: npm gitHead, cargo vcs-info) or attested-tag (our tag peel; down-weight it). The same field rides on each POST /v1/packages/status dependency, so signet deps --json consumers can gate on it. Direct repo purls carry no grade: the mapping is the signed subject itself.
Mutation incidents. When the TTL refresh has recorded a mapping *change* for the queried release (the resolution_mutations incident log, also on GET /v1/disputes and the package dossier), the purl answer carries the latest one:
"mutation": { "confidence": "verified", "oldCommit": "…", "newCommit": "…",
"detectedAt": "…" }verified means the immutable registry artifact itself changed (registry tampering — signet gate fails the build on it); attested-tag means a release tag was repointed (the gate warns). Attached even when the answer is no-audit-history: "nothing to skip-scan" and "this mapping was tampered" are different answers. The same mutation object rides on each POST /v1/packages/status dependency — including uncataloged ones — so signet deps enforces the identical contract at lockfile level.
Monorepo scoping (PD-9). A purl query may carry an explicit purl-spec #subpath fragment (pkg:npm/langchain@0.3.7#libs/langchain) to scope the answer to that subtree; without one, a package the catalog knows to live in a subdirectory is scoped to it automatically — a monorepo purl never answers with a sibling package's coverage. When scoping applied, the response echoes the effective subpath and its files/status cover only that subtree (fragments are normalized; .. traversal is rejected).
A purl query (bulk or coverageForPurl) additionally carries a compact aiAssessment for that exact release — the automated AI pre-screen — so an SBOM or AI-agent consumer gets both evidence types in one call:
"aiAssessment": { "verdict": "clean", "riskLevel": "none", "findingCount": 0,
"model": "qwen2.5-7b-instruct", "summary": "…", "completedAt": "…" }verdict is clean | findings | inconclusive. It is present independently of the human-audit status: a package with no signed audit can still carry an AI verdict (and vice versa), so the two signals compose rather than gate each other. When a verified-grade mutation was detected after the assessment completed, the object carries "stale": true — the verdict examined the pre-swap artifact and must not be read as current (/v1/packages/status mirrors this as aiStale on the dependency). The package badge likewise shows red tampering detected, outranking every other state including vulnerability found.
GET /v1/coverage/by-hash?sha256=…
Cross-repo content lookup: coverage for this exact file content anywhere (vendored copies, forks). Returns matching attestations with their repo contexts (each match also carries the audited lines ranges, as in the repo/commit response), plus a top-level status — the FR-2 skip-scan verdict for this content: fully-audited (a content hash is a complete unit, so an undisputed covering audit means it is fully audited), disputed, or no-audit-history. (A repo/commit query cannot report fully-audited: its full file inventory is unknown, so it reports the conservative partially-audited.)
The response also carries a top-level falsePositives array — every FP documented for this exact content (FR-1 §5.2). Because the suppression key is ruleId + content hash, an FP recorded against these bytes in one repo follows them wherever they are vendored or forked, which is exactly what a by-hash lookup resolves.
AI Security Assessments
The analysis fleet runs a local model (llama.cpp, default qwen2.5-7b-instruct) over a package's source and records an AI-security verdict per version. Source is never sent to a hosted model.
GET /v1/ai-verifications?ecosystem=…&name=…&version=…
The latest assessment plus history for one package version (this is what the portal's package page renders). latest is null when the version was never assessed.
{
"ecosystem": "pypi", "name": "langchain", "version": "0.3.9",
"latest": {
"status": "completed", // completed | failed
"verdict": "findings", // clean | findings | inconclusive | null (on failure)
"riskLevel": "high", // none | low | medium | high
"summary": "Untrusted tool output flows into a code path that can execute.",
"model": "qwen2.5-7b-instruct",
"confidence": 0.79,
"findings": [
{ "path": "libs/langchain/agents/agent.py", "lineStart": 311, "lineEnd": 329,
"vulnClass": "CWE-94", "severity": "high", "title": "Tool output reaches an exec sink" }
],
"methodology": { "parameters": { "model": "qwen2.5-7b-instruct", "temperature": 0 } }
},
"history": [ /* prior runs, newest first — e.g. a findings run later re-scanned clean */ ]
}A run that completed before a verified-grade mapping mutation carries "stale": true (in latest and in history): it read the pre-swap artifact, and the portal's AI panel labels it "stale — artifact changed since this read". A post-mutation re-read (which the sweep queues first) is current again.
POST /v1/ai-verifications/run?ecosystem=…&name=…&version=… (operator)
Triggers one assessment synchronously: resolves the version's (repo, commit), samples a bounded source slice from GitHub, asks the model to review it, and stores the verdict. Returns the recorded run's summary. Spends local-model time and outbound fetches, so it is operator-only: disabled (404) until signet.admin.token is set, 401 on a bad Authorization: Bearer token. A version with no resolved (repo, commit), or whose source can't be fetched, records an inconclusive result rather than failing.
POST /v1/ai-verifications/run-batch?limit=N (operator)
Sweeps up to N not-yet-assessed catalog packages through the same path; returns { "ran": k, "ids": [...] }. Same operator gate. A scheduled internal sweep runs the same logic automatically.
GET /v1/ai-findings?cwe=…&ecosystem=…&page=…
The AI-fleet analogue of /v1/findings: every issue the current AI assessment of a package flagged, across the whole registry, newest assessment first — one query for "what has the model caught?", filterable by CWE class and ecosystem. Only the latest completed run per (ecosystem, name, version) contributes, so a re-assessment supersedes its predecessor rather than duplicating. Each row carries the package, severity, vulnClass, title, and path. These are automated pre-screens (source: "ai-pre-screen"), not signed attestations — the signed feed at /v1/findings remains the authoritative record.
GET /v1/ai-verifications/stats
Fleet-wide summary — the at-a-glance view of the AI subsystem (the portal renders it above the AI pre-screen feed). Public, read-only aggregate; no live model ping.
{
"model": "qwen2.5-7b-instruct",
"enabled": true, // an AI base URL is configured
"assessedPackages": 5, // distinct packages with any run
"assessablePackages": 40, // catalog releases resolved to a commit (the denominator)
"byVerdict": { "clean": 2, "findings": 1, "inconclusive": 1 },
"failed": 1,
"totalRuns": 6, // every run ever (history included)
"tokens": { "prompt": 22900, "completion": 1190 }, // cumulative cost proxy
"lastRunAt": "2026-07-23T21:56:15Z"
}Verdict/status counts are over the latest run per package (a re-assessment supersedes), so they describe the fleet's current state; totalRuns and tokens sum every run for a cumulative cost view. assessedPackages / assessablePackages is the catalog coverage — what fraction of commit-resolved releases (the set the sweep draws from) has a verdict.
Identities & Trust
GET /v1/identities/{id}
Public profile: tier, current R score (plus E/G components and paramsVersion), attestation/verification counts, slash history summary. Score explanations link to reputation_events via GET /v1/identities/{id}/events?cursor=….
GET /v1/trust/params · GET /v1/trust/seeds
The signed trust-parameter config and EigenTrust seed manifest this instance computes with — everything a consumer needs to recompute scores from a dump.
Notifications (webhooks)
Watch a package or repo and get a signed POST when a matching event lands — the pull→push flip. The CLI wrapper is signet watch.
POST /v1/subscriptions
{ "scope": "package", "ecosystem": "pypi", "name": "langchain",
"url": "https://your.example/hook", "secret": "shared-hmac-key",
"events": ["audit", "ai-assessment"] }scope:package(needsecosystem+name) orrepo(needsrepo).events(optional): any of `audit | verification | flag | resolution | vouch |
ai-assessment | mutation.recorded; omit for all. ai-assessment fires when the AI fleet flags findings for the watched package. mutation.recorded fires when the TTL refresh records a mapping change for it — verified confidence means the immutable registry artifact changed (registry tampering), attested-tag` means a release tag was repointed.
secret(optional but recommended): HMAC key for the signature header below.- Rate-limited per IP (creation is an unauthenticated write).
Response: { "id": 42, "scope": "package", "signed": true, "manageToken": "…" }. **Store the manageToken** — it is shown once and is required to test or delete the subscription (ids are sequential, so the token is what proves ownership):
POST /v1/subscriptions/{id}/test?token=…— send a synthetictestevent to
confirm your endpoint before trusting it.
DELETE /v1/subscriptions/{id}?token=…— deactivate it.GET /v1/subscriptions/{id}— status only; the webhook URL is returned redacted.
Delivery
Each event is a POST to your URL with headers:
X-Signet-Event: <event>— the event type.X-Signet-Signature: sha256=<hex>—HMAC-SHA256(secret, rawBody), present
only when a secret was set. Verify it over the exact received bytes before trusting the payload, and reject on mismatch:
import hmac, hashlib
def verify(secret: str, raw_body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header) # constant-timeRecord event body (audit/verification/flag/resolution/vouch):
{ "event": "audit", "recordId": "urn:signet:att:sha256:…", "signer": "sha256:…",
"subscriptionId": 42, "recordUrl": "https://portal.example/records/urn%3A…" }AI-assessment body:
{ "event": "ai-assessment", "subscriptionId": 42, "verificationId": 1337,
"ecosystem": "pypi", "name": "langchain", "version": "0.3.9",
"verdict": "findings", "riskLevel": "high", "findingCount": 2,
"packageUrl": "https://portal.example/packages/pypi/langchain" }Mutation-incident body:
{ "event": "mutation.recorded", "subscriptionId": 42, "mutationId": 7,
"ecosystem": "pypi", "name": "langchain", "version": "0.3.9",
"confidence": "verified", "oldCommit": "…", "newCommit": "…",
"detectedAt": "…", "packageUrl": "https://portal.example/packages/pypi/langchain" }A non-2xx response (or an unreachable endpoint) is retried with backoff and dead-lettered after the max attempts; a target that resolves to a private/internal address is refused (SSRF guard) and never retried.
Registry Metadata
| Endpoint | Purpose |
|---|---|
GET / | root index: name, version, and links to the spec + key read/write endpoints — so a developer or agent that hits the base URL is oriented, not 404'd |
GET /v1/schemas/{name} | published predicate JSON Schemas *(live, M0)* |
GET /v1/incidents.atom | trust incidents (mapping mutations) as an Atom feed — the passive sibling of the mutation.recorded webhook; tampering and repoints as distinct categories, entries link to the affected dossier |
GET /v1/dumps/manifest | latest dump manifest (signed): partitions, hashes, generatedAt |
POST /v1/admin/dumps/export | refresh the signed dump now (operator credentials) — the startup export predates seeding/backfill, and the cron is daily; returns the fresh manifest |
GET /v1/dumps/partitions/{file} | a dump partition, manifest-whitelisted — records.jsonl (signed bundles) and mutations.jsonl (mapping-change incidents, kind: "mutations"; see attestation-schema §7) |
GET /v1/openapi.json | machine-readable spec (M1) |
GET /healthz, GET /readyz | liveness / readiness *(healthz live, M0)* |
Milestone Availability
| Endpoint group | Milestone |
|---|---|
/v1/schemas, /healthz | M0 (live) |
POST/GET records, GET coverage, bulk, dumps manifest | M1 |
| identities, trust params/seeds, by-hash, record listing | M2 |
| NDJSON streaming, OpenAPI publication | M2 |