Skip to content
GUIDE-0004Rehber · IntegrationTaslaksürüm 0.1.02026-09-27T00:00:00.000Z

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/issuer
sh
pnpm add @tamga-network/verifier @tamga-network/trust @tamga-network/issuer
sh
yarn add @tamga-network/verifier @tamga-network/trust @tamga-network/issuer

Paketler 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/trustimport { verifyPresentation } from "@tamga-network/verifier"
Kurum olarak belge vermek@tamga-network/issuerimport { createIssuerClient } from "@tamga-network/issuer/client"
Kurumun yetkisini sorgulamak@tamga-network/trustimport { 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