Məzmuna keç
Tamga Network

Bu səhifə avtomatik tərcümə olunub. Səhv görsəniz, info@tamga.network ünvanına yazın.

SDK

SDK

Şəbəkədəki hər iş üçün açıq mənbəli TypeScript paketləri: yoxlama, rəqəmsal sənəd vermə, etibar siyahıları və rəqəmsal cüzdanın nüvəsi. Apache-2.0, EUDI profillərinə uyğun.

Quraşdırma

Paketlər npm-də 0.7.0 sınaq versiyası kimi dərc olunur; sabit 1.0.0 versiyası hər şey hazır olduqda çıxacaq. Sınaq versiyalarında API dəyişə bilər.

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

Paketlər

Hər paketin adı onun npm səhifəsini açır; “Sənədlər” isə tərtibatçı sənədlərindəki arayışını açır. Göstərilən versiya (0.7.0) cari sınaq versiyasıdır.

0.7.0
@tamga-network/core

heşlər, identifikatorun törədilməsi, sertifikat köməkçiləri

Sənədlər
0.7.0
@tamga-network/trust

imzalanmış etibar siyahılarını yükləyir və yoxlayır; vahid oxuma interfeysi (gələcəkdə zəncir üçün də)

Sənədlər
0.7.0
@tamga-network/schemas

sənəd növləri kataloqu: növ metaməlumatları, JSON Schema, məzmun heşləri

Sənədlər
0.7.0
@tamga-network/sd-jwt

SD-JWT VC: seçici paylaşım (selective disclosure), cihaza bağlama, format yoxlamaları

Sənədlər
0.7.0
@tamga-network/mdoc

ISO 18013-5 mdoc: CBOR, COSE, vermə və yoxlama

Sənədlər
0.7.0
@tamga-network/issuer

rəqəmsal sənəd fabriki, OpenID4VCI köməkçiləri, ləğv siyahısının dərc edicisi; yerləşdirilmiş xidmət üçün /client

Sənədlər
0.7.0
@tamga-network/verifier

yoxlama xətti (T0 + A–E), üç nəticə, OpenID4VP sorğuları; səhifə dəsti üçün /web; sıfır bilikli yaş sübutları üçün /zk

Sənədlər
0.7.0
@tamga-network/wallet-core

Node və React Native üçün rəqəmsal cüzdan nüvəsi: açarlar, qəbul etmə, yerli yoxlamalar, təqdim etmə

Sənədlər
0.7.0
@tamga-network/zk

cüzdan tərəfində sıfır bilikli sübut generatoru (ISO mdoc, ilk istifadə: 18 yaşdan yuxarı): Android yerli generatoru hazırdır, iOS gözlənilir

Sənədlər

İşlək nümunələr

Repozitoriyanın examples qovluğundakı real kod: hər test işə salınanda paketlərlə birlikdə icra olunur, buna görə də onlardan ayrı düşə bilməz.

1 · Veb saytda “Tamga ilə daxil olun”

Təqdimatı serveriniz etibar siyahısındakı açarınızla imzalanmış qısamüddətli bəyanla açır; səhifə yalnız QR kodu göstərir; təsdiqlənmiş dəyərlər serverinizə bir dəfə ötürülür.

/**
 * Example 01 — "Sign up / Sign in with Tamga" 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) — client_id is x509_hash of your access certificate (HAIP 1.0). */
  rp: RpSigner;
  /** Your own secret; the stored account key is a keyed hash of the site pseudonym. */
  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-signup" | "site-signin"): 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> };

    // ADR-0031: the account key is the wallet's pseudonym for YOUR site (another site sees a different one); the verifier
    // has checked its signature, audience, nonce and the wallet instance attestation. No document value is sent.
    if (typeof claims.pseudonym !== "string") return { ok: false, outcome: "REJECTED" };
    const accountKey = createHmac("sha256", cfg.siteSecret).update(claims.pseudonym).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 serverinizdə sənədləri yoxlayın

