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

@dpdpguard/convex

v0.4.2

Published

Convex Component for dpdpbot (DPDP Guard) - native Convex integration over the dpdpbot /api/v1 contract

Readme

@dpdpguard/convex

A Convex Component for dpdpbot (DPDP Guard) — a native Convex integration over dpdpbot's public /api/v1 contract, as an alternative to wiring @dpdpguard/server into a Convex action by hand.

Status

Wired to dpdpbot's real /api/v1 contract (paths, auth model, and generated types come from the installed @dpdpguard/contract package — see "Building" below), currently tracking contract 1.5.0 / OpenAPI 2.8.0. The brokered-principal methods mirror @dpdpguard/server's method names 1:1 so migrating from the Node SDK is close to a search-and-replace; the service-key surface (Consent Gate, compliance registers, offline capture) is described under "Two credentials, two surfaces" below.

Why this exists instead of just using the Node SDK

Convex mutations/queries can't make outbound HTTP calls — only actions can — so any integration with dpdpbot from a Convex app needs an action layer somewhere. What a Convex Component adds over a hand-rolled action wrapper:

  • Reactive local cache. Notices, DSR requests, grievances, and nominations are mirrored into this component's own isolated tables. useQuery(api.dpdp.listDsrRequests, ...) in the host app's frontend is a live subscription, not a manual refetch loop.
  • Webhook mounting. registerRoutes() wires an httpAction the host app mounts in its own convex/http.ts, verified against dpdpbot's real X-DPDP-Signature header. As of writing dpdpbot only dispatches consent.given/consent.withdrawn webhooks (org-scoped, no per-principal id in the payload) — DSR/grievance state is kept in sync by polling (dsr.refresh/grievances.refresh, run on a cron), not by webhook, until dpdpbot ships principal-scoped lifecycle webhooks. See webhooks.ts and crons.ts.
  • Isolated schema. The component's tables live in src/component/schema.ts, invisible to the host app's own convex/schema.ts — no naming collisions, no migrations to coordinate.
  • No node runtime. Webhook signature verification is ported to Web Crypto (src/component/_lib/dpdpClient.ts) instead of node's crypto module, so the whole component runs in Convex's default V8 action runtime — faster cold starts, no "use node" bundling weight.

This is not a special backend-to-backend channel into dpdpbot's own Convex deployment — dpdpbot is a separate deployment, so this component still just does fetch() against its public API, same as the Node SDK. The value-add is entirely in Convex-native packaging.

Layout

src/
  component/           # runs inside the host app's Convex deployment
    convex.config.ts   # defineComponent("dpdpguard")
    schema.ts           # isolated tables: config, notices, consentLinks,
                         # dsrRequests, grievances, nominations, webhookEvents,
                         # offlineCaptures, offlineConsentLinks
    config.ts            # setup() mutation to store baseUrl/apiKey/orgId
    notices.ts            # cached read + refresh action
    consent.ts             # brokerToken, linkAnonymousConsent, token cache
    dsr.ts                  # createDsrRequest, listDsrRequests, refresh
    grievances.ts            # createGrievance, listGrievances, refresh
    nominations.ts            # upsert/get/revoke
    consentGate.ts             # verify() — stop-processing check, never cached
    partner.ts                  # retention/breach/cross-border register reads
    offline.ts                   # QR links, capture batch sync, POS + IVR
    reconcile.ts                  # cron-driven per-principal DSR/grievance poll
    webhooks.ts                    # httpAction handler + signature verification
    http.ts                         # registerRoutes() the host mounts
    crons.ts                         # reconciliation fallback for missed webhooks
    generated/api-types.ts            # openapi-typescript output, gitignored
    _lib/dpdpClient.ts                 # typed fetch wrapper + Web Crypto HMAC verify
    _lib/errorCatalog.ts                # ERROR_CATALOG + DpdpGuardApiError
  client/
    index.ts                      # DpdpGuard class — the app-facing API
example/                           # minimal host app used to run `npx convex dev`
                                    # and generate src/component/_generated

Using it in a host app

// convex/convex.config.ts
import { defineApp } from "convex/server";
import dpdpguard from "@dpdpguard/convex/convex.config";

const app = defineApp();
app.use(dpdpguard);
export default app;
// convex/http.ts
import { httpRouter } from "convex/server";
import { registerRoutes } from "@dpdpguard/convex/http";

const http = httpRouter();
registerRoutes(http);
export default http;
// convex/dpdp.ts
import { DpdpGuard } from "@dpdpguard/convex";
import { components } from "./_generated/api";

export const dpdp = new DpdpGuard(components.dpdpguard);
// one-time setup, e.g. from an admin mutation or a setup script.
// baseUrl is your dpdpbot deployment's HTTP Actions URL
// (https://{deployment}.convex.site), apiKey is a service API key
// (convex/apiKeys.ts on the dpdpbot side), orgId is your organization's id.
await dpdp.configure(ctx, { baseUrl: "https://your-deployment.convex.site", apiKey: "...", orgId: "..." });
// per principal, before any DSR/grievance/nomination call for them - mints
// and caches a brokered bearer token (ADR-004). Re-call after it expires.
await dpdp.brokerToken(ctx, user.tokenIdentifier);
// in an action
const dsr = await dpdp.createDsrRequest(ctx, { externalId: user.tokenIdentifier, type: "erasure" });

