Skip to content

eID Gerege — TypeScript RP SDK

@eid-mongolia/sdk — a Node.js/TypeScript library that runs on the Relying Party (RP) backend. It gathers every call to the RP-API (/v3) (authentication, signing, session poll) into a single EidClient, and adds cryptographic verification of responses (cert chain + signature) on top.

This is the typed wrapper over the raw HTTP contract described in docs/RP_INTEGRATION.md — the same endpoints, the same security model, but it generates challenges, repeatedly polls sessions, and validates certificates on your behalf.

⚠️ Server-side only. The API secret (rp_sk_…) must never be exposed to a browser/phone. The SDK relies on node:crypto, so it is not a browser bundle.

Source: sdk/typescript/src/


1. Installation

npm install @eid-mongolia/sdk
  • Node.js ≥ 18 (global fetch, node:crypto).
  • Ships both ESM and CommonJS (import / require).
import { EidClient, sha256Base64 } from "@eid-mongolia/sdk";

Package name and engine requirements: sdk/typescript/package.json.


2. Registering an RP

The operator registers the RP in advance (docs/RP_INTEGRATION.md §1). Via the admin console: admin.eidmongolia.mnRelying Party (RP)+ New RP. Once registered, a UUID and an API secret (rp_sk_…) are produced — the secret is shown only once.

Pass these into RpCredentials:

Field Value
rpUUID The RP UUID registered in relying_parties
rpName The RP name shown to the citizen (≤ 32 bytes UTF-8)
apiSecret API secret (rp_sk_…) — backend only

Source: types.tsRpCredentials.


3. Configuring the client

EidClient is the entry point for every flow:

import { EidClient } from "@eid-mongolia/sdk";

const eid = new EidClient({
  baseUrl: "https://rp-api.eidmongolia.mn",          // without /v3 — the SDK adds it
  credentials: {
    rpUUID: process.env.EID_RP_UUID!,
    rpName: "Khan Bank",
    apiSecret: process.env.EID_SECRET!,              // rp_sk_…
  },
  trust: { trustAnchorsPem: [process.env.EID_ROOT_CA_PEM!] }, // REQUIRED in production
});

All fields of ClientConfig (client.ts):

Field Required Value
baseUrl RP-API base URL. The SDK appends /v3.
credentials RpCredentials (above). If apiSecret is empty the constructor throws.
trust ✅ in production TrustConfig — trust anchor (Root CA) + validator settings.
defaultCertificateLevel QUALIFIED (default) or ADVANCED.
timeoutMs HTTP timeout (ms). Default 15000.
dispatcher undici Dispatcher — mTLS client cert (§8).
fetchImpl fetch replacement (testing).

The constructor throws an Error immediately if baseUrl or credentials.apiSecret is missing.

EidClient exposes the following sub-APIs:

Field Class Role
eid.auth AuthApi Start authentication (push / QR)
eid.sign SignApi Start signing (digest)
eid.session SessionApi Poll for the result (long-poll)
eid.validator ResponseValidator Cryptographically verify the response

4. Core flows

4.1 Authentication (push)

Each method on eid.auth internally generates a random rpChallenge and attaches it to the returned session (the validator matches it against the signature). Source: auth.ts.

// РД / civil ID / ETSI — any of them works (the server detects the type)
const s = await eid.auth.notificationByEtsi("PNOMN-111949212017", [
  { type: "displayTextAndPIN", displayText60: "Sign in to Khan Bank" },
]);

showToUser(s.vc);   // ← the verification code (VC) shown on the citizen's phone

const result = await eid.session.waitForResult(s.sessionId);  // long-poll (§4.3)
const who = eid.validator.validateAuth(result, s.rpChallenge); // ← cryptographically verifies

console.log(who.documentNumber, who.subject);  // verified citizen

AuthApi methods:

