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ň?

MaksatPaket
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.

PaketNäme edýär
@tamga-network/coreheşler, belgi almak, sertifikat kömekçileri
@tamga-network/trustgol çekilen ynam sanawlaryny ýükleýär we barlaýar; bir okamak interfeýsi (geljekde zynjyr üçin hem)
@tamga-network/schemasresminama görnüşleriniň katalogy: görnüş kesgitlemesi, JSON Schema, mazmun heşleri
@tamga-network/sd-jwtSD-JWT VC: saýlama açyklama, enjam baglanyşygy, görnüş barlaglary
@tamga-network/mdocISO 18013-5 mdoc: CBOR, COSE, bermek we barlamak
@tamga-network/issuerresminama fabrigi, OpenID4VCI kömekçileri, ýatyrylyş sanawyny çap ediji; ýerleşdirilen hyzmat üçin /client
@tamga-network/verifierbarlag hatary (T0 + A–E), üç netije, OpenID4VP haýyşlary; sahypa toplumy üçin /web
@tamga-network/wallet-coreNode we React Native üçin gapjyk ýadrosy: açarlar, almak, ýerli barlag, hödürlemek

Häzirki ýagdaý — dogruçyl

BölekÝagdaýBellik
Paketlernpm-de (0.1.0)deslapky wersiýa; 1.0-a çenli interfeýs üýtgäp biler
Ýerleşdirilen barlaýjyişleýärnetije 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ş hyzmatyişleýärgurama başyna çäkli, möhletli we ýatyrylyp bilinýän API açary
Ynam sanawyna hasaba alyşişleýärhaý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.