Node SDK: @oid4pay/oid4ac-merchant
The Node SDK ships ESM + CJS bundles. Node 20+ is required (Web Crypto Ed25519). TypeScript types are bundled. Zero third-party JWT deps; the SDK relies on Web Crypto and a thin RFC 9421 implementation.
Install
npm install @oid4pay/oid4ac-merchant
# or
pnpm add @oid4pay/oid4ac-merchant
# or
yarn add @oid4pay/oid4ac-merchantAPI reference
verifyOffer(body, headers, jwks, options?)
Verifies an RFC 9421 signed offer body against a merchant JWKS. Throws OfferVerifyError with one of the offer_signature_expired, offer_keyid_unknown, offer_body_digest_mismatch, offer_target_uri_mismatch, offer_alg_rejected codes on failure. Returns { keyid, alg, created, expires, bodyDigest, method, targetUri, authority } on success.
signOffer(body, options)
Helper to sign your own outgoing offers. Returns { body, headers: { "Content-Digest", "Signature-Input",
"Signature" } }. Required options: privateJwk, keyid, targetUri, expiresIn (seconds, capped at 300 by the HTTP message signature rules).
verifyMandate(sdJwtVc, kbJwt, asJwks, options)
Verifies an SD-JWT VC mandate + KB-JWT pair against the AS JWKS. Throws MandateVerifyError with one of the mandate_signature_invalid, mandate_kb_audience_mismatch, mandate_kb_nonce_mismatch, mandate_status_revoked codes on failure.
Returns a VerifiedMandate with the disclosed claims and the cnf.jkt the wallet bound at issue time.
charge({ accessToken, dpopKey, offerDigest, idempotencyKey, destinationAccount })
Settles a charge against the AS by presenting the JWT-AT and a fresh
DPoP proof. The DPoP key MUST be the same key whose jkt appears in the JWT-AT's cnf claim. Returns { charge_id, mandate_id, stripe_payment_intent_id, settled_at }.
Errors carry sentinel codes: charge_mandate_revoked, charge_offer_amount_exceeds_mandate, charge_dpop_nonce_required, charge_idempotency_replay.
fetchStatusList(asOrigin, listId)
Fetches the W3C VC Status List 2021 credential. See the status list for the shape. Returns the decoded bitmap and the list credential's signature for verification.
End-to-end example
import {
verifyOffer,
verifyMandate,
charge,
} from "@oid4pay/oid4ac-merchant";
import { randomBytes, createHash } from "node:crypto";
export async function POST(request) {
const {
offerBody,
offerHeaders,
sdJwtVc,
kbJwt,
accessToken,
dpopProof,
} = await request.json();
const ownJwks = await loadOwnJwks();
const asJwks = await fetchAsJwks();
const v = await verifyOffer(offerBody, offerHeaders, ownJwks, {
expectedTargetUri: `https://shop.example.com/products/${offerBody.sku}`,
});
const merchantNonce = randomBytes(16).toString("base64url");
const expectedNonce = createHash("sha256")
.update(merchantNonce + ":" + v.bodyDigest)
.digest("base64url");
const m = await verifyMandate(sdJwtVc, kbJwt, asJwks, {
expectedAudience: "https://shop.example.com",
expectedNonce,
});
if (m.spendCapMinor < offerBody.amount_minor) {
return Response.json(
{ error: "mandate_cap_exceeded" },
{ status: 402 },
);
}
const result = await charge({
accessToken,
dpopProof,
offerDigest: v.bodyDigest,
idempotencyKey: crypto.randomUUID(),
destinationAccount: process.env.STRIPE_CONNECT_ACCT,
});
return Response.json(result);
}Algorithm whitelist
The Node SDK accepts ed25519 and ecdsa-p256-sha256 for signed offers; it refuses HMAC, alg=none, and every other RFC 9421 algorithm. JWT-AT
verification accepts EdDSA only, per the algorithm whitelist.
Error reference
| Class | Codes |
|---|---|
OfferVerifyError | offer_signature_expired, offer_signature_in_future, offer_keyid_unknown, offer_body_digest_mismatch, offer_target_uri_mismatch, offer_alg_rejected |
MandateVerifyError | mandate_signature_invalid, mandate_audience_mismatch, mandate_kb_audience_mismatch, mandate_kb_nonce_mismatch, mandate_kb_signature_invalid, mandate_expired, mandate_status_revoked |
ChargeError | charge_mandate_revoked, charge_offer_amount_exceeds_mandate, charge_dpop_nonce_required, charge_idempotency_replay |
Source
The package lives at sdks/node-oid4ac-merchant/ in the OID4Pay
repo. Releases follow SemVer; breaking wire-version bumps move the major.
Subscribe to the changelog for advance notice.