// in a React component
const myDsrRequests = useQuery(api.dpdp.listDsrRequests, { externalId });

Two credentials, two surfaces

Everything above is the brokered-principal surface: a data principal acting on their own records, so each call carries the short-lived bearer token brokerToken() mints.

The rest of the component is the service-key surface — the host app's own backend acting as the fiduciary, authenticated with the apiKey passed to configure(). No brokerToken() call is needed for any of it.

Consent Gate

// in an action, immediately before acting on a principal's data
const { decision } = await dpdp.verifyConsent(ctx, consentId);
if (decision !== "allow") return; // withdrawn — stop processing

This is an action, not a query, and nothing about the answer is cached or stored. The gate's whole value is that the decision comes from the consent record's current withdrawal state rather than from anything the caller remembers, so subscribing to it would defeat it. Treat a thrown DpdpGuardApiError as "do not process" — an unreachable gate is not consent.

Compliance registers

const { due } = await dpdp.listDueRetentions(ctx);        // GET /api/v1/retention/due
const { breaches } = await dpdp.listBreaches(ctx);        // GET /api/v1/breaches
await dpdp.listCrossBorderTransfers(ctx);                 // GET /api/v1/cross-border/transfers

Read live rather than mirrored into component tables: these are registers a host app acts on now, and a stale copy of "who is past their retention window" is worse than no copy. Each route is flag-gated on the dpdpbot side — while its flag is off it 404s, arriving here as a DpdpGuardApiError with code NOT_FOUND, which stays distinguishable from an enabled route that simply has nothing to report.

Offline / physical capture

// mint a QR/offline consent link from a POS terminal or hospital system
const link = await dpdp.createOfflineConsentLink(ctx, {
  noticeId, purpose: "Account opening", dataTypes: ["email"], label: "Counter 3",
});

// push queued device-signed sessions when connectivity returns
const results = await dpdp.syncOfflineCaptures(ctx, sessions);
// a batch that resolves without throwing may still contain rejected sessions
const failed = results.filter((r) => r.status !== "verified");

// what this component recorded for one session (idempotency bookkeeping)
const outcome = useQuery(api.dpdp.getOfflineCapture, { clientSessionId });

// a till raises an async request — NOT a consent until the principal confirms
await dpdp.createPosConsentRequest(ctx, { noticeId, purpose, dataTypes, phone });

// a telephony partner submits a completed call's artefact
await dpdp.recordIvrConsentArtifact(ctx, artifact);

Capture results are stored keyed by clientSessionId — the contract's own idempotency key — so a device that loses connectivity mid-sync can ask what already committed instead of replaying blind. Replaying a committed batch writes nothing on either side.

Building

src/component/generated/api-types.ts is generated by scripts/codegen.mjs (via openapi-typescript, run against the openapi/v1.yaml shipped inside the installed @dpdpguard/contract package — the same mechanism dpdpguard-server-node-sdk/scripts/codegen.mjs uses) and is gitignored. src/component/_generated and example/convex/_generated are Convex-CLI generated and also gitignored. To build:

npm install        # runs codegen via postinstall
cd example
npm install
npx convex dev --once   # generates _generated/ for both the example app and the component
cd ..
npm run build       # re-runs codegen, then tsc

Known gaps

  • Only notices and (via the reconcile.principals cron) linked principals' DSR/grievance state are polled for reconciliation. Nomination changes made directly on dpdpbot's dashboard aren't reflected locally until the next time the host app calls getNomination/re-fetches — there's no nomination polling cron, since GET /api/v1/nomination only returns the caller's own single record and dpdpbot doesn't page/filter that endpoint the way DSR/grievance listing do.
  • Webhook signature verification expects DPDPGUARD_WEBHOOK_SECRET as a host app environment variable; there's no config.setup() field for it yet. The header name (X-DPDP-Signature) and HMAC-SHA256/hex algorithm are confirmed against dpdpbot's actual convex/webhooks.ts, but as noted above, the events it currently dispatches (consent.given/consent.withdrawn) don't carry enough information to update this component's per-principal cache tables — webhooks.ts currently just records that an event arrived, and does nothing with the payload yet.
  • Every call is tested against a mocked dpdpClient, never against a live dpdpbot deployment — the shapes those mocks return are typed from @dpdpguard/contract, so they can't drift from the wire contract, but nothing here exercises a real HTTP round trip.
  • @dpdpguard/contract 1.5.0 (now the installed/tracked version) adds two Consent Gate observability routes (GET /api/v1/consent/gate/decisions and .../alerts) that aren't bound to this component's client surface yet — the 0.4.0 sync only updated the contract dependency and regenerated types. Wiring dpdp.listConsentGateDecisions() / dpdp.listConsentGateAlerts() is left for a follow-up change.
  • The /api/v1/staff/* fiduciary/DPO surface is not wrapped at all. It authenticates with a staff login session rather than either credential this component holds, so it has no natural home here.
  • The reconcile.principals cron loops over every linked principal every 30 minutes with no batching/pagination — fine at small scale, but should be revisited (e.g. only reconcile principals with an open DSR/grievance) before use with a large user base.

License

Apache-2.0, matching @dpdpguard/contract.