Skip to main content

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-merchant

API 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

ClassCodes
OfferVerifyErroroffer_signature_expired, offer_signature_in_future, offer_keyid_unknown, offer_body_digest_mismatch, offer_target_uri_mismatch, offer_alg_rejected
MandateVerifyErrormandate_signature_invalid, mandate_audience_mismatch, mandate_kb_audience_mismatch, mandate_kb_nonce_mismatch, mandate_kb_signature_invalid, mandate_expired, mandate_status_revoked
ChargeErrorcharge_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.