npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@capseal/sdk

v0.1.0

Published

Verify where a photo, video or document came from. Reads C2PA manifests, EXIF and XMP, and tells you how it knows - never a confidence score it cannot justify.

Readme

CapSeal

Find out where a photo, video or document actually came from.

C2PA manifests, EXIF, XMP — read in one call, with an answer that tells you how it knows.

npm node types

Try it now, no signup · Free API key · Docs


npm i @capseal/sdk
import { CapSeal } from "@capseal/sdk";
import { readFileSync } from "node:fs";

const capseal = new CapSeal({ apiKey: process.env.CAPSEAL_API_KEY });
const { verdict } = await capseal.inspect(readFileSync("photo.jpg"));

console.log(verdict.verdict);  // "AI_GENERATED"
console.log(verdict.basis);    // "declared"
console.log(verdict.headline); // "This file says it was generated by Adobe Firefly."

That's the whole integration. Get a key — free, instant, no card.


Why you need this

You accept files from people. A claims photo, a KYC selfie, an insurance inspection, a marketplace listing, a signed PDF, a piece of user-generated content. Until recently you could assume that a photograph was a photograph.

That assumption is gone. Anyone can generate a convincing image of damage that never happened, in seconds, for free. Meanwhile the countermeasure is arriving: cameras, phones and the major generative tools are all shipping C2PA Content Credentials — a signed record, embedded in the file, of where it came from and what was done to it.

The problem is that reading it correctly is fiddly and most implementations get it wrong. Manifests span multiple JPEG segments. Assertions are CBOR inside JUMBF boxes. The interesting facts are scattered across the manifest, EXIF and XMP, and they contradict each other in informative ways.

This SDK is one call that reads all of it and gives you a straight answer.

What makes the answer trustworthy

Most tools in this space return a percentage. 0.87 likely AI. That number is almost always invented, and it fails in the direction that hurts: a screenshot of a real photograph gets flagged, a generated image that has been re-encoded gets waved through.

CapSeal returns a basis — how the answer was reached:

| basis | what happened | automate on it? | | --- | --- | --- | | proven | cryptography settles it | yes | | declared | the file says so, and a signature backs the file | yes | | claimed | the file says so, nothing backs the claim | no — review | | indicative | metadata leans that way, could be wrong | no — review | | unknown | the file does not say, and neither will we | no — review |

const { verdict } = await capseal.inspect(bytes);

if (verdict.basis === "proven" || verdict.basis === "declared") {
  act(verdict.verdict);         // safe to route automatically
} else {
  review(verdict.headline);     // a person should look
}

Build on basis and your logic stays correct as detection improves, because you never encoded a guess as a fact.

Why there is no "is it AI?" score

An unsigned JPEG with no metadata is a grid of pixels. Nothing in those bytes distinguishes:

  • a genuine photograph that WhatsApp stripped the metadata from
  • a lightly edited photograph
  • a diffusion model's output

Anything claiming otherwise is guessing, and defeating it costs an attacker one screenshot. So for that case you get NO_PROVENANCE / unknown and a sentence explaining why — which is less satisfying and considerably more useful, because you can build on it.

When a file does carry provenance — and an increasing share do — you get a definite answer with the receipts attached.


Quick start

1. Get a key at capseal.ai/account. Free, no card, 1,000 verifications a day.

2. Install and call.

npm i @capseal/sdk
export CAPSEAL_API_KEY=csk_live_...
import { CapSeal } from "@capseal/sdk";

const capseal = new CapSeal({ apiKey: process.env.CAPSEAL_API_KEY });
const result = await capseal.inspect(fileBytes);

3. Or try it first with no signup at all:

curl -X POST --data-binary @photo.jpg https://capseal.ai/api/demo/inspect

Examples

Runnable versions of everything below are in examples/.

Node — the basics

import { CapSeal } from "@capseal/sdk";
import { readFileSync } from "node:fs";

const capseal = new CapSeal({ apiKey: process.env.CAPSEAL_API_KEY });
const { verdict, report } = await capseal.inspect(readFileSync("evidence.jpg"));

console.log(verdict.headline);
console.log(`basis: ${verdict.basis}`);

