SDK
SDK
Şəbəkədəki hər iş üçün açıq mənbəli TypeScript paketləri: yoxlama, rəqəmsal sənəd vermə, etibar siyahıları və rəqəmsal cüzdanın nüvəsi. Apache-2.0, EUDI profillərinə uyğun.
Quraşdırma
Paketlər npm-də 0.7.0 sınaq versiyası kimi dərc olunur; sabit 1.0.0 versiyası hər şey hazır olduqda çıxacaq. Sınaq versiyalarında API dəyişə bilər.
npm install @tamga-network/verifier @tamga-network/trust @tamga-network/issuerPaketlər
Hər paketin adı onun npm səhifəsini açır; “Sənədlər” isə tərtibatçı sənədlərindəki arayışını açır. Göstərilən versiya (0.7.0) cari sınaq versiyasıdır.
imzalanmış etibar siyahılarını yükləyir və yoxlayır; vahid oxuma interfeysi (gələcəkdə zəncir üçün də)
Sənədlərsənəd növləri kataloqu: növ metaməlumatları, JSON Schema, məzmun heşləri
SənədlərSD-JWT VC: seçici paylaşım (selective disclosure), cihaza bağlama, format yoxlamaları
Sənədlərrəqəmsal sənəd fabriki, OpenID4VCI köməkçiləri, ləğv siyahısının dərc edicisi; yerləşdirilmiş xidmət üçün /client
Sənədləryoxlama xətti (T0 + A–E), üç nəticə, OpenID4VP sorğuları; səhifə dəsti üçün /web; sıfır bilikli yaş sübutları üçün /zk
SənədlərNode və React Native üçün rəqəmsal cüzdan nüvəsi: açarlar, qəbul etmə, yerli yoxlamalar, təqdim etmə
Sənədlərcüzdan tərəfində sıfır bilikli sübut generatoru (ISO mdoc, ilk istifadə: 18 yaşdan yuxarı): Android yerli generatoru hazırdır, iOS gözlənilir
Sənədlərİşlək nümunələr
Repozitoriyanın examples qovluğundakı real kod: hər test işə salınanda paketlərlə birlikdə icra olunur, buna görə də onlardan ayrı düşə bilməz.
1 · Veb saytda “Tamga ilə daxil olun”
Təqdimatı serveriniz etibar siyahısındakı açarınızla imzalanmış qısamüddətli bəyanla açır; səhifə yalnız QR kodu göstərir; təsdiqlənmiş dəyərlər serverinizə bir dəfə ötürülür.
/**
* Example 01 — "Sign up / Sign in with Tamga" on your website, using the hosted verifier.
*
* Your SERVER opens each presentation, proving who it is with a short-lived assertion signed by the key of
* your trust-list registration (ADR-0017). The page only shows the QR code and polls a status token; the
* values the person approved are handed to your server once.
*
* npm install @tamga-network/verifier
*/
import { createHmac } from "node:crypto";
import { createRpAssertion, type RpSigner } from "@tamga-network/verifier";
export interface SignInConfig {
/** The hosted verifier, e.g. "https://verify.tamga.network". */
verifier: string;
/** Your registration: pemRpSigner(KEY_PEM, CERT_PEM) — client_id is x509_hash of your access certificate (HAIP 1.0). */
rp: RpSigner;
/** Your own secret; the stored account key is a keyed hash of the site pseudonym. */
siteSecret: string;
fetch?: typeof fetch;
}
/** What your page needs to draw the QR code and poll the status. */
export interface StartedPresentation {
presentation_id: string;
qr_payload: string;
expires_at: string;
status_token: string;
}
export type SignInResult =
| { ok: true; accountKey: string; givenName?: string; familyName?: string }
| { ok: false; outcome: "PENDING" | "REJECTED" | "INDETERMINATE" | "ALREADY_USED" };
export function tamgaSignIn(cfg: SignInConfig) {
const f = cfg.fetch ?? fetch;
const auth = async () => ({ authorization: `Bearer ${await createRpAssertion(cfg.rp, cfg.verifier)}` });
/** POST /tamga/start → return this JSON to your page (TamgaVerifier.mount({ start })). */
async function start(policy: "site-signup" | "site-signin"): Promise<StartedPresentation> {
const r = await f(`${cfg.verifier}/presentations`, {
method: "POST",
headers: { ...(await auth()), "content-type": "application/json", accept: "application/json" },
body: JSON.stringify({ policy_id: policy }),
});
if (!r.ok) throw new Error(`verifier refused the request (${r.status})`);
return (await r.json()) as StartedPresentation;
}
/** POST /tamga/session → your page sends the presentation id after ACCEPTED; decide here, on the server. */
async function finish(presentationId: string): Promise<SignInResult> {
const id = encodeURIComponent(presentationId);
const result = (await (await f(`${cfg.verifier}/presentations/${id}`, { headers: await auth() })).json()) as {
outcome?: "ACCEPTED" | "REJECTED" | "INDETERMINATE";
};
if (result.outcome !== "ACCEPTED") return { ok: false, outcome: result.outcome ?? "PENDING" };
const r = await f(`${cfg.verifier}/presentations/${id}/claims`, { headers: await auth() });
if (r.status === 410) return { ok: false, outcome: "ALREADY_USED" }; // values are handed out once
const { claims } = (await r.json()) as { claims: Record<string, unknown> };
// ADR-0031: the account key is the wallet's pseudonym for YOUR site (another site sees a different one); the verifier
// has checked its signature, audience, nonce and the wallet instance attestation. No document value is sent.
if (typeof claims.pseudonym !== "string") return { ok: false, outcome: "REJECTED" };
const accountKey = createHmac("sha256", cfg.siteSecret).update(claims.pseudonym).digest("base64url");
return {
ok: true,
accountKey,
givenName: claims.given_name as string | undefined,
familyName: claims.family_name as string | undefined,
};
}
return { start, finish };
}2 · Öz serverinizdə sənədləri yoxlayın
Yerləşdirilmiş yoxlayıcı olmadan: etibar siyahılarını yoxlayır, ləğv siyahılarını əvvəlcədən yükləyir, sorğunu imzalayır, şifrələnmiş cavabı açır və xətti (T0 + A–E) işə salır. INDETERMINATE “hazırda yoxlamaq mümkün olmadı” deməkdir, heç vaxt “etibarsız” demək deyil.
/**
* Example 02 — verify documents on your own server (hiring, campus, age checks), without the hosted verifier.
*
* 1. Load the signed trust lists (who may issue what) and pre-fetch the revocation lists.
* 2. Create a signed OpenID4VP request with your registration certificate → show it as a QR code. Tamga Wallet refuses a
* request that carries no registration certificate (from packages 0.7.0), so fetch yours from the trust lists.
* 3. The wallet posts an encrypted answer → decrypt it → run the canonical checks (T0 + A–E).
* Three outcomes: ACCEPTED, REJECTED (with the failing step) or INDETERMINATE ("could not check right now").
*
* npm install @tamga-network/verifier @tamga-network/trust @tamga-network/core
*/
import { pemToDer } from "@tamga-network/core";
import { fetchListTrustSource, verifyJws } from "@tamga-network/trust";
import {
createPresentationRequest,
dcqlFromPolicy,
decryptResponse,
PrefetchStatusCache,
verifyPresentation,
type Policy,
type PresentationRequest,
type RpSigner,
} from "@tamga-network/verifier";
/** A hiring policy: a bachelor's diploma, four fields, nothing else. */
export const DIPLOMA_POLICY: Policy = {
policy_id: "hiring-bachelor",
purpose: { "en-GB": "Confirm graduation for a job application" },
credentials: [
{
id: "diploma",
vct_values: ["urn:tamga:edu:DiplomaCredential:1"],
required_claims: ["is_graduate", "qualification_title", "eqf_level", "awarding_body_name"],
constraints: { is_graduate: true, eqf_level: { min: 6 } },
},
],
trust: { min_issuer_assurance: "I2", allowed_categories: ["EDUCATION"], require_recognition: true, state_code: "TR" },
freshness: { max_status_token_age_sec: 7200, max_trust_age_sec: 86400 },
};
export interface OwnVerifierConfig {
/** Where the trust lists are published, e.g. "https://trust.tamga.network". */
trustBase: string;
/** Root fingerprints, fixed in your configuration (published at tamga.network/trust-anchor). */
rootFingerprints: string[];
/** Your registration: pemRpSigner(KEY_PEM, CERT_PEM) — client_id is x509_hash of your access certificate (HAIP 1.0). */
signer: RpSigner;
/** Your public base URL; the wallet fetches /vp/req/:id and posts to /vp/response here. */
publicBase: string;
/**
* The registered use (`scope_id` of your trust-list record) this verifier asks for, e.g. "job-application-1". Its
* registration certificate (`<trustBase>/wrprc/…`) goes into every request (`verifier_info`). "none" only for wallets and
* tests that do not check registration certificates.
*/
registrationScope: string | "none";
fetch?: typeof fetch;
/** Revocation-list fetcher (defaults to fetch with a timeout). */
fetchStatus?: (url: string) => Promise<string | null>;
anchorMaxAgeMs?: number;
}
export async function createOwnVerifier(cfg: OwnVerifierConfig) {
const f = cfg.fetch ?? fetch;
const http = async (url: string) => {
const r = await f(url);
return { status: r.status, text: () => r.text() };
};
// 1) Trust lists, verified against your fixed root fingerprints; anchors included (revocation checks need them).
const { source: trust, store } = await fetchListTrustSource(cfg.trustBase, http, {
rootFingerprints: cfg.rootFingerprints,
verifyJws,
anchors: true,
anchorMaxAgeMs: cfg.anchorMaxAgeMs,
});
const rootCertsDer = [...store.root_cas.values()].flatMap((ca) => (ca.cert_pem ? [pemToDer(ca.cert_pem)] : []));
// Pre-fetch every anchored revocation list: no network call at verification time.
const statusCache = new PrefetchStatusCache(cfg.fetchStatus);
await statusCache.refresh([...store.status_anchors.values()].map((a) => a.list_uri));
// Open requests, in memory. Each expires with its request object (expiresAt) and the map is capped, so abandoned QR codes
// cannot grow it without limit. A multi-instance deployment keeps these in a shared store instead.
const MAX_PENDING = 10_000;
const pending = new Map<string, { req: PresentationRequest; policy: Policy }>();
const sweep = (now = Math.floor(Date.now() / 1000)) => {
for (const [id, e] of pending) if (e.req.expiresAt < now) pending.delete(id);
for (const id of pending.keys()) {
if (pending.size < MAX_PENDING) break;
pending.delete(id); // oldest first
}
};
const live = (id: string) => {
const e = pending.get(id);
if (e && e.req.expiresAt < Math.floor(Date.now() / 1000)) {
pending.delete(id);
return undefined;
}
return e;
};
// Your registration certificate: published with the trust lists, renewed by the registrar (at most 12 months); re-read hourly.
let registration: { jwt: string; at: number } | null = null;
async function registrationCert(): Promise<string | undefined> {
if (cfg.registrationScope === "none") return undefined;
if (registration && Date.now() - registration.at < 3600_000) return registration.jwt;
const index = JSON.parse(await (await http(`${cfg.trustBase}/wrprc/index.json`)).text()) as {
items: Array<{ kind: string; client_id?: string; scope_id?: string; path: string }>;
};
const item = index.items.find(
(i) => i.kind === "rp" && i.client_id === cfg.signer.clientId && i.scope_id === cfg.registrationScope,
);
if (!item)
throw new Error(
`no registration certificate for ${cfg.signer.clientId} / ${cfg.registrationScope}: wallets will refuse the request — check your registration (identity number in the access certificate, scope still valid)`,
);
registration = { jwt: (await (await http(`${cfg.trustBase}/${item.path}`)).text()).trim(), at: Date.now() };
return registration.jwt;
}
/** 2) Start: returns the QR payload. Serve `requestObject(id)` at GET /vp/req/:id. */
async function start(policy: Policy = DIPLOMA_POLICY) {
const req = await createPresentationRequest({
signer: cfg.signer,
dcql: dcqlFromPolicy(policy),
responseUri: `${cfg.publicBase}/vp/response`,
requestUriBase: `${cfg.publicBase}/vp/req`,
purpose: Object.values(policy.purpose)[0],
registrationCert: await registrationCert(),
});
sweep();
pending.set(req.presentationId, { req, policy });
return { presentationId: req.presentationId, qrPayload: req.qrPayload };
}
/** GET /vp/req/:id → body with content-type application/oauth-authz-req+jwt. */
const requestObject = (id: string) => live(id)?.req.requestJwt ?? null;
/** 3) POST /vp/response (form field `response`) → verify. Each request answers once. */
async function handleResponse(jwe: string) {
const kid = JSON.parse(Buffer.from(jwe.split(".")[0], "base64url").toString("utf8")).kid as string;
const entry = live(String(kid).replace(/^enc-/, ""));
if (!entry) throw new Error("unknown, expired or already answered request");
pending.delete(entry.req.presentationId);
const answer = await decryptResponse(jwe, entry.req.encPrivateKey);
if (answer.state !== entry.req.state) throw new Error("state mismatch");
const pc = entry.policy.credentials[0];
return verifyPresentation({
presentation: answer.vp_token[pc.id][0],
aud: cfg.signer.clientId,
nonce: entry.req.nonce,
policy: entry.policy,
policyCredentialId: pc.id,
trust,
statusCache,
rootCertsDer,
rp: trust.relyingParty(cfg.signer.clientId),
}); // → { result: { outcome, failed_step, … }, claims } — store names, never values, in your logs
}
/** Refresh revocation lists on a timer (e.g. every few minutes). */
const refreshStatus = () => statusCache.refresh([...store.status_anchors.values()].map((a) => a.list_uri));
return { start, requestObject, handleResponse, refreshStatus, trust, statusCache };
}3 · Qurum olaraq sənəd verin
Yerləşdirilmiş sənəd vermə xidmətində qurumunuzun məhdud səlahiyyətli API açarı ilə. Təklif keçidi QR koda çevrilir; PIN ayrı kanalla göndərilir, heç vaxt keçidin içində olmur.
/**
* Example 03 — issue documents from your own system, using Tamga's hosted issuing service.
*
* Your institution gets a scoped API key from the Tamga operator (ADR-0016). Your server calls the service;
* the person scans the returned link as a QR code and types the PIN you show them separately.
*
* npm install @tamga-network/issuer
*/
import { createIssuerClient, IssuerClientError } from "@tamga-network/issuer/client";
export function institution(cfg: { apiKey: string; slug: string; baseUrl?: string; fetch?: typeof fetch }) {
const tamga = createIssuerClient({
baseUrl: cfg.baseUrl ?? "https://issuer.tamga.network",
slug: cfg.slug,
apiKey: cfg.apiKey, // tmg_<slug>_… — server side only
fetch: cfg.fetch,
});
return {
/** A university: offer a diploma to a graduate in your records (your own student number). */
async offerDiploma(studentNo: string) {
const offer = await tamga.createOffer({ subjectId: studentNo, vct: "urn:tamga:edu:DiplomaCredential:1" });
// offer.deepLink → show as a QR code or an "Add to wallet" link.
// offer.txCode → show on a DIFFERENT channel than the link (screen, SMS, e-mail): never inside the QR.
return offer;
},
/** A ticket seller: sell a ticket straight into the buyer's wallet (the ticket carries no personal data). */
async sellTicket(eventId: string, ticketClass: string) {
const sale = await tamga.sellTicket({ eventId, ticketClass });
return { ticketId: sale.ticketId, qr: sale.offer.deepLink, pin: sale.offer.txCode };
},
/** Revoke (final) or suspend (reversible); verifiers see it at the next fixed publication. */
async cancel(credentialId: string, reason: string) {
try {
return await tamga.revoke(credentialId, reason);
} catch (e) {
if (e instanceof IssuerClientError && e.status === 404) return null; // unknown credential
throw e;
}
},
};
}4 · Qurumu yoxlayın
Qeydiyyatdadırmı, aktivdirmi, bu sənəd növü üçün səlahiyyəti varmı? Yalnız imzalanmış etibar siyahılarını oxuyur — fərdi məlumatlar yoxdur.
/**
* Example 04 — ask the trust lists: is this institution registered, active, and authorized for this document?
* Useful for a directory page, an onboarding screen or a back-office check. No personal data involved.
*
* npm install @tamga-network/trust
*/
import { fetchListTrustSource, verifyJws } from "@tamga-network/trust";
export async function institutionStatus(
trustBase: string,
rootFingerprints: string[],
slug: string,
vct: string,
opts: { fetch?: typeof fetch; anchorMaxAgeMs?: number } = {},
) {
const f = opts.fetch ?? fetch;
const { source } = await fetchListTrustSource(
trustBase,
async (url) => {
const r = await f(url);
return { status: r.status, text: () => r.text() };
},
{ rootFingerprints, verifyJws },
);
const issuer = source.issuers().find((i) => i.slug === slug);
if (!issuer) return { registered: false as const };
const now = Date.now();
const authorized = issuer.schema_authorizations.some(
(a) =>
a.vct === vct &&
a.allowed &&
new Date(a.valid_from).getTime() <= now &&
(!a.valid_until || now < new Date(a.valid_until).getTime()),
);
return {
registered: true as const,
name: issuer.legal_name,
category: issuer.category,
status: issuer.status, // ACTIVE | SUSPENDED | REVOKED | RETIRED
authorized,
};
}