Appearance
Kod örnekleri
Aşağıdaki kodlar tamga-network/examples/ klasöründeki gerçek dosyalardır: her test çalıştırmasında gerçek paketlerle (mümkün olanlarda gerçek doğrulayıcıyla) uçtan uca denenir. Sayfadaki kopyala düğmesiyle alıp kullanabilirsiniz.
Kurulum
sh
npm install @tamga-network/verifier @tamga-network/trust @tamga-network/issuersh
pnpm add @tamga-network/verifier @tamga-network/trust @tamga-network/issuersh
yarn add @tamga-network/verifier @tamga-network/trust @tamga-network/issuerPaketler npm'e yakında
Paketler yazıldı ve test edildi; @tamga-network organizasyonu açılınca yayınlanacak. O zamana kadar kaynak depodan kullanılabilir (npm run release:check yayına hazır paketleri .publish/ klasöründe üretir).
| Ne yapmak istiyorsunuz? | Paket | İçe aktarma |
|---|---|---|
| Web sitenize TamgaID ile giriş | @tamga-network/verifier (+ sayfa kiti /web) | import { createRpAssertion } from "@tamga-network/verifier" |
| Kendi sunucunuzda belge doğrulamak | @tamga-network/verifier, @tamga-network/trust | import { verifyPresentation } from "@tamga-network/verifier" |
| Kurum olarak belge vermek | @tamga-network/issuer | import { createIssuerClient } from "@tamga-network/issuer/client" |
| Kurumun yetkisini sorgulamak | @tamga-network/trust | import { fetchListTrustSource } from "@tamga-network/trust" |
1. Web sitesine "TamgaID ile giriş"
Sunumu sitenizin sunucusu açar (güven listesindeki anahtarınızla imzalı kısa ömürlü beyanla); sayfa yalnızca QR'ı gösterir ve durumu izler; onaylanan değerler sunucunuza bir kez verilir (ADR-0017). Ayrıntı: GUIDE-0001.
ts
/**
* 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 };
}html
<!-- Example 01 — the page. The kit draws the QR code (or "Open in Tamga Wallet" on a phone) and polls
a status token. It never sees the person's data and never decides: your server does (server.ts). -->
<script src="https://verify.tamga.network/tamga-verifier.js"></script>
<div id="tamga"></div>
<script>
const post = (url, body) =>
fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
TamgaVerifier.mount(document.getElementById("tamga"), {
verifier: "https://verify.tamga.network",
policy: "site-uyelik", // sign-in for an existing account: "site-giris"
start: () => post("/tamga/start", { policy: "site-uyelik" }).then((r) => r.json()),
onResult: (presentationId) =>
post("/tamga/session", { presentation_id: presentationId }).then(() => location.reload()),
onError: (e) => console.warn("Tamga:", e),
});
</script>2. Kendi sunucunuzda belge doğrulama
Barındırılan doğrulayıcı olmadan: güven listelerini doğrular, iptal listelerini önceden çeker, imzalı istek üretir, şifreli cevabı çözer ve kanonik denetimi (T0 + A–E) çalıştırır. Sonuç üç değerlidir: ACCEPTED, REJECTED, INDETERMINATE ("şu an denetlenemedi" — belge geçersiz demek değildir). Ayrıntı: GUIDE-0002.
ts
/**
* 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. Kurum olarak belge vermek (barındırılan servis)
Kurumunuza Tamga operatörünün verdiği kapsamlı API anahtarıyla (ADR-0016). Teklif bağlantısı QR olarak gösterilir; PIN ayrı bir kanaldan verilir, bağlantının içinde asla gitmez. Ayrıntı: GUIDE-0003.
ts
/**
* 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. Kurum yetkisini sorgulamak
Bir kurum güven listesinde kayıtlı mı, etkin mi, bu belge türünü vermeye yetkili mi? Kişisel veri içermez.
ts
/**
* 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,
};
}Örnekleri çalıştırmak
sh
git clone https://github.com/tamga-network/tamga-network && cd tamga-network
npm install && npm run d1 # geliştirme PKI'si + güven listeleri
npx vitest run examples # dört örnek, gerçek paketlerle