Geliştiriciler

Tamga ile geliştir

Tamga’ya üç tür kurum bağlanır: web siteleri, doğrulayıcılar ve belge veren kurumlar. Her parça açık kaynaktır ve EUDI profillerini izler; yaptığın şey Tamga’ya bağımlı kalmaz.

Ne yapmak istiyorsun?

AmaçPaket
Web sitesine “TamgaID ile Kayıt Ol / Giriş Yap” eklemek · kılavuz@tamga-network/verifier/web + your server
Sunucunda belge doğrulamak (işe alım, kampüs, yaş)@tamga-network/verifier
Kurumunun belgelerini kişilerin cüzdanına vermek@tamga-network/issuer/client (hosted) · @tamga-network/issuer (own service)

Paketler

Tüm paketler Apache-2.0 lisanslıdır ve @tamga-network kapsamında yayınlanır. Güncel sürüm: 0.1.0 (ön sürüm) — 1.0'a kadar arayüz değişebilir.

PaketNe yapar
@tamga-network/coreözetler, kimlik türetme, sertifika yardımcıları
@tamga-network/trustimzalı güven listelerini yükler ve doğrular; tek okuma arayüzü (ileride zincir için de)
@tamga-network/schemasbelge tipi kataloğu: tip tanımı, JSON Schema, içerik özetleri
@tamga-network/sd-jwtSD-JWT VC: seçici açıklama, cihaz bağı, biçim denetimleri
@tamga-network/mdocISO 18013-5 mdoc: CBOR, COSE, verme ve doğrulama
@tamga-network/issuerbelge fabrikası, OpenID4VCI yardımcıları, iptal listesi yayıncısı; barındırılan servis için /client
@tamga-network/verifierdoğrulama hattı (T0 + A–E), üç sonuç, OpenID4VP istekleri; sayfa kiti için /web
@tamga-network/wallet-coreNode ve React Native için cüzdan çekirdeği: anahtarlar, alma, yerel denetim, sunma

Bugünkü durum — dürüstçe

ParçaDurumNot
Paketlernpm'de (0.1.0)ön sürüm; 1.0'a kadar arayüz değişebilir
Barındırılan doğrulayıcıçalışıyorsonuç yalnızca sunumu açan siteye (imzalı beyan), değerler bir kez; politikalar şimdilik sabit
Barındırılan belge verme servisiçalışıyorkurum başına kapsamlı, süreli ve iptal edilebilir API anahtarı
Güven listesi kaydıçalışıyoristek üzerine Tamga operatörü yapar

Kod örnekleri

Paketleri kurun, sonra dört çalışan örnekten birinden başlayın. Bu, deponun examples klasöründeki gerçek koddur: her test çalıştırmasında gerçek paketlerle denenir, paketlerden kopamaz. Paketler henüz npm’de değil; o zamana kadar kaynak depodan kullanılabilir.

npm install @tamga-network/verifier @tamga-network/trust @tamga-network/issuer

1 · Web sitesine “TamgaID ile giriş”

Sunumu sunucunuz, güven listesindeki anahtarınızla imzalı kısa ömürlü bir beyanla açar; sayfa yalnızca QR'ı gösterir; onaylanan değerler sunucunuza bir kez verilir.

/**
 * 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 · Kendi sunucunuzda belge doğrulama

Barındırılan doğrulayıcı olmadan: güven listelerini doğrular, iptal listelerini önceden çeker, isteği imzalar, şifreli cevabı çözer ve hattı (T0 + A–E) çalıştırır. INDETERMINATE “şu an denetlenemedi” demektir, “geçersiz” değil.

/**
 * 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 belge verme servisinde kurumunuzun kapsamlı API anahtarıyla. Teklif bağlantısı QR olur; PIN ayrı bir kanaldan gider, bağlantının içinde asla gitmez.

/**
 * 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 · Kurumu sorgulamak

Kayıtlı mı, etkin mi, bu belge türüne yetkili mi? Yalnızca imzalı güven listelerini okur — kişisel veri yok.

/**
 * 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 entegrasyonda kurallar

  • Yalnızca ihtiyacın olan alanları iste — kayıtlı kapsamını aşan istek cüzdan tarafından reddedilir.
  • INDETERMINATE sonucunu “tekrar dene” olarak yorumla, asla “geçersiz” olarak değil.
  • Kişisel veriyi asla loglama — ad, kimlik numarası, passkey kimliği yok.
  • Kararı sunucuda ver. Tarayıcı kodu kullanıcı tarafından değiştirilebilir.

Geliştirici belgelerinin tamamı — kılavuzlar, spesifikasyonlar, kararlar: docs.tamga.network. Roller ve katılımcı kuralları: Tamga ARF. Arka plan: mimari · güven listeleri · Tamga ve EUDI.