Yerləşdirilmiş yoxlayıcı olmadan: etibar siyahılarını yoxlayır, ləğv siyahılarını əvvəlcədən yükləyir, sorğunu imzalayır, şifrələnmiş cavabı açır və xətti (T0 + A–E) işə salır. INDETERMINATE “hazırda yoxlamaq mümkün olmadı” deməkdir, heç vaxt “etibarsız” demək deyil.

/**
 * 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 with your registration certificate → show it as a QR code. Tamga Wallet refuses a
 *    request that carries no registration certificate (from packages 0.7.0), so fetch yours from the trust lists.
 * 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) — client_id is x509_hash of your access certificate (HAIP 1.0). */
  signer: RpSigner;
  /** Your public base URL; the wallet fetches /vp/req/:id and posts to /vp/response here. */
  publicBase: string;
  /**
   * The registered use (`scope_id` of your trust-list record) this verifier asks for, e.g. "job-application-1". Its
   * registration certificate (`<trustBase>/wrprc/…`) goes into every request (`verifier_info`). "none" only for wallets and
   * tests that do not check registration certificates.
   */
  registrationScope: string | "none";
  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));

  // Open requests, in memory. Each expires with its request object (expiresAt) and the map is capped, so abandoned QR codes
  // cannot grow it without limit. A multi-instance deployment keeps these in a shared store instead.
  const MAX_PENDING = 10_000;
  const pending = new Map<string, { req: PresentationRequest; policy: Policy }>();
  const sweep = (now = Math.floor(Date.now() / 1000)) => {
    for (const [id, e] of pending) if (e.req.expiresAt < now) pending.delete(id);
    for (const id of pending.keys()) {
      if (pending.size < MAX_PENDING) break;
      pending.delete(id); // oldest first
    }
  };
  const live = (id: string) => {
    const e = pending.get(id);
    if (e && e.req.expiresAt < Math.floor(Date.now() / 1000)) {
      pending.delete(id);
      return undefined;
    }
    return e;
  };

  // Your registration certificate: published with the trust lists, renewed by the registrar (at most 12 months); re-read hourly.
  let registration: { jwt: string; at: number } | null = null;
  async function registrationCert(): Promise<string | undefined> {
    if (cfg.registrationScope === "none") return undefined;
    if (registration && Date.now() - registration.at < 3600_000) return registration.jwt;
    const index = JSON.parse(await (await http(`${cfg.trustBase}/wrprc/index.json`)).text()) as {
      items: Array<{ kind: string; client_id?: string; scope_id?: string; path: string }>;
    };
    const item = index.items.find(
      (i) => i.kind === "rp" && i.client_id === cfg.signer.clientId && i.scope_id === cfg.registrationScope,
    );
    if (!item)
      throw new Error(
        `no registration certificate for ${cfg.signer.clientId} / ${cfg.registrationScope}: wallets will refuse the request — check your registration (identity number in the access certificate, scope still valid)`,
      );
    registration = { jwt: (await (await http(`${cfg.trustBase}/${item.path}`)).text()).trim(), at: Date.now() };
    return registration.jwt;
  }

  /** 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],
      registrationCert: await registrationCert(),
    });
    sweep();
    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) => live(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 = live(String(kid).replace(/^enc-/, ""));
    if (!entry) throw new Error("unknown, expired 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 · Qurum olaraq sənəd verin

Yerləşdirilmiş sənəd vermə xidmətində qurumunuzun məhdud səlahiyyətli API açarı ilə. Təklif keçidi QR koda çevrilir; PIN ayrı kanalla göndərilir, heç vaxt keçidin içində olmur.

/**
 * 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 · Qurumu yoxlayın

Qeydiyyatdadırmı, aktivdirmi, bu sənəd növü üçün səlahiyyəti varmı? Yalnız imzalanmış etibar siyahılarını oxuyur — fərdi məlumatlar yoxdur.

/**
 * 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,
  };
}