TamgaNetwork

Tamga nähili işleýär

Tamga bilen gir

“Google bilen gir” sana tanyş. “Tamga bilen gir” şeýle görünýär, ýöne tersine işleýär: şahsyýet seniň gapjygyňda, saýt diňe tassyklan meýdanlaryňy alýar we ilkinji gezekden soň hiç zat almaýar.

Iki ädim: bir gezek hasaba durmak, soň passkey

  1. Hasaba durmak (bir gezek). Saýt az sanly meýdan soraýar — meselem, ady we familiýasy — we seniň üçin özüne degişli lakamy alýar. Kompýuterde QR kody okadýarsyň; telefonda gapjyk göni açylýar. Gapjyk nämäniň, kim tarapyndan soralýandygyny takyk görkezýär; sen tassyklaýarsyň.
  2. Barlaýjy hödürlemäni barlaýar — gol, ynam sanawy, ýatyrylyş ýagdaýy we enjam baglanyşygy. Saýt netijäni öz serwerinde alýar we hasabyňy açýar.
  3. Passkey goş. Hasaba duranyňdan soň saýt “bu enjama passkey goş” diýip teklip edýär. Mundan beýläk Face ID ýa-da barmak yzy bilen girýärsiň: gapjyk açylmaýar, hiç bir meýdan paýlaşylmaýar. Passkey diňe şol saýt üçin işleýär.
  4. Täze enjam, passkey ýokmy? “Tamga bilen gir” diňe şol saýta degişli lakamyňy iberýär — hiç bir resminama meýdany ýok — soň ýene passkey goşýarsyň.

Web saýt

meýdan soraýar

Tamga Wallet

sen tassyklaýarsyň

Barlaýjy

subutnamany barlaýar

Passkey

gündelik giriş

Meýdan-meýdan sen tassyklaýarsyň

Saýt diňe zerur zady soraýar; tassyklamadyk meýdanyň asla iberilmeýär. Käbir maglumatlar açylman subut edilip bilner — doglan senesi berilmän “18 ýaşdan uly”.

Tamga Wallet

example.com şulara girmek isleýär:

Ady we familiýasy shared
18 ýaşdan uly proven
E-poçta shared
Öý salgysy hidden
Rugsat ber
Ret et

Parol paýlaşylmaýar · her meýdany sen dolandyrýarsyň

Saýt näme alyp biler — näme alyp bilmez

Saýt diňe tassyklan meýdanlaryňy alýar. Ogurlanjak parol ýok. Hasaba alnan çäginden çykyp bilmez: her barlaýjy soraýan meýdanlary bilen birlikde ynam sanawynda hasaba alnandyr we gapjygyň artykmajy barada duýduryş berýär. Hasap açaryň diňe şol saýta degişli lakamdyr: başga saýt başga lakam görýär, saýtlar seni deňeşdirip bilmeýär. Tassyklanan şahsyýetiňden alnandygy üçin täze telefonda şahsyýetiňi täzeden tassyklanyňda şol lakamlar yzyna gelýär. Şahsyýet we resminama belgiň hiç haçan iberilmeýär.

Işläp düzüjiler üçin

Sahypa toplumy @tamga-network/verifier/web (barlaýjy ony skript hökmünde hem hödürleýär). QR kody ýa-da “gapjykda aç” düwmesini çekýär we netijäni habar berýär — karary bermeýär: brauzer kody ulanyjy tarapyndan üýtgedilip bilner, şonuň üçin karar hemişe seniň serweriňde berilýä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 };
}
Passkey (WebAuthn)
import { passkey } from "@tamga-network/verifier/web";
// after sign-up, while the session is open:
const o = await post("/passkey/register/options");
await post("/passkey/register/verify", await passkey.create(o));
// daily sign-in — no wallet, no fields shared:
const { flow, options } = await post("/passkey/login/options");
await post("/passkey/login/verify", { flow, response: await passkey.get(options) });
  • Bir gezek ulanmak: bir hödürleme takyk bir sessiýa açýar.
  • Üç netije: ACCEPTED, REJECTED ýa-da INDETERMINATE. INDETERMINATE “häzir barlap bolmady — gaýtadan synanyş” diýmekdir, asla “resminama nädogry” däl.
  • Kuki: HttpOnly, SameSite=Lax, Secure, serwer tarapynda möhlet; at, açar ýa-da passkey belgisi asla žurnala ýazylmaýar.
  • WebAuthn ýalaňaç IP salgysynda işlemeýär — localhost ýa-da HTTPS domen ulan.

Açyk standartlara esaslanýar

Ýapyk giriş ulgamy däl: hödürlemeler SD-JWT VC resminamalary bilen OpenID for Verifiable Presentations (OpenID4VP) ulanýar — ÝB-niň EUDI Wallet-y bilen şol bir profiller — gündelik giriş bolsa standart WebAuthn passkey.

Häzirki ýagdaý

Nusga saýtymyzda başdan-aýak işleýär (hasaba durmak, gaýtadan ulanmagyň ret edilmegi, giriş, passkey). Ýerleşdirilen barlaýjy netijäni diňe hödürlemäni açan saýta berýär — saýt muny ynam sanawyndaky açary bilen gol çekip subut edýär — we bahalary bir gezek berýär. Gapjyk her saýta aýry lakam berýär; barlaýjy ony gapjyk nusgasynyň subutnamasy bilen barlaýar.