Method Endpoint Response
notificationByEtsi(id, interactions, opts?) POST /authentication/notification/etsi/{id} NotificationSession
notificationByDocument(documentNumber, interactions, opts?) POST /authentication/notification/document/{id} NotificationSession
deviceLinkAnonymous(interactions, opts?) POST /authentication/device-link/anonymous DeviceLinkSession
deviceLinkByEtsi(id, interactions, opts?) POST /authentication/device-link/etsi/{id} DeviceLinkSession
  • notification (push) — a notification is sent directly to the citizen's phone. NotificationSession returns { sessionId, vc, rpChallenge }.
  • device-link (QR/App2App) — for generating a QR code / deeplink. DeviceLinkSession returns { sessionId, sessionToken, sessionSecret, deviceLinkBase, rpChallenge }.

AuthOptions (auth.ts): certificateLevel (override the default for this session), callbackUrl (same-device App2App return URL — if provided, it is sent as initialCallbackUrl).

4.2 Signing (qualified signature)

The RP sends the SHA-256 digest of its document and verifies the returned signature against that digest + the citizen's cert. Source: sign.ts.

import { sha256Base64 } from "@eid-mongolia/sdk";

const digest = sha256Base64(pdfBytes);   // the document's SHA-256 (base64)

const s = await eid.sign.digestByEtsi("PNOMN-111949212017", digest, [
  { type: "displayTextAndPIN", displayText60: "Sign the loan agreement" },
]);
showToUser(s.vc);

const result = await eid.session.waitForResult(s.sessionId);
const sig = eid.validator.validateSign(result, digest);  // verified against the digest

console.log(sig.signatureValueB64, sig.subject);

SignApi methods:

Method Endpoint Description
digestByEtsi(id, digestB64, interactions, opts?) POST /signature/notification/etsi/{id} With a ready digest (binary document)
digestByDocument(documentNumber, digestB64, interactions, opts?) POST /signature/notification/document/{id} With a ready digest (specific device)
textByEtsi(id, text, interactions, opts?) POST /signature/notification/etsi/{id} Computes the digest of the text internally, then signs

SignOptions: certificateLevel, callbackUrl, hashType (SHA256 default, or SHA384/SHA512 — match it if you prepared the digest yourself).

Note: in the sign flow NotificationSession.rpChallenge is empty ("") — the signature is verified against the digest, not a challenge (sign.ts:54).

4.3 Session poll (long-poll)

SessionApi (session.ts) repeatedly long-polls the RP-API's GET /session/{id}?timeoutMs=.

Method Description
poll(sessionId, serverPollMs = 30000) A single long-poll; waits up to serverPollMs.
waitForResult(sessionId, opts?) Repeatedly polls until COMPLETE. opts = { maxWaitMs?, serverPollMs? }.
  • Default serverPollMs = 30_000, maxWaitMs = 150_000 (cuts off if the citizen does not respond).
  • The HTTP timeout is set 10 seconds longer than the server poll.
  • If waitForResult does not reach COMPLETE before the deadline, it returns the last (RUNNING) result — the validator treats it as an unfinished session and raises ValidationError.

SessionResult fields (types.ts): state, endResult, documentNumber, certificateDerB64, certificateLevel, signatureValueB64, signatureAlgorithm, interactionTypeUsed.


5. Response verification — why the validator is mandatory

⚠️ Do not trust endResult === "OK" on its own. If the RP-API is compromised/proxied, a forged OK may arrive.

ResponseValidator (validator.ts) performs the following steps:

  1. state === COMPLETE && endResult === OK (otherwise SessionFailedError).
  2. Verifies the citizen's certificate by chaining it up to the trust anchor (Gerege Root CA) (the issuer must be a CA, depth ≤ 8).
  3. The certificate is within its validity period (validFrom/validTo, clockSkewMs allowed) + satisfies the required certificateLevel (QUALIFIED default).
  4. Verifies the signature with the citizen's public key:
  5. auth → over the ACSP_V2 payload (LP("ACSP_V2") ‖ LP(rpChallenge) ‖ LP(SPKI))
  6. sign → over the digest supplied by the RP

If any step fails it throws ValidationErrordo not trust the response.

Public methods:

