Embed the verifier
@promptsign/verify puts the PromptSign verifier inside your Node service — the same audited core the CLI runs, wrapped for JavaScript. Check who signed a skill or agent, and whether a byte changed since, without shelling out to a binary or re-implementing the checks.
Why embed it
For marketplaces, registries, and CI that want to answer is this signed by who it claims, and unchanged? question at ingest time, this package wraps the exact Rust verifier the CLI uses. No need to re-implement signature, certificate-chain, and transparency-log checks.
Verify-only. Signing needs a login and a network round-trip, so it stays in the CLI and the browser Sign tool — this package is the read side.
Install
npm install @promptsign/verifyPrebuilt native binaries are included for common platforms; there is no Rust toolchain to install to use it.
Verify in your app
verify(path) takes a signed directory or file and returns the same VerifyResult the CLI emits with --json. action is the verdict: pass, warn, or fail.
import { verify } from '@promptsign/verify';
// Same result as `promptsign verify --json` — the identical audited Rust core.
const r = verify('./skills/pdf', { noPinUpdates: true });
if (r.action === 'fail') {
reject('signature invalid or contents changed', r.findings);
} else {
console.log(`signed by ${r.identity}`);
}Pass { noPinUpdates: true } from a server. By default, verifying an unseen signer pins that identity to the artifact name (Trust On First Use), so a later swap to a different signer trips a warning. That pin is a durable side effect written under PROMPTSIGN_HOME — useful on a workstation, but in a stateless or concurrent service it means every request tries to mutate shared state and the “first use” is whichever request happened to run first. noPinUpdates keeps verification pure: the same checks run and any existing pins are still enforced, but nothing new is written.
Verify on ingest
The demand-side integration: a directory verifies a submitted bundle before it's listed, and can show “signed by X” honestly. verifyTree(roots) checks several at once; there is also verifyKeyless(bundle) for the certificate-and-log checks alone.
import { verify } from '@promptsign/verify';
// verify-on-ingest: a marketplace checks a submitted bundle before listing it.
export function onSubmit(uploadedDir) {
const r = verify(uploadedDir);
return {
accepted: r.action !== 'fail',
signer: r.identity,
issuer: r.issuer, // present for keyless (Sigstore) signatures
reasons: r.findings,
};
}What you get back
verify and verifyTree return a VerifyResult — the same fields, in the same shape, as promptsign verify --json:
{
"target": "./skills/pdf", // what you asked to verify
"policySource": "project (.promptsign/policy.toml)",
"name": "pdf", // manifest name the signature covers
"version": "1.2.0", // optional — omitted if unset
"kind": "skill", // optional — skill | agent | ...
"identity": "alice@example.com", // signer; null when unsigned
"issuer": "https://github.com/login/oauth", // keyless OIDC provider; absent for local-key
"keyid": "SHA256:…", // signing key id; null when unsigned
"integratedTime": 1783779720, // keyless: Rekor witness time (Unix s); absent for local-key
"signed": true, // a signature was present at all
"action": "pass", // verdict: "pass" | "warn" | "fail"
"findings": [ // reasons behind a warn/fail
{ "level": "info", "message": "pinned \"pdf\" to identity \"alice@example.com\"" }
]
}action- The verdict, and the only field most callers gate on:
pass,warn, orfail. Treatfailas “do not list / do not run.” signed- Whether any signature was present. A
falsehere with afailaction means unsigned-and-required; distinguish it from a present-but-invalid signature. identity- Who signed — the string to render in a “signed by” badge.
nullwhen unsigned. issuer- For keyless (Sigstore) signatures, the OIDC provider that vouched for the identity — e.g. GitHub or Google. Absent for local-key signatures.
keyid- The signing key identifier.
nullwhen unsigned. integratedTime- For keyless signatures, the Rekor log integration time (Unix seconds) — the authenticated moment the signature was witnessed, checked against the certificate's validity window. Absent for local-key signatures; it's what the badge renders as the signing time.
name/version/kind- The manifest metadata the signature actually covers.
versionandkindare omitted when unset. findings- The reasons behind a warn or fail (each a
level+message) — surface these in a tooltip so a rejection is explainable. target/policySource- What was verified, and which policy decided the verdict — useful in logs and audit trails.
Trust policy and Trust On First Use pins follow the CLI's rules and honour PROMPTSIGN_HOME.
A signature proves who and what — never that an artifact is safe. Compose it with your own review and scanning; see the docs for the trust model.
Show the verdict
To render the result, drop in <VerifyBadge> — a small, dependency-free React component you can copy into your app. It takes a VerifyResult and nothing else: the colour and label are derived from the verdict, so there is no “verified” state a caller can assert by hand. An unsigned or tampered artifact cannot render a green badge.
Hover a badge to see its tooltip — the verified signing time on a passing one, and the findings behind a warn or fail. Verify on the server, then hand the plain result to the client to render:
import { verify } from '@promptsign/verify';
import VerifyBadge from './VerifyBadge';
// server: verify once, hand the plain result to the client
const result = verify(listing.path, { noPinUpdates: true });
// client: colour, text, and the signing time in the tooltip all come from the
// result — there is no "verified" state a caller can pass in by hand
<VerifyBadge result={result} />The component is yours to copy — grab both files and style them with the four CSS variables (--green / --amber / --red / --gold), or your own:
You don't need to edit the component's CSS to re-skin it. You can theme the badge by defining those variables above it in the DOM — set them on :root to match your brand everywhere:
/* your app's stylesheet — the badge inherits these */
:root {
--green: #16a34a; /* pass */
--amber: #d97706; /* warn */
--red: #dc2626; /* fail */
--gold: #b45309; /* the signer's name */
}Because custom properties inherit, set them on a wrapper instead to re-theme only the badges inside that section, leaving the rest of the page alone:
/* Because custom properties inherit, set them on a wrapper to
re-theme only the badges inside it — the rest of the page is untouched. */
.marketplace-panel {
--green: #0d9488;
--gold: #0f766e;
}
// <div className="marketplace-panel"><VerifyBadge result={result} /></div>