@noviuz/kyc-server
v2.0.0
Published
Partner-server helper that HMAC-signs the Noviuz hosted-KYC session mint (Tier-1 credential) and returns a sessionId. Node-only — never bundle for the browser.
Readme
@noviuz/kyc-server
Node/edge server SDK for Noviuz partners: mints hosted-KYC sessions against the Noviuz
backend (noviuz-app) with a server-only Bearer secret — the market-standard pattern
(Stripe, Plaid) — and returns the sessionId your frontend passes to
@noviuz/kyc-js.
Never bundle this package for the browser. It carries your server-only secret. The package throws at import time if it detects a
windowglobal.
Install
npm install @noviuz/kyc-server # Node >= 18 (global fetch), or any Workers/edge runtimeQuick start (Bearer — recommended)
The @noviuz/kyc-server/bearer entry is Workers-safe (plain fetch, no node:crypto) — it
runs on Node, Cloudflare Workers / Pages Functions, Vercel/Netlify edge, Deno, and Lambda.
import { createSessionClient } from '@noviuz/kyc-server/bearer';
const kyc = createSessionClient({
baseUrl: 'https://api.noviuz.com', // origin only — no path, credentials, query, or fragment
secretKey: process.env.NOVIUZ_SECRET_KEY!, // server-only Bearer secret; never in the browser
timeoutMs: 10_000, // optional; default 10s
// allowInsecureLocalhost: true, // ONLY for http://localhost|127.0.0.1|::1 during local dev
});
// In your session endpoint (e.g. POST /kyc/session on YOUR server):
const session = await kyc.mintSession(
{
customerType: 'individual', // or 'business'
verificationMethods: ['pix', 'openfinance'], // optional rail allowlist
merchantId: 'NVZ123', // optional
orderId: 'order_42', // optional
},
{ idempotencyKey: crypto.randomUUID() }, // REQUIRED — same key + same body replays the same session
);
// Hand ONLY the sessionId to your frontend:
res.json({ sessionId: session.sessionId });Authoritative result reconciliation
The hosted page's redirect/callback outcome is a notification, not a decision — always reconcile the true outcome server-to-server before acting on it (e.g. before granting access or releasing funds):
const result = await kyc.getResult(sessionId); // GET .../result — read-only, never mutates state
// result: { status, sessionId, referenceId?, euid?, noviuz_euid? } — see @noviuz/kyc-contracts
switch (result.status) {
case 'approved':
/* … */ break;
case 'rejected':
case 'expired':
case 'cancelled':
case 'under_review':
/* … */ break;
}getResult throws a typed KycMintError for 401 (UNAUTHORIZED), 404 (NOT_FOUND — unknown
session or a session owned by another operator; the two are deliberately indistinguishable), 409
(NOT_TERMINAL — no decision yet, retry later), 410 (EXPIRED), and 429 (RATE_LIMITED). The
200 body is runtime-validated against @noviuz/kyc-contracts's kycResultSchema — a malformed or
unexpected shape throws MALFORMED_RESPONSE instead of propagating a broken result into your app.
Frontend side — open with the server-minted sessionId; treat the callback as a notification
and reconcile server-side before acting on it (never persist a value straight off the callback):
NoviuzKyc.open({
sessionId,
onSuccess: (result) => reconcileOnYourServer(result.sessionId), // notification — not a decision
onError: (error) => console.error(error.code, error.message),
});API
createSessionClient(options): MintClient — @noviuz/kyc-server/bearer
| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| baseUrl | string | — | Origin only (https://api.noviuz.com) — no path, embedded credentials, query, or fragment. HTTPS required. |
| secretKey | string | — | Server-only secret, sent as Authorization: Bearer <secretKey>. Must be non-blank. Keep it in a secret manager. |
| timeoutMs | number | 10000 | Per-request timeout (AbortController). |
| fetch | typeof fetch | global | Injectable for tests/instrumentation. |
| allowInsecureLocalhost | boolean | false | Opt-in to allow plaintext http:// — ONLY for localhost/127.0.0.1/::1. Never set for a remote/production origin. |
mintSession(params, options): Promise<SessionInit>
POSTs params ({ customerType, verificationMethods?, merchantId?, orderId? }) to
/api/v1/kyc/sessions with a required options.idempotencyKey, sent as the Idempotency-Key
header — production Bearer mint is idempotent per
the v2 contract: the same key +
the same body replays the original minted session; the same key + a different body is a conflict.
The response is runtime-validated — a malformed response throws KycMintError('MALFORMED_RESPONSE')
instead of propagating a broken shape into your app.
getResult(sessionId): Promise<KycResultV2>
See Authoritative result reconciliation above.
KycMintError
Typed failures with a stable code and the HTTP status. Bearer mint surfaces
INVALID_CREDENTIAL, IP_NOT_ALLOWED (403), RATE_LIMITED (429), INVALID_REQUEST (other 4xx),
NETWORK, TIMEOUT, and MALFORMED_RESPONSE. getResult surfaces UNAUTHORIZED (401),
NOT_FOUND (404), NOT_TERMINAL (409), EXPIRED (410), RATE_LIMITED (429), NETWORK,
TIMEOUT, and MALFORMED_RESPONSE — a dedicated mapping, since the result endpoint's status
semantics differ from mint's. (MISSING_HEADERS, INVALID_SIGNATURE, and NONCE_REUSED only
arise in the HMAC mode below.) Error messages never echo your secret, headers, or the response
body.
Advanced: HMAC request-signing (non-live only)
Bearer is the only production auth mode. A
live-environment mint presentingX-Api-Key(this HMAC mode) is hard-rejected401, fail-closed — the backend refuses to bootlivewith anything but Bearer (v2 contract §3.2). HMAC exists only for non-live(dev/staging) environments during the migration window; do not build a production integration on it.
The main @noviuz/kyc-server export offers a request-signing mode for teams that need
per-request HMAC signatures in a non-live environment instead of a static Bearer secret. It is
Node-only (uses node:crypto), so it is not Workers-safe. For production, use Bearer.
import { createSessionClient } from '@noviuz/kyc-server';
const kyc = createSessionClient({
baseUrl: 'https://api.noviuz.com',
credential: {
apiKey: process.env.NOVIUZ_API_KEY!,
signingSecret: process.env.NOVIUZ_SIGNING_SECRET!,
},
timeoutMs: 10_000,
retries: 2, // optional; retries transient failures only, each freshly signed
});Each request carries X-Api-Key, X-Timestamp, X-Nonce, and X-Signature, where the
signature is HMAC-SHA256(secret, "{timestamp}.{method}.{path}.{sha256(body)}") — a byte-exact
replica of the backend's compute_signature.
Known limitation:
X-Nonceis sent but not part of the signed message, so the backend's replay check only catches byte-identical replays; a captured signed request can be replayed with a fresh nonce within the timestamp-tolerance window. Blast radius is orphan sessions — a replayed mint can only create an unauthorized session, never approve a KYC or move funds. Fixed when the signing contract adds the nonce (noviuz-appprotocol v2).
Security checklist
- ✅ Secrets only from env/secret manager — never in code or the frontend.
- ✅ This package never logs or echoes the secret, auth headers, or response bodies.
- ✅ Node ≥ 18 (or a
fetch-capable edge runtime), TLS-onlybaseUrl(enforced). - ✅ Session IDs are short-lived, single-flow tokens — safe to hand to the browser.
