@indiciopbc/proven-id-lib
v0.1.3
Published
Proven ID Library — server-side credential verification orchestration.
Keywords
Readme
@indiciopbc/proven-id-lib
Server-side, framework-agnostic library for integrating Proven ID credential verification into any Node backend. It handles:
- A WebSocket server that pushes verification state to the browser (paired with
@indiciopbc/proven-id-json the frontend) - Outbound calls to the Proven Agent (OOB invitation creation, credential presentation requests)
- Parsing and processing the Proven Agent's webhook callbacks — via
handleWebhookPayload(), callable from any HTTP framework - Your business logic hook (
validationRules) for deciding whether a presented credential grants access
For the conceptual walkthrough — system components, the full initialization/presentation sequence, WebSocket message schema, and webhook payload shapes — see SPEC.md. This README covers installing and calling the actual API.
Install
yarn add @indiciopbc/proven-id-libThe core library has no framework dependency. If you use the bundled Express adapter (@indiciopbc/proven-id-lib/dist/express), express (^4.19.0) is a peer dependency — install it if your backend doesn't already have it. It's optional otherwise.
Quick start
import { ProvenIdLib } from "@indiciopbc/proven-id-lib";
import { createExpressWebhookRouter } from "@indiciopbc/proven-id-lib/dist/express";
import express from "express";
const provenLib = ProvenIdLib.init({
websocket: { path: "/ws", sessionTimeoutMs: 5 * 60 * 1000 },
credentialSchema: "<issuer-did>:2:<schema-name>:<schema-version>",
credentialAttributes: ["employer_name"],
validationRules: async (attributes) => {
if (attributes.employer_name !== "Acme Corp") {
return { status: "failed", code: "employer_mismatch" };
}
return { status: "success", code: "verification_success" };
},
provenAgent: { baseUrl: process.env.PROVEN_AGENT_BASE_URL! },
provenApiKey: process.env.PROVEN_API_KEY!,
});
const app = express();
app.use(express.json());
app.use("/api/controller-webhook", createExpressWebhookRouter(provenLib.webhookDeps()));
const server = app.listen(3000);
provenLib.attachWebSocketServer(server);Using a different framework
The Express adapter is a thin, optional convenience. Any framework can wire up the webhook endpoint directly against handleWebhookPayload(), which takes the already-parsed JSON body and returns { ok: boolean }:
// Fastify, Koa, Hono, raw http — anything that hands you a parsed body.
fastify.post("/api/controller-webhook", async (req, reply) => {
const result = await provenLib.handleWebhookPayload(req.body);
reply.status(200).send(result);
});See config.sample.ts for every ProvenIdConfig field with inline documentation, and backend/src/provenIdConfig.ts / backend/src/server.ts in this repo for a complete, real-world wiring example (including graceful shutdown via provenLib.close()).
Note the setup ordering from SPEC.md §3: provenApiKey only exists once you've configured the webhook URL in the Proven Agent UI and generated a key there, so the everything-else-first / provenApiKey-last sequence in that section is expected, not a chicken-and-egg bug.
ProvenIdLib API
| Method | Description |
|---|---|
| ProvenIdLib.init(config: ProvenIdConfig) | Validates config (throws ConfigValidationError on bad input) and returns a ProvenIdLib instance. |
| .handleWebhookPayload(payload: unknown): Promise<{ ok: boolean }> | Framework-agnostic entry point — pass it the already-parsed JSON body from any HTTP framework's webhook route. |
| .webhookDeps(): WebhookHandlerDeps | Exposes the internal deps bundle for framework adapters, e.g. createExpressWebhookRouter(provenLib.webhookDeps()). |
| .attachWebSocketServer(httpServer: http.Server) | Attaches the WebSocket server to an existing HTTP server at config.websocket.path. Call once, after app.listen(). |
| .close(): Promise<void> | Closes all open WebSocket sessions and the WebSocket server. Call during shutdown. |
createExpressWebhookRouter(deps: WebhookHandlerDeps): express.Router, from @indiciopbc/proven-id-lib/dist/express, builds an Express router around handleWebhookPayload. It's the only place in the package that imports express.
Writing validationRules
type ValidationRulesFn = (
attributes: Record<string, string>,
) => ValidationResult | Promise<ValidationResult>;
interface ValidationResult {
status: "success" | "failed" | "error";
code: string;
message?: string;
redirectUrl?: string;
}This is called only on a successful, cryptographically-verified presentation (verification_status: "success") — it's where your business logic decides whether the presented attributes are actually sufficient (e.g. checking Nationality against an allowlist), not where you check whether the credential itself is valid. A failed or errored presentation never reaches validationRules; see onVerificationEvent below for those.
redirectUrl (if returned) takes precedence over message in the frontend UI. If omitted on success, the library falls back to config.redirectUrl, if set.
Real example, from this repo's demo backend (backend/src/provenIdConfig.ts):
function buildValidationRules(app: AppConfig, pool: Pool): ValidationRulesFn {
return async (attributes) => {
if (attributes.Nationality && attributes.Nationality !== app.identity.expectedNationality) {
return { status: "failed", code: "nationality_mismatch", message: "Nationality does not match the expected nationality." };
}
const { rows } = await pool.query(
`INSERT INTO verification_log (schema_id, attributes, verification_status, result_code)
VALUES ($1, $2, 'success', 'verification_success') RETURNING id`,
[app.proven.credentialSchema, attributes],
);
const sessionId = randomUUID();
await pool.query(
`INSERT INTO sessions (id, verification_log_id, expires_at) VALUES ($1, $2, now() + ($3 || ' minutes')::interval)`,
[sessionId, rows[0].id, app.session.durationMinutes],
);
return { status: "success", code: "verification_success", redirectUrl: `/api/session/exchange?token=${sessionId}` };
};
}onVerificationEvent
type VerificationEventHandler = (event: VerificationEvent) => void | Promise<void>;Optional. Fires for every outcome where validationRules is not invoked — a failed verification (bad signature/schema compliance) or an error/user-rejection. Use it for audit logging; per SPEC.md §4.3, a failed verification should be treated as potentially fraudulent, so don't trust event.attributes in this handler for anything beyond logging.
VerificationEvent includes sessionId, invitationId, schemaId, attributes, verificationStatus ("success" | "failed"), resultStatus ("success" | "failed" | "error"), code, message, error, and receivedAt.
Webhook endpoint
Wire handleWebhookPayload() (directly, or via createExpressWebhookRouter()) at the path you register as the Proven Agent's webhook URL (SPEC.md §3.2). It accepts both webhook payload shapes described in SPEC.md §6 (connection-established and presentation-result). Whatever framework you use, make sure the JSON body is already parsed before it reaches this call (e.g. express.json() upstream in the middleware chain).
Webhook authentication
SPEC.md §6 states the Proven Agent authenticates its webhook calls with an API key in an HTTP header. Set config.webhookAuth = { headerName, apiKey } to require that header on every inbound webhook request — requests missing it or presenting the wrong value are rejected ({ ok: false, unauthorized: true }, surfaced as 401 by createExpressWebhookRouter) before the payload is processed. If you call handleWebhookPayload()/processWebhookEvent() directly instead of through the Express adapter, pass the incoming request's headers as the third argument. webhookAuth is optional — omit it and no check is performed (useful for local dev before you've confirmed the header name against your live agent).
WebSocket protocol
The outbound message types (invitation, processing, result, session_expired) and their fields are specified in SPEC.md §5 — @indiciopbc/proven-id-js already implements the client side of this protocol, so you only need it if you're writing a custom frontend client.
Errors
ConfigValidationError— thrown synchronously fromProvenIdLib.init()when required config fields are missing or malformed.ProvenAgentError— thrown by the internal Proven Agent HTTP client on non-2xx responses after retries are exhausted; surfaces as anerror-status result message to the connected client rather than an unhandled rejection.
See also
@indiciopbc/proven-id-jsREADME — the paired frontend snippetSPEC.md— protocol and workflow reference
