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 onnode:crypto, so it is not a browser bundle.
Source: sdk/typescript/src/
1. Installation¶
- Node.js ≥ 18 (global
fetch,node:crypto). - Ships both ESM and CommonJS (
import/require).
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.mn → Relying 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.ts — RpCredentials.
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.
NotificationSessionreturns{ sessionId, vc, rpChallenge }. - device-link (QR/App2App) — for generating a QR code / deeplink.
DeviceLinkSessionreturns{ 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.rpChallengeis 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
waitForResultdoes not reachCOMPLETEbefore the deadline, it returns the last (RUNNING) result — the validator treats it as an unfinished session and raisesValidationError.
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:
state === COMPLETE && endResult === OK(otherwiseSessionFailedError).- Verifies the citizen's certificate by chaining it up to the trust anchor (Gerege Root CA) (the issuer must be a CA, depth ≤ 8).
- The certificate is within its validity period (
validFrom/validTo,clockSkewMsallowed) + satisfies the requiredcertificateLevel(QUALIFIEDdefault). - Verifies the signature with the citizen's public key:
- auth → over the ACSP_V2 payload (
LP("ACSP_V2") ‖ LP(rpChallenge) ‖ LP(SPKI)) - sign → over the digest supplied by the RP
If any step fails it throws ValidationError — do 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
revocationhook is not configured, the cert's revocation status is not checked — that is the RP's responsibility. If configured, callawait eid.validator.checkRevocation(who.certificate)aftervalidateAuth/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:
- RP integration guide (raw HTTP)
- A working example RP client:
web/src/lib/rpclient.ts
10. Development¶
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).