JavaScript SDK
packages/sdk is a small TypeScript client for the REST API. Its one promise: it returns data only after the answer's signature, signer, deployment, time window, data age and nonce have all been checked against what the caller pinned and sent. If any check fails, it throws and returns nothing.
The package is @certichain/sdk version 0.1.0, marked private and not on npm. The name is a placeholder; the name and publishing are the project owner's decision. Inside the repository it is built with npm run sdk:build and tested with npm run sdk:test.
| Module format | ESM only |
| Runtime | Node.js 20 or later, or any runtime with fetch and Web Crypto |
| Dependencies | ethers 6.14.4 and canonicalize 2.1.0, pinned exactly |
Quick start
import { createClient, UntrustedAnswerError } from "@certichain/sdk";
const client = createClient({
baseUrl: "https://certichain.example",
signer: "0x6666666666666666666666666666666666666666", // pinned, from the deployment owner
});
try {
const { data, standing, evidence } = await client.getCredential("0x3333333333333333333333333333333333333333333333333333333333333333");
if (standing.good) {
// found, active, not expired, issuer registered and not suspended
} else {
console.log(standing.reasons); // for example ["status-revoked"]
}
// keep `evidence` (the exact signed answer) if you need a record of what the chain held
} catch (error) {
if (error instanceof UntrustedAnswerError) console.error(`answer refused: ${error.reason}`);
else throw error;
}
Options
| Option | Required | Default | Rule |
|---|---|---|---|
baseUrl | yes | https, or http only for localhost, 127.0.0.1 and [::1]. No credentials, query or fragment. | |
signer | yes | The API signer address you pinned. Without it an answer proves nothing. | |
chainId | no | Decimal string. Pin it when one signer could serve several deployments. | |
registry | no | One CredentialRegistry address, or a non-empty list (one per contract set) | |
apiKey | no | Printable text, no spaces, up to 512 characters. Sent as a bearer token to baseUrl only. | |
maxAnswerAgeSeconds | no | 300 | Refuse answers generated longer ago (max 86,400) |
maxDataAgeSeconds | no | 60 | Refuse answers whose chain data may be older (max 3,600) |
timeoutMs | no | 10,000 | Per request |
maxResponseBytes | no | 1,048,576 | Larger answers are cut off with a NetworkError |
fetch, now | no | global fetch, system clock | For tests and special runtimes |
Bad options throw ConfigError from createClient.
Calls
| Call | Endpoint | Returns | Extra checks |
|---|---|---|---|
getCredential(id) | GET /credentials/{id} | { data, standing, evidence } | id must be 0x plus 64 hex; the answer's id must match |
verifyFile(text) | POST /verify | { data, standing, evidence } | 1 to 65,536 bytes; data.fileSha256 must equal the SHA-256 of text, else UntrustedAnswerError("file") |
getStatus() | GET /status | { data, evidence } | The answer's signer, chain and registry must match its meta and your pins |
Each call makes a fresh 32-byte nonce. Requests use redirect: "error", credentials: "omit" and cache: "no-store".
Checks on every answer
verifyEnvelope (also exported for callers who fetch answers themselves) runs, in order:
| Step | Reason on failure |
|---|---|
Exact envelope shape, times in YYYY-MM-DDTHH:MM:SSZ form | shape |
apiVersion is "1" | version |
Signature recovers to the address the answer names (65 bytes, last byte 1b or 1c) | signature |
| That address is the pinned signer | signer |
| Chain and registry match the pins | chain, registry |
Not past validUntil, not more than 60 seconds in the future, not older than maxAnswerAgeSeconds | stale |
dataMaxAgeSeconds within maxDataAgeSeconds | data-age |
| The nonce is echoed | replayed |
The returned envelope is rebuilt from the signed parts only, so fields an intermediary added never reach your code. Details of the format: API envelope.
Standing
credentialStanding(data, now) and fileStanding(verdict) turn data into { good, reasons }:
| Function | Reasons |
|---|---|
credentialStanding | not-found, issuer-not-registered, status-suspended, status-revoked, status-superseded, expired, issuer-suspended |
fileStanding | other-chain, not-found, anchor-mismatch, issuer-not-registered, signature-invalid, not-active, file-differs-from-claims, id-mismatch, expired, standard-proof-invalid, or not-fully-verified when no single check is named |
credentialStanding also counts an expiry that fell after the answer was made when now is given (the client passes it). A file is good only when the server reports fullyVerified.
Errors
All errors extend CertichainError.
| Error | When | Useful fields |
|---|---|---|
ConfigError | Bad options | |
ArgumentError | Bad call argument; nothing was sent | |
ApiError | The API answered with an error | code, status, serverMessage (control characters removed, at most 200 characters, the API key redacted), retryAfterSeconds (only a plain 1 to 5 digit value) |
NetworkError | No answer, timeout, or answer too large | kind: transport, timeout or too-large; causeName |
UntrustedAnswerError | An answer failed a check; no data returned | reason: shape, version, signature, signer, chain, registry, stale, data-age, replayed or file |
An error body that does not match the documented shape becomes ApiError with code UNEXPECTED_RESPONSE.
Example script
packages/sdk/examples/verify-credential.mjs checks one credential, and optionally a file:
CERTICHAIN_URL=https://certichain.example CERTICHAIN_SIGNER=0x6666666666666666666666666666666666666666 \
node examples/verify-credential.mjs <credential id> [credential-file.json]
CERTICHAIN_API_KEY is optional. Exit codes: 0 good standing, 2 not in good standing, 1 the answer was not trusted or the call failed.
Source files
packages/sdk/src/(client.ts,options.ts,transport.ts,envelope.ts,canonical.ts,data.ts,standing.ts,errors.ts,types.ts,index.ts)packages/sdk/examples/verify-credential.mjspackages/sdk/README.md
Tests
packages/sdk/test/(boundaries,client,envelope,example,openapi-binding,standing)- The
sdkCI job; see Interop and SDK tests - E2E:
e2e/journeys/verification-api.journey.tsruns the SDK against the real handlers