if (report.c2pa.present) {
  const m = report.c2pa.activeManifest;
  console.log(`signed by  ${m?.claimGenerator}`);
  console.log(`algorithm  ${m?.signatureAlgorithm}`);
  console.log(`actions    ${m?.actions.map((a) => a.action).join(", ")}`);
}

if (report.exif?.make) {
  console.log(`camera     ${report.exif.make} ${report.exif.model}`);
}

Express — gate an upload

import express from "express";
import { CapSeal } from "@capseal/sdk";

const capseal = new CapSeal({ apiKey: process.env.CAPSEAL_API_KEY });
const app = express();

app.post("/claims/:id/evidence",
  express.raw({ type: "*/*", limit: "25mb" }),
  async (req, res) => {
    const { verdict, report } = await capseal.inspect(req.body);

    // Refuse only what is provably wrong. Everything uncertain goes to a human,
    // because rejecting an honest customer costs more than a review does.
    if (verdict.verdict === "AI_GENERATED" && verdict.basis === "declared") {
      return res.status(422).json({
        accepted: false,
        reason: verdict.headline,
        evidence: verdict.supportedBy,
      });
    }

    await store(req.params.id, req.body, {
      sha256: report.file.sha256,
      verdict: verdict.verdict,
      basis: verdict.basis,
      needsReview: verdict.basis !== "declared" && verdict.basis !== "proven",
    });

    res.json({ accepted: true, verdict: verdict.verdict, basis: verdict.basis });
  });

Next.js — a route handler

// app/api/verify/route.ts
import { CapSeal } from "@capseal/sdk";
import { NextResponse } from "next/server";

const capseal = new CapSeal({ apiKey: process.env.CAPSEAL_API_KEY! });

export async function POST(request: Request) {
  const form = await request.formData();
  const file = form.get("file");
  if (!(file instanceof Blob)) {
    return NextResponse.json({ error: "no file" }, { status: 400 });
  }

  const { verdict, report } = await capseal.inspect(file);
  return NextResponse.json({
    verdict: verdict.verdict,
    basis: verdict.basis,
    headline: verdict.headline,
    sha256: report.file.sha256,
  });
}

Browser — never put the key in the client

// The key belongs on your server. This posts to your own endpoint, which calls
// CapSeal. A key shipped to a browser is a key you have given away.
const response = await fetch("/api/verify", { method: "POST", body: file });
const { verdict, basis, headline } = await response.json();

document.querySelector("#result").textContent =
  basis === "declared" || basis === "proven"
    ? headline
    : `${headline} (needs a human: ${basis})`;

Cloudflare Workers

import { CapSeal } from "@capseal/sdk";

export default {
  async fetch(request: Request, env: { CAPSEAL_API_KEY: string }) {
    const capseal = new CapSeal({ apiKey: env.CAPSEAL_API_KEY });
    const { verdict } = await capseal.inspect(await request.arrayBuffer());
    return Response.json(verdict);
  },
};

Handling every verdict

const { verdict } = await capseal.inspect(bytes);

switch (verdict.verdict) {
  case "SEALED_BY_CAPSEAL":
    // Captured through a CapSeal SDK. Integrity signals were measured on the
    // device at the moment of capture.
    break;
  case "AI_GENERATED":
    // A generator declared itself, in a signed manifest or in XMP.
    break;
  case "EDITED":
    // The file records edits: crops, colour work, composites.
    break;
  case "PROVENANCE_PRESENT":
    // C2PA provenance from a camera or tool, nothing notable declared.
    break;
  case "CAMERA_ORIGINAL_LIKELY":
    // MakerNote, capture timestamp, no editor named. Indicative, not proof.
    break;
  case "NO_PROVENANCE":
    // Nothing to go on. Common and not suspicious on its own.
    break;
}

Verify a proof with no key and no network

If someone hands you a CapSeal proof object, checking it through our API means trusting us about our own work. Don't.

npm i @capseal/wasm
import { loadCapseal } from "@capseal/wasm";
import { readFileSync } from "node:fs";

const capseal = await loadCapseal(
  readFileSync("node_modules/@capseal/wasm/capseal.wasm"),
);

const result = capseal.replay({ signedProof, proofPublicKeyBase64, assetBase64 });

result.signatureValid    // we issued it, unedited
result.reasoningReplays  // the conclusions follow from the checks
result.evidenceReplays   // the checks follow from the file
result.trustworthy