Method Returns Description
validateAuth(result, rpChallengeB64) VerifiedIdentity { documentNumber, certificate, subject, certificateLevel }
validateSign(result, digestB64) VerifiedSignature { documentNumber, signatureValueB64, signatureAlgorithm, certificate, subject }
checkRevocation(cert) Promise<void> OCSP/CRL hook (below).

5.1 TrustConfig settings

Passed via client.trust (validator.ts):

Field Value
trustAnchorsPem Root CA PEMs. REQUIRED in production. If empty, ValidationError (only bypassed with allowUntrusted).
intermediatesPem Intermediate CA PEMs (leaf ↔ root).
requiredLevel Required certLevel (default QUALIFIED).
clockSkewMs Clock skew allowed when checking validity (default 0).
allowUntrusted If true, the chain is not checked — dev/test only.
revocation OCSP/CRL checker hook (RevocationChecker).
revocationMode "hard-fail" (default; rejects unknown) or "soft-fail".

Revocation. If the revocation hook is not configured, the cert's revocation status is not checked — that is the RP's responsibility. If configured, call await eid.validator.checkRevocation(who.certificate) after validateAuth/validateSign (revocation is network I/O, so it is async, while validate is sync).


6. Error handling

Every error inherits from EidError (errors.ts):

Error Meaning Fields
AuthenticationError HTTP 401 — API secret wrong/missing
ForbiddenError HTTP 403 — IP allowlist / mTLS not permitted
ApiError Other HTTP errors (4xx/5xx) .status, .body
NetworkError Timeout / network error
SessionFailedError endResult ≠ OK (TIMEOUT, USER_REFUSED*, DOCUMENT_UNUSABLE, WRONG_VC…) .endResult
ValidationError Cert chain / signature / level check failed
import { SessionFailedError, ValidationError } from "@eid-mongolia/sdk";

try {
  const result = await eid.session.waitForResult(s.sessionId);
  const who = eid.validator.validateAuth(result, s.rpChallenge);
} catch (e) {
  if (e instanceof SessionFailedError) {
    // e.endResult: "USER_REFUSED" | "TIMEOUT" | …  → UI message, retry
  } else if (e instanceof ValidationError) {
    // possibly forged/proxied — do NOT trust the response
  }
  throw e;
}

7. Crypto helpers

Exports built on node:crypto (crypto.ts):

Function Description
sha256Base64(data) SHA-256 digest (base64) of text/bytes — for preparing the sign flow digest.
randomChallenge(bytes = 64) Random RP challenge (base64). Called internally by the auth methods.
buildAcspV2Payload(rpChallengeB64, spkiDER) Assembles the ACSP_V2 payload — used by the validator.

⚠️ The ACSP_V2 payload must be byte-identical across the server (server/internal/crypto/acsp.go), the phone, and the RP SDK. LP(x) = 4-byte big-endian length ‖ x.


8. mTLS (eIDAS qualified environment)

If the production RP-API requires a client cert, provide an undici Agent via dispatcher (http.ts):

import { Agent } from "undici";

const eid = new EidClient({
  baseUrl: "https://rp-api.eidmongolia.mn",
  credentials: { /* … */ },
  trust: { trustAnchorsPem: [ROOT_CA_PEM] },
  dispatcher: new Agent({ connect: { cert: clientCertPem, key: clientKeyPem } }),
});

If dispatcher is provided it is used for every fetch call (Node environment only).


9. Relationship to the raw HTTP version

The SDK follows the raw HTTP contract of docs/RP_INTEGRATION.md exactly — the endpoints, body fields (relyingPartyUUID, relyingPartyName, certificateLevel, signatureProtocol: "ACSP_V2", interactions), and Bearer auth are all identical. To integrate directly over HTTP without the SDK, or to see the full list of endpoints and the citizen identifier numbers:


10. Development

cd sdk/typescript
npm install
npm run lint   # tsc --noEmit
npm test       # build + node --test

Package: sdk/typescript/. TypeScript is the reference implementation — the Go/Python versions follow its API surface, security model (ResponseValidator), and wire contract (sdk/README.md).