Işläp düzüjiler
Tamga bilen düz
Tamga-a üç görnüşli gurama birikýär: web saýtlar, barlaýjylar we resminama berýän guramalar. Her bölek açyk çeşmelidir we EUDI profillerine eýerýär; düzen zadyň Tamga-a bagly galmaýar.
Näme etmek isleýärsiň?
| Maksat | Paket |
|---|---|
| Web saýta “TamgaID bilen hasaba dur / gir” goşmak · gollanma | @tamga-network/verifier/web + your server |
| Serweriňde resminama barlamak (işe almak, kampus, ýaş) | @tamga-network/verifier |
| Guramaňyň resminamalaryny adamlaryň gapjygyna bermek | @tamga-network/issuer/client (hosted) · @tamga-network/issuer (own service) |
Paketler
Ähli paketler Apache-2.0 ygtyýarnamalydyr we @tamga-network çäginde çap edilýär. Häzirki wersiýa: 0.1.0 (deslapky) — 1.0-a çenli interfeýs üýtgäp biler.
| Paket | Näme edýär |
|---|---|
@tamga-network/core | heşler, belgi almak, sertifikat kömekçileri |
@tamga-network/trust | gol çekilen ynam sanawlaryny ýükleýär we barlaýar; bir okamak interfeýsi (geljekde zynjyr üçin hem) |
@tamga-network/schemas | resminama görnüşleriniň katalogy: görnüş kesgitlemesi, JSON Schema, mazmun heşleri |
@tamga-network/sd-jwt | SD-JWT VC: saýlama açyklama, enjam baglanyşygy, görnüş barlaglary |
@tamga-network/mdoc | ISO 18013-5 mdoc: CBOR, COSE, bermek we barlamak |
@tamga-network/issuer | resminama fabrigi, OpenID4VCI kömekçileri, ýatyrylyş sanawyny çap ediji; ýerleşdirilen hyzmat üçin /client |
@tamga-network/verifier | barlag hatary (T0 + A–E), üç netije, OpenID4VP haýyşlary; sahypa toplumy üçin /web |
@tamga-network/wallet-core | Node we React Native üçin gapjyk ýadrosy: açarlar, almak, ýerli barlag, hödürlemek |
Häzirki ýagdaý — dogruçyl
| Bölek | Ýagdaý | Bellik |
|---|---|---|
| Paketler | npm-de (0.1.0) | deslapky wersiýa; 1.0-a çenli interfeýs üýtgäp biler |
| Ýerleşdirilen barlaýjy | işleýär | netije diňe hödürlemäni açan saýta (gol çekilen beýan), bahalar bir gezek; syýasatlar häzirlikçe hemişelik |
| Ýerleşdirilen resminama beriş hyzmaty | işleýär | gurama başyna çäkli, möhletli we ýatyrylyp bilinýän API açary |
| Ynam sanawyna hasaba alyş | işleýär | haýyş boýunça Tamga operatory edýär |
Kod mysallary
Paketleri guruň, soňra dört işleýän mysalyň birinden başlaň. Bu, ammaryň examples bukjasyndaky hakyky koddyr: her synag işledilende hakyky paketler bilen barlanýar, olardan aýrylyp bilmeýär. Paketler entek npm-de ýok; oňa çenli çeşme ammaryndan ulanyp bolýar.
npm install @tamga-network/verifier @tamga-network/trust @tamga-network/issuer
1 · Web saýta “TamgaID bilen gir”
Hödürlemäni serweriňiz ynam sanawyndaky açaryňyz bilen gol çekilen gysga möhletli beýan bilen açýar; sahypa diňe QR-y görkezýär; tassyklanan bahalar serweriňize bir gezek berilýär.
/**
* Example 01 — "Sign up / Sign in with TamgaID" 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, "x509_san_dns:example.com"). */
rp: RpSigner;
/** Your own secret; account keys are derived from it (never store the raw document value). */
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-uyelik" | "site-giris"): 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> };
// Keep your own keyed hash as the account key; the raw value is the same for every site.
const accountKey = createHmac("sha256", cfg.siteSecret)
.update(String(claims.document_number_hash))
.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 serweriňizde resminama barlamak
Ýerleşdirilen barlaýjysyz: ynam sanawlaryny barlaýar, ýatyrylyş sanawlaryny öňünden alýar, haýyşa gol çekýär, şifrlenen jogaby açýar we hatary (T0 + A–E) işledýär. INDETERMINATE “häzir barlap bolmady” diýmekdir, “nädogry” däl.
/**
* 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 → show it as a QR code.
* 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, "x509_san_dns:example.com"). */
signer: RpSigner;
/** Your public base URL; the wallet fetches /vp/req/:id and posts to /vp/response here. */
publicBase: string;
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));
const pending = new Map<string, { req: PresentationRequest; policy: Policy }>();
/** 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],
});
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) => pending.get(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 = pending.get(String(kid).replace(/^enc-/, ""));
if (!entry) throw new Error("unknown 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 · Gurama hökmünde resminama bermek
Ýerleşdirilen beriş hyzmatynda guramaňyzyň çäkli API açary bilen. Teklip salgysy QR bolýar; PIN başga kanaldan gidýär, salgynyň içinde hiç haçan gitmeýär.
/**
* 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 · Guramany barlamak
Hasaba alnanmy, işjeňmi, bu resminama görnüşine ygtyýarlymy? Diňe gol çekilen ynam sanawlaryny okaýar — şahsy maglumat ýok.
/**
* 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,
};
}Her integrasiýada düzgünler
- Diňe zerur meýdanlary sora — hasaba alnan çägiňden çykýan haýyş gapjyk tarapyndan ret edilýär.
- INDETERMINATE netijäni “gaýtadan synanyş” diýip düşün, asla “nädogry” diýip däl.
- Şahsy maglumaty asla žurnala ýazma — at, şahsyýet belgisi, passkey belgisi ýok.
- Karary serwerde ber. Brauzer kody ulanyjy tarapyndan üýtgedilip bilner.
Işläp düzüjiler üçin doly resminamalar — gollanmalar, spesifikasiýalar, kararlar: docs.tamga.network. Rollar we gatnaşyjy düzgünleri: Tamga ARF. Esas: arhitektura · ynam sanawlary · Tamga we EUDI.