That module imports nothing — it cannot open a socket or read your disk, and you can confirm that in one line rather than take our word for it:

WebAssembly.Module.imports(new WebAssembly.Module(bytes));  // []

It exists precisely so a verdict never depends on our goodwill or our uptime.


What it reads

C2PA / Content Credentials — the full manifest: claim generator, every assertion, actions, ingredients, signature algorithm. Multi-segment manifests are reassembled correctly (a manifest routinely exceeds JPEG's 64KB segment limit, and readers that miss this report valid files as corrupt).

EXIF — every IFD0, ExifIFD and GPS tag, with the provenance-relevant ones surfaced: camera make and model, MakerNote presence (editors usually destroy it), Software, capture and modify times, coordinates.

XMP — CreatorTool, edit history, and the IPTC digitalSourceType vocabulary, which is how a generative model declares itself.

PDF — Producer and Creator, plus the count of appended revisions, which is direct evidence of modification after writing.

Containers — JPEG, PNG, WebP, GIF, TIFF, HEIC, AVIF, MP4, QuickTime, WebM, PDF, SVG. Detected by magic bytes, never by the filename you were given.

The full report

verdict is the answer. report is everything it was based on — so if you disagree, the material to argue with is in the same response.

report.file.sha256                        // ties the result to exact bytes
report.file.container                     // "jpeg"
report.c2pa.activeManifest?.claimGenerator
report.c2pa.activeManifest?.actions       // [{ action: "c2pa.color_adjustments" }]
report.c2pa.activeManifest?.assertions    // every one, with size and kind
report.exif                               // every tag
report.xmp                                // parsed, plus the raw packet
report.evidence                           // each observation, with its source
report.warnings                           // anything that failed to parse

Errors you can act on

import { CapSealError } from "@capseal/sdk";

try {
  await capseal.inspect(bytes);
} catch (error) {
  if (error instanceof CapSealError) {
    error.status;    // 401, 413, 429 ...
    error.retryable; // true on 429
    error.message;   // "That API key was revoked. Issue a new one at ..."
  }
}

The platform distinguishes not recognised from revoked from over quota, and the SDK passes that through rather than collapsing it into "request failed".

Watch your quota without guessing:

const { quota } = await capseal.inspect(bytes);
if (quota && quota.remaining < 50) warnOperations();

Reference

new CapSeal({
  apiKey: string,        // required — https://capseal.ai/account
  baseUrl?: string,      // defaults to https://capseal.ai
  fetch?: typeof fetch,  // for tests or a proxy
  timeoutMs?: number,    // defaults to 30000
});

| method | does | | --- | --- | | inspect(file) | read a file's provenance. Takes Uint8Array, ArrayBuffer, Buffer or Blob | | replay(proof) | re-check a proof object we issued | | capabilities() | what this deployment does, and what it deliberately will not |

Node 18+, Deno, Bun, browsers, Cloudflare Workers. Types included. No runtime dependencies.


FAQ

Can you tell me if an image is AI-generated? If the generator declared it — which Firefly, and a growing number of others, do via C2PA — then yes, definitively, with the manifest as evidence. If it didn't, then no, and neither can anyone else reliably. We say so rather than guess.

What about a photo with the metadata stripped? NO_PROVENANCE / unknown. That is the honest answer: stripping is what every messaging app does to perfectly genuine photographs, so its absence tells you almost nothing. Route those to a human.

Do you store my files? No. Files are read in memory and discarded. The response says so explicitly in retention.

Is the SDK open source? Yes — github.com/CapSeal-ai/sdk. Verification runs on the platform, which is what makes the API key meaningful. If you want verification that runs entirely on your machine, that is @capseal/wasm, and it needs no key at all.

How do I stop this happening in the first place? Read provenance and you are always reacting. Seal at capture and you are not: the CapSeal capture SDKs for iOS and Android measure integrity signals on the device — parallax against the IMU, depth relief, screen-replay detection, hardware attestation — and bind them into the manifest at the moment the shutter fires. That turns unknown into proven. See capseal.ai/platform.


Limits

  • 25 MB per file
  • 1,000 calls a day on the free tier
  • Nothing stored

By SYPTime Pty Ltd · Issues · capseal.ai