PageSourceSearch

https://www.otrust.eu/timestamp

html otrust.eu collected 2026-10-02 01:53:17 UTC 106,851 bytes, 2,281 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en" class="otrust-dashboard-shell" data-otrust-page="home">
3<head>
4  <meta charset="UTF-8">
5  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6  <title>OTRUST by Kris Ledel | Timestamping, signing and open proof infrastructure</title>
7  <meta name="description" content="OTRUST by Kris Ledel provides production timestamping and document signing, plus an open self-attested ZK proof lab and gated issuer-bound Auth design.">
8  
9  <!-- SEO Meta Tags -->
10  <meta name="author" content="Kristian Ledel">
11  <meta name="creator" content="Kris Ledel">
12  <link rel="author" href="https://www.otrust.eu/krisledel">
13  <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
14  <link rel="canonical" href="https://www.otrust.eu/">
15  <meta name="theme-color" content="#16160f">
16  
17  <!-- Open Graph / Facebook -->
18  <meta property="og:type" content="website">
19  <meta property="og:url" content="https://www.otrust.eu/">
20  <meta property="og:title" content="OTRUST by Kris Ledel - Cryptographic Trust Infrastructure">
21  <meta property="og:description" content="Timestamp files, sign document hashes, and inspect open proof infrastru
21cture with explicit production boundaries.">
22  <meta property="og:image" content="https://www.otrust.eu/og-image.svg">
23  <meta property="og:site_name" content="OTRUST">
24  <meta property="article:author" content="Kristian Ledel">
25  <meta property="og:locale" content="en_US">
26  
27  <!-- Twitter -->
28  <meta name="twitter:card" content="summary_large_image">
29  <meta name="twitter:url" content="https://www.otrust.eu/">
30  <meta name="twitter:title" content="OTRUST by Kris Ledel - Trust Infrastructure">
31  <meta name="twitter:description" content="Production timestamping and signing with an open ZK proof lab.">
32  <meta name="twitter:image" content="https://www.otrust.eu/og-image.svg">
33  <meta name="twitter:creator" content="@kledel">
34  
35  <!-- Additional SEO -->
36  <meta name="geo.region" content="SE">
37  <meta name="geo.placename" content="Sweden">
38  <link rel="alternate" hreflang="en" href="https://www.otrust.eu/">
39  <link rel="alternate" hreflang="x-default" href="https://www.otrust.eu/">
40  
41  <!-- Structured Data: WebApplication -->
42  
42<script type="application/ld+json">
43  {
44    "@context": "https://schema.org",
45    "@type": "WebApplication",
46    "name": "OTRUST",
47    "alternateName": "OTRUST Trust Infrastructure",
48    "url": "https://www.otrust.eu",
49    "description": "Cryptographic trust infrastructure by Kristian (Kris) Ledel for timestamping, document signing, self-attested zero-knowledge range proofs, and a gated issuer-bound Auth design.",
50    "applicationCategory": "SecurityApplication",
51    "applicationSubCategory": "Document Verification",
52    "operatingSystem": "Any",
53    "browserRequirements": "Requires JavaScript",
54    "softwareVersion": "1.0",
55    "author": {
56      "@type": "Person",
57      "name": "Kristian Ledel",
58      "alternateName": "Kris Ledel",
59      "url": "https://www.otrust.eu/krisledel",
60      "sameAs": [
61        "https://krisledel.com/",
62        "https://github.com/krisledel",
63        "https://www.linkedin.com/in/krisledel"
64      ],
65      "jobTitle": "Founder & Developer",
66      "knowsAbout": ["blockchain", "cryptography", "digital signatures", "OpenTimestamps", "Bitcoin", "electronic identity", "digital identity", "partner Auth"]
67    },
68    "creator": {
69      "@type": "Person",
70      "name": "Kristian Ledel",
71      "alternateName": "Kris Ledel",
72      "url": "https://www.otrust.eu/krisledel"
73    },
74    "offers": {
75      "@type": "Offer",
76      "price": "0",
77      "priceCurrency": "USD"
78    },
79    "featureList": [
80      "Bitcoin blockchain anchoring",
81      "OpenTimestamps standard",
82      "Self-attested zero-knowledge range proofs",
83      "Cryptographic signatures",
84      "Bulk timestamping",
85      "Issuer-bound Auth integration contract",
86      "Email timestamping",
87      "Email notifications",
88      "Downloadable .ots receipts"
89    ],
90    "audience": {
91      "@type": "Audience",
92      "audienceType": ["Product Teams", "B2B Platforms", "Researchers", "Legal Professionals", "Compliance Teams", "Developers"]
93    }
94  }
95  </script>
95
96  
97  <!-- Structured Data: Organization -->
98  
98<script type="application/ld+json">
99  {
100    "@context": "https://schema.org",
101    "@type": "Organization",
102    "name": "OTRUST",
103    "url": "https://www.otrust.eu",
104    "logo": "https://www.otrust.eu/favicon.svg",
105    "founder": {
106      "@type": "Person",
107      "name": "Kristian Ledel",
108      "alternateName": "Kris Ledel",
109      "url": "https://www.otrust.eu/krisledel",
110      "sameAs": [
111        "https://krisledel.com/",
112        "https://github.com/krisledel",
113        "https://www.linkedin.com/in/krisledel"
114      ]
115    },
116    "foundingDate": "2024",
117    "sameAs": [
118      "https://github.com/otrust-eu",
119      "https://github.com/krisledel",
120      "https://www.linkedin.com/company/otrustse"
121    ]
122  }
123  </script>
123
124  
125  <!-- Structured Data: FAQ for rich snippets -->
126  
126<script type="application/ld+json">
127  {
128    "@context": "https://schema.org",
129    "@type": "FAQPage",
130    "mainEntity": [
131      {
132        "@type": "Question",
133        "name": "How can I prove when my research data existed?",
134        "acceptedAnswer": {
135          "@type": "Answer",
136          "text": "OTRUST creates a cryptographic timestamp of your research data anchored to the Bitcoin blockchain. This provides immutable, third-party verifiable proof of when your data existed, useful for establishing priority in scientific discoveries."
137        }
138      },
139      {
140        "@type": "Question",
141        "name": "Is blockchain timestamping legally valid for patents?",
142        "acceptedAnswer": {
143          "@type": "Answer",
144          "text": "Blockchain timestamps via OpenTimestamps provide independently verifiable evidence that a hash existed at a point in time. They are not legal advice and do not replace formal patent filing or counsel."
145        }
146      },
147      {
148        "@type": "Question",
149        "name": "How does OTRUST protect my privacy?",
150        "acceptedAnswer": {
151          "@type": "Answer",
152          "text": "OTRUST uses a blind-notary architecture. Your files are hashed locally and only the SHA-256 hash, signature, public key, timestamp, and record metadata are stored. The app does not intentionally persist IP addresses; notification emails are stored separately and removed by TTL."
153        }
154      },
155      {
156        "@type": "Question",
157        "name": "Does OTRUST work with partner identity flows?",
158        "acceptedAnswer": {
159          "@type": "Answer",
160          "text": "OTRUST is designed for partner-branded Auth flows around approved identity methods. OTRUST remains visible as the verification provider and handles ID creation, callback verification, branding boundaries, and auditability."
161        }
162      },
163      {
164        "@type": "Question",
165        "name": "Who created OTRUST?",
166        "acceptedAnswer": {
167          "@type": "Answer",
168          "text": "OTRUST was created by Kristian (Kris) Ledel, a Swedish developer and entrepreneur specializing in blockchain technology and cryptographic security. Kris Ledel built OTRUST as a free, open-source tool for researchers, inventors, and legal professionals who need verifiable proof of when their work existed."
169        }
170      }
171    ]
172  }
173  </script>
173
174  
175  <!-- Structured Data: Creator Profile -->
176  
176<script type="application/ld+json">
177  {
178    "@context": "https://schema.org",
179    "@type": "Person",
180    "name": "Kristian Ledel",
181    "alternateName": ["Kris Ledel", "krisledel"],
182    "url": "https://www.otrust.eu/krisledel",
183    "jobTitle": "Founder & Developer",
184    "description": "Kris Ledel is a Swedish tech founder who built OTRUST — zero-knowledge Bitcoin timestamping and open-source blind notary infrastructure at otrust.eu.",
185    "knowsAbout": ["blockchain technology", "cryptography", "Bitcoin", "OpenTimestamps", "digital signatures", "web security", "electronic identity", "digital identity"],
186    "nationality": {"@type": "Country", "name": "Sweden"},
187    "sameAs": [
188      "https://krisledel.com/",
189      "https://github.com/krisledel",
190      "https://www.linkedin.com/in/krisledel",
191      "https://github.com/otrust-eu"
192    ],
193    "worksFor": {
194      "@type": "Organization",
195      "name": "OTRUST",
196      "url": "https://www.otrust.eu"
197    },
198    "brand": {
199      "@type": "Brand",
200      "name": "OTRUST",
201      "url": "https://www.otrust.eu"
202    }
203  }
204  </script>
204
205
206  <!-- Structured Data: BreadcrumbList -->
207  
207<script type="application/ld+json">
208  {
209    "@context": "https://schema.org",
210    "@type": "BreadcrumbList",
211    "itemListElement": [
212      {"@type": "ListItem", "position": 1, "name": "OTRUST by Kristian Ledel", "item": "https://www.otrust.eu/"},
213      {"@type": "ListItem", "position": 2, "name": "Kris Ledel", "item": "https://www.otrust.eu/krisledel"},
214      {"@type": "ListItem", "position": 3, "name": "Documentation", "item": "https://www.otrust.eu/docs"},
215      {"@type": "ListItem", "position": 4, "name": "API Reference", "item": "https://www.otrust.eu/api-docs"}
216    ]
217  }
218  </script>
218
219
220  <link rel="icon" type="image/svg+xml" href="/favicon.svg">
221  <!-- Privacy-respecting analytics (no cookies, no personal data) -->
222  
222<script defer data-domain="otrust.eu" src="https://plausible.io/js/script.js"></script>
222
223  <link rel="stylesheet" href="/otrust-redesign.css?v=20260723-03">
224</head>
225<body data-otrust-page="home">
226  <a href="#main-content" class="skip-link">Skip to content</a>
227
228  <main id="main-content" role="main">
229  <section class="dashboard-home otrust-bento" aria-label="OTRUST dashboard overview">
230    <div class="bento-grid" id="dashboard-card-grid">
231
232      <!-- HERO -->
233      <section class="bento-hero" aria-label="Intro">
234        <header class="dashboard-site-nav bento-nav" aria-label="Dashboard navigation">
235          <a href="/" class="dashboard-logo">OTRUST</a>
236          <nav class="dashboard-links" aria-label="OTRUST site navigation">
237            <a href="/timestamp">Timestamp</a>
238            <a href="/proof">ID</a>
239            <a href="/sign">Sign</a>
240            <a href="/sign-in" class="bento-nav-cta">Auth</a>
241          </nav>
242        </header>
243        <div class="bento-hero-copy">
244          <h1>Trust,<br><span>without permission.</span></h1>
245          <p class="bento-rule" aria-hidden="true"></p>
246          <p class="bento-hero-lede">Production timestamps and signatures, with open ZK proof infrastru
246cture and explicit feature readiness.</p>
247          <div class="bento-hero-actions">
248            <a class="bento-primary-action" href="/timestamp#timestamp-tool">
249              <span aria-hidden="true"></span>
250              <strong>Timestamp a file</strong>
251            </a>
252            <a class="bento-selfhost" href="/quickstart.html">
253              <span>Self-host in 60s</span>
254              <span aria-hidden="true">&rarr;</span>
255            </a>
256            <a class="bento-text-action" href="#flow">How it works <span aria-hidden="true">&darr;</span></a>
257          </div>
258        </div>
259        <div class="bento-hero-burst dashboard-orbit dashboard-orbit-hero" aria-hidden="true"></div>
260        <div class="bento-hero-foot">
261          <span class="bento-lock" aria-hidden="true">
262            <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="11" width="14" height="9" rx="1.5"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>
263          </span>
264          <p>Local-first records. Anchored on Bitcoin.<br>Independently verifiable outcomes.</p>
265        </div>
266      </section>
267
268      <!-- 01 TIMESTAMP -->
269      <a class="bento-card bento-function-card bento-c01" href="/timestamp" aria-label="Open Timestamp">
270        <div class="bento-card-head">
271          <span class="bento-num">01</span><span class="bento-rule-sm" aria-hidden="true"></span>
272        </div>
273        <h2 class="bento-card-title">Timestamp</h2>
274        <p class="bento-card-lede">Create a verifiable record that a file, hash, or text existed at a specific point in time.</p>
275        <span class="bento-function-glyph" aria-hidden="true">
276          <svg viewBox="0 0 96 96" width="96" height="96">
277            <line x1="48" y1="48" x2="48" y2="9"/><line x1="48" y1="48" x2="81" y2="29"/><line x1="48" y1="48" x2="78" y2="70"/><line x1="48" y1="48" x2="17" y2="72"/><line x1="48" y1="48" x2="18" y2="26"/>
278            <circle cx="48" cy="48" r="9"/><rect x="45" y="45" width="6" height="6"/>
279            <circle cx="48" cy="9" r="2.5"/><circle cx="81" cy="29" r="2.5"/><circle cx="78" cy="70" r="2.5"/><circle cx="17" cy="72" r="2.5"/><circle cx="18" cy="26" r="2.5"/>
280          </svg>
281        </span>
282        <div class="bento-card-foot">
283          <span class="bento-card-link">Open timestamp <span aria-hidden="true">&rarr;</span></span>
284          <span class="bento-meta">Local SHA-256 &middot; Bitcoin via OTS</span>
285        </div>
286      </a>
287
288      <!-- 02 PROOF LAB -->
289      <a class="bento-card bento-function-card bento-c02" href="/proof" aria-label="Open proof lab">
290        <div class="bento-card-head">
291          <span class="bento-num">02</span><span class="bento-rule-sm" aria-hidden="true"></span>
292        </div>
293        <h2 class="bento-card-title">Proof lab</h2>
294        <p class="bento-card-lede">Generate self-attested age or income range proofs locally and inspect their public signals.</p>
295        <span class="bento-function-glyph" aria-hidden="true">
296          <svg viewBox="0 0 96 96" width="96" height="96">
297            <rect x="20" y="20" width="56" height="56"/><rect x="30" y="30" width="36" height="36"/><rect x="40" y="40" width="16" height="16"/>
298            <circle cx="20" cy="20" r="2"/><circle cx="48" cy="14" r="2"/><circle cx="76" cy="20" r="2"/><circle cx="82" cy="48" r="2"/><circle cx="76" cy="76" r="2"/><circle cx="48" cy="82" r="2"/><circle cx="20" cy="76" r="2"/><circle cx="14" cy="48" r="2"/>
299          </svg>
300        </span>
301        <div class="bento-card-foot">
302          <span class="bento-card-link">Open proof lab <span aria-hidden="true">&rarr;</span></span>
303          <span class="bento-meta">Groth16 &middot;
303 Ceremony pending</span>
304        </div>
305      </a>
306
307      <!-- 03 SIGN -->
308      <a class="bento-card bento-function-card bento-c03" href="/sign" aria-label="Open Sign">
309        <div class="bento-card-head">
310          <span class="bento-num">03</span><span class="bento-rule-sm" aria-hidden="true"></span>
311        </div>
312        <h2 class="bento-card-title">Sign</h2>
313        <p class="bento-card-lede">Create a signing request, bind signers to a document hash, and track the audit trail.</p>
314        <span class="bento-function-glyph" aria-hidden="true">
315          <svg viewBox="0 0 96 96" width="96" height="96">
316            <path d="M10 62 C24 31 35 74 48 49 S67 24 84 45"/><path d="M16 72 C33 60 47 67 61 57 S78 47 88 52"/>
317            <line x1="26" y1="20" x2="26" y2="76"/><line x1="70" y1="18" x2="70" y2="78"/>
318            <circle cx="48" cy="49" r="5"/>
319          </svg>
320        </span>
321        <div class="bento-card-foot">
322          <span class="bento-card-link">Open sign <span aria-hidden="true">&rarr;</span></span>
323          <span class="bento-meta">Ed25519 &middot; Audit trail</span>
324        </div>
325      </a>
326
327      <!-- 04 AUTH -->
328      <a class="bento-card bento-function-card bento-c04" href="/sign-in" aria-label="Open Auth">
329        <div class="bento-card-head">
330          <span class="bento-num">04</span><span class="bento-rule-sm" aria-hidden="true"></span>
331          <span class="bento-dot-mark" aria-hidden="true"></span>
332        </div>
333        <h2 class="bento-card-title">Auth preview</h2>
334        <p class="bento-card-lede">Review the issuer-bound partner Auth contract. Production challenges are currently disabled.</p>
335        <span class="bento-function-glyph" aria-hidden="true">
336          <svg viewBox="0 0 96 96" width="96" height="96">
337            <rect x="22" y="22" width="52" height="52" rx="8"/><rect x="32" y="32" width="32" height="32" rx="6"/>
338            <line x1="48" y1="10" x2="48" y2="22"/><line x1="48" y1="74" x2="48" y2="86"/><line x1="10" y1="48" x2="22" y2="48"/><line x1="74" y1="48" x2="86" y2="48"/>
339            <circle cx="48" cy="48" r="4"/><path d="M39 58 C42 52 54 52 57 58"/>
340          </svg>
341        </span>
342        <div class="bento-card-foot">
343          <span class="bento-card-link">Review Auth <span aria-hidden="true">&rarr;</span></span>
344          <span class="bento-meta">Issuer required &middot; Fail closed</span>
345        </div>
346      </a>
347
348      <!-- STEP BAND (black) -->
349      <section class="bento-band" id="flow" aria-label="How it works">
350        <ol class="bento-band-steps">
351          <li>
352            <span class="bento-band-icon" aria-hidden="true"><svg viewBox="0 0 32 32" width="22" height="22"><circle cx="11" cy="13" r="1.4"/>
352<circle cx="20" cy="11" r="1.4"/><circle cx="16" cy="20" r="1.4"/><circle cx="22" cy="19" r="1.4"/></svg></span>
353            <span class="bento-band-num">01</span>
354            <strong>Hash</strong>
355            <p>Hash files and signing inputs locally.</p>
356          </li>
357          <li>
358            <span class="bento-band-icon" aria-hidden="true"><svg viewBox="0 0 32 32" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M11 21 21 11M13 10h8v8"/></svg></span>
359            <span class="bento-band-num">02</span>
360            <strong>Sign</strong>
361            <p>Bind records with signatures or commitments.</p>
362          </li>
363          <li>
364            <span class="bento-band-icon" aria-hidden="true"><svg viewBox="0 0 32 32" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="16" cy="16" r="9"/><circle cx="16" cy="16" r="4.5"/><circle cx="16" cy="16" r="1.2" fill="currentColor"/></svg></span>
365            <span class="bento-band-num">03</span>
366            <strong>Anchor</strong>
367            <p>Anchor records externally for durability.</p>
368          </li>
369          <li>
370            <span class="bento-band-icon" aria-hidden="true"><svg viewBox="0 0 32 32" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="16" cy="16" r="9"/></svg></span>
371            <span class="bento-band-num">04</span>
372            <strong>Prove</strong>
373            <p>Verify later without accounts or permission.</p>
374          </li>
375        </ol>
376      </section>
377
378      <!-- PRINCIPLES -->
379      <section class="bento-principles" aria-labelledby="bento-principles-heading">
380        <div class="bento-principles-copy">
381          <span class="bento-eyebrow">Built on principles</span>
382          <h2 id="bento-principles-heading">Trust is<br>earned by design.</h2>
383          <p>OTRUST exists to make digital truth verifiable without permission, accounts or intermediaries.</p>
384        </div>
385        <div class="bento-principles-grid">
386          <article>
387            <span class="bento-pr-icon" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.4"><rect x="4" y="4" width="16" height="16" rx="2"/><circle cx="12" cy="12" r="3"/></svg></span>
388            <strong>Local-first</strong>
389            <p>Hashing and sensitive checks run in your browser where possible.</p>
390          </article>
391          <article>
392            <span class="bento-pr-icon" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M3 12s3-7 9-7 9 7 9 7-3 7-9 7-9-7-9-7Z"/><path d="m4 4 16 16"/></svg></span>
393            <strong>Zero-knowledge</strong>
394            <p>Timestamping never needs your file contents.</p>
395          </article>
396          <article>
397            <span class="bento-pr-icon" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.4"><rect x="5" y="10" width="14" height="10" rx="1.5"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></span>
398            <strong>Immutable</strong>
399            <p>Anchored in the Bitcoin blockchain via OpenTimestamps.</p>
400          </article>
401          <article>
402            <span class="bento-pr-icon" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.4"><path d="m8 8-4 4 4 4M16 8l4 4-4 4M14 6l-4 12"/></svg></span>
403            <strong>Open source</strong>
404            <p>Transparent by default. Verifiable by everyone.</p>
405          </article>
406        </div>
407      </section>
408
409      <!-- 05 WHAT YOU GET -->
410      <article class="bento-card bento-c05">
411        <div class="bento-card-head">
412          <span class="bento-num">05</span><span class="bento-rule-sm" aria-hidden="true"></span>
413        </div>
414        <div class="bento-proof-burst dashboard-orbit" aria-hidden="true"></div>
415        <h2 class="bento-card-title small">What you get</h2>
416        <dl class="bento-proof-dl">
417          <div><dt>Output</dt>
417<dd class="bento-proof-text">Verifiable record</dd></div>
418          <div><dt>Created by</dt><dd class="bento-proof-text">Timestamp, ID, Sign or Auth</dd></div>
419          <div><dt>Record ID</dt><dd>a3f2bde6fc7d9e1b3a6c8d2e4f6a1b7c9d0e2f1&hellip;</dd></div>
420          <div><dt>Timestamp (UTC)</dt><dd>May 24, 2025 &mdash; 14:32:18</dd></div>
421          <div><dt>Signature</dt><dd>ed25519:7f31a1c&hellip;e9b2</dd></div>
422        </dl>
423        <p class="bento-proof-valid"><span class="ot-dot" aria-hidden="true"></span> Save the record ID to verify later.</p>
424        <a class="bento-card-link" href="/docs#how-it-works">How records verify <span aria-hidden="true">&rarr;</span></a>
425      </article>
426
427      <!-- 06 FOR DEVELOPERS -->
428      <article class="bento-card bento-c06">
429        <div class="bento-card-head">
430          <span class="bento-num">06</span><span class="bento-rule-sm" aria-hidden="true"></span>
431        </div>
432        <h2 class="bento-card-title small">For developers</h2>
433        <ul class="bento-doclist">
434          <li><a href="/api-docs"><span><strong>API docs</strong><em>Endpoint reference</em></span><span class="arr" aria-hidden="true">&rarr;</span></a></li>
435          <li><a href="/playground/"><span><strong>SDK playground</strong><em>Client flow tests</em></span><span class="arr" aria-hidden="true">&rarr;</span></a></li>
436        </ul>
437      </article>
438
439      <!-- 07 DOCS & RESOURCES -->
440      <article class="bento-card bento-c07">
441        <div class="bento-card-head">
442          <span class="bento-num">07</span><span class="bento-rule-sm" aria-hidden="true"></span>
443        </div>
444        <h2 class="bento-card-title small">Docs &amp; resources</h2>
445        <ul class="bento-doclist">
446          <li><a href="/docs"><span><strong>Getting started</strong><em>The basics in 3 minutes</em></span><span class="arr" aria-hidden="true">&rarr;</span></a></li>
447          <li><a href="/docs#how-it-works"><span><strong>How it works</strong><em>Technical deep dive</em></span><span class="arr" aria-hidden="true">&rarr;</span></a></li>
448        </ul>
449      </article>
450
451    </div>
452
453    <footer class="bento-footer">
454      <div class="bento-footer-meta">
455        <a href="/" class="footer-brand">OTRUST</a>
456        <span class="footer-copy">&copy; 2026 OTRUST <span class="made-in-eu" aria-hidden="true">&middot; Made in Sweden, Europe</span></span>
457      </div>
458      <nav class="footer-nav-groups" aria-label="Footer">
459        <div class="footer-nav-group">
460          <span class="footer-nav-label">Use OTRUST</span>
461          <div class="footer-links">
462            <a href="/timestamp">Timestamp</a>
463            <a href="/sign">Sign</a>
464            <a href="/about">About</a>
465            <a href="/sign-in">Auth</a>
466          </div>
467        </div>
468        <div class="footer-nav-group">
469          <span class="footer-nav-label">Build with OTRUST</span>
470          <div class="footer-links">
471            <a href="/docs">Docs</a>
472            <a href="/api-docs">API</a>
473            <a href="/use-cases">Use cases</a>
474            <a href="/changelog">Changelog</a>
475            <a href="https://github.com/otrust-eu" rel="noopener noreferrer">GitHub</a>
476          </div>
477        </div>
478        <div class="footer-nav-group footer-nav-legal">
479          <div class="footer-links">
480            <a href="/privacy-policy">Privacy</a>
481            <a href="/terms">Terms</a>
482          </div>
483        </div>
484      </nav>
485    </footer>
486
487
488    <section class="dashboard-tool-slot" id="dashboard-tool-shell" aria-label="OTRUST timestamp tool">
489    <section class="claim-section home-claim-section" id="timestamp-tool" aria-label="Timestamp tool">
490    <div class="claim-card timestamp-card">
491      <div class="timestamp-dashboard-head">
492        <div>
493          <span class="workspace-topline">Local-first record workspace</span>
494          <h2>
494Timestamp, verify and manage records.</h2>
495          <p>Hash locally, sign with your browser key, anchor externally, and keep every receipt independently verifiable.</p>
496        </div>
497        <div class="timestamp-live-meta" aria-label="Timestamp guarantees">
498          <span>SHA-256 local hash</span>
499          <span>Ed25519 signature</span>
500          <span>Bitcoin anchoring via OTS</span>
501
502        </div>
503      </div>
504
505      <div class="timestamp-dashboard-body">
506        <div class="tabs timestamp-tool-nav" role="tablist" aria-label="Timestamp actions">
507          <button class="tab active" type="button" role="tab" id="tab-button-file" data-tab="file" aria-selected="true" aria-controls="tab-file">
508            <span>Timestamp</span>
509            <small>Files</small>
510          </button>
511          <button class="tab" type="button" role="tab" id="tab-button-text" data-tab="text" aria-selected="false" aria-controls="tab-text">
512            <span>Text</span>
513            <small>Content</small>
514          </button>
515          <button class="tab" type="button" role="tab" id="tab-button-verify" data-tab="verify" aria-selected="false" aria-controls="tab-verify">
516            <span>Verify</span>
517            <small>Hash or file</small>
518          </button>
519          <button class="tab" type="button" role="tab" id="tab-button-receipt" data-tab="receipt" aria-selected="false" aria-controls="tab-receipt">
520            <span>History</span>
521            <small>Receipts <span class="history-count" id="history-count" style="display: none;">0</span></small>
522          </button>
523          <button class="tab" type="button" role="tab" id="tab-button-compare" data-tab="compare" aria-selected="false" aria-controls="tab-compare">
524            <span>Compare</span>
525            <small>Bulk check</small>
526          </button>
527          <button class="tab" type="button" role="tab" id="tab-button-keys" data-tab="keys" aria-selected="false" aria-controls="tab-keys">
528            <span>Keys</span>
529            <small>Browser key</small>
530          </button>
531          <button class="tab" type="button" role="tab" id="tab-button-integrations" data-tab="integrations" aria-selected="false" aria-controls="tab-integrations">
532            <span>Integrations</span>
533            <small>SDKs and API</small>
534          </button>
535        </div>
536
537        <div class="timestamp-panel-stage">
538          <!-- File Tab -->
539          <div id="tab-file" class="tab-content active timestamp-panel" role="tabpanel" aria-labelledby="tab-button-file" tabindex="0">
540            <div class="timestamp-panel-heading">
541              <span>01 / Hash and sign</span>
542              <h3>Timestamp files</h3>
543              <p>Drop one or many files. Only hashes and record metadata leave the browser.</p>
544            </div>
545
546            <div class="timestamp-action-grid">
547              <div class="dropzone" id="dropzone" role="button" tabindex="0" aria-describedby="file-drop-help">
548                <div class="dropzone-icon">
549                  <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="color: var(--accent);">
550                    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
551                    <polyline points="14 2 14 8 20 8"/>
552                    <line x1="12" y1="18" x2="12" y2="12"/>
553                    <line x1="9" y1="15" x2="15" y2="15"/>
554                  </svg>
555                </div>
556                <div class="dropzone-text">
557                  Drop files here or <strong>browse</strong>
558                  <small id="file-drop-help">Hashed locally - never uploaded</small>
559                </div>
560                <input type="file" id="file-input" multiple hidden>
561              </div>
562
563              <div class="timestamp-side-stack">
564                <div class="form-group notify-email-group">
565                  <label class="notify-email-opt">
566                    <input type="checkbox" id="notify-email-opt-in" checked>
567                    <span>Email me when Bitcoin confirms</span>
568                  </label>
569                  <input type="email" id="notify-email" placeholder="[email protected]" autocomplete="email">
570                  <small class="field-hint">Automatic alert in ~1–2 hours. Email is deleted after sending.</small>
571                </div>
572
573                <button class="btn btn-primary" id="claim-btn" disabled>
574                  Create timestamp
575                </button>
576              </div>
577            </div>
578
579            <div class="file-list" id="file-list"></div>
580
581            <div class="progress-section" id="progress" style="display: none;" aria-live="polite">
582              <div class="progress-step" id="step-pow">
583                <span class="icon">1</span>
584                <span>Solving proof-of-work...</span>
585              </div>
586              <div class="progress-step" id="step-sign">
587                <span class="icon">2</span>
588                <span>Signing with your key...</span>
589              </div>
590              <div class="progress-step" id="step-submit">
591                <span class="icon">3</span>
592                <span>Submitting to blockchain...</span>
593              </div>
594            </div>
595
596            <div id="result" aria-live="polite"></div>
597          </div>
598
599          <!-- Text Tab -->
600          <div id="tab-text" class="tab-content timestamp-panel" role="tabpanel" aria-labelledby="tab-button-text" tabindex="0" hidden>
601            <div class="timestamp-panel-heading">
602              <span>02 / Text record</span>
603              <h3>Timestamp raw content</h3>
604              <p>Paste text, code, notes or a claim. OTRUST hashes the content locally before registration.</p>
605            </div>
606
607            <div class="timestamp-action-grid timestamp-text-grid">
608              <div class="form-group timestamp-text-field">
609                <label for="text-input">Content</label>
610                <textarea id="text-input" placeholder="Paste text, code, or any content..."></textarea>
611              </div>
612
613              <div class="timestamp-side-stack">
614                <div class="form-group notify-email-group">
615                  <label class="notify-email-opt">
616                    <input type="checkbox" id="text-notify-email-opt-in" checked>
617                    <span>Email me when Bitcoin confirms</span>
618                  </label>
619                  <input type="email" id="text-notify-email" placeholder="[email protected]" autocomplete="email">
620                </div>
621
622                <button class="btn btn-primary" id="text-claim-btn">
623                  Create timestamp
624                </button>
625              </div>
626            </div>
627
628            <div id="text-result"></div>
629          </div>
630
631          <!-- Verify Tab -->
632          <div id="tab-verify" class="tab-content timestamp-panel" role="tabpanel" aria-labelledby="tab-button-verify" tabindex="0" hidden>
633            <div class="timestamp-panel-heading">
634              <span>03 / Independent check</span>
635              <h3>Verify a record</h3>
636              <p class="tab-intro">Use a file or SHA-256 hash to check if content has been timestamped.</p>
637            </div>
638
639            <div class="timestamp-action-grid timestamp-verify-grid">
640              <div class="dropzone" id="verify-dropzone" role="button" tabindex="0" aria-label="Choose a file to verify">
641                <div class="dropzone-icon">
642                  <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="color: var(--accent);">
643                    <circle cx="11" cy="11" r="8"/>
644                    <path d="m21 21-4.35-4.35"/>
645                  </svg>
646                </div>
647                <div class="dropzone-text">
648                  Drop file to verify
649                  <small>Hash calculated on this device</small>
650                </div>
651                <input type="file" id="verify-file-input" hidden>
652              </div>
653
654              <div class="timestamp-side-stack">
655                <div class="or-divider">or paste hash</div>
656                <div class="form-group">
657                  <label class="sr-only" for="verify-hash-input">SHA-256 hash</label>
658                  <input type="text" id="verify-hash-input" placeholder="SHA-256 hash" style="font-family: monospace; font-size: 0.75rem;">
659                </div>
660
661                <button class="btn btn-primary" id="verify-btn">Verify</button>
662              </div>
663            </div>
664
665            <div id="verify-result"></div>
666          </div>
667
668          <!-- Compare Tab -->
669          <div id="tab-compare" class="tab-content timestamp-panel" role="tabpanel" aria-labelledby="tab-button-compare" tabindex="0" hidden>
670            <div class="timestamp-panel-heading">
671              <span>04 / Bulk status</span>
672              <h3>Compare files</h3>
673              <p class="tab-intro">Find duplicates and check registration status across multiple files.</p>
674            </div>
675
676            <div class="timestamp-action-grid timestamp-compare-grid">
677              <div class="dropzone" id="compare-dropzone" role="button" tabindex="0" aria-label="Cho
677ose files to compare">
678                <div class="dropzone-icon">
679                  <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="color: var(--accent);">
680                    <path d="M20 9V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v3"/>
681                    <path d="M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3"/>
682                    <path d="M12 12v9"/>
683                    <path d="m8 8 4-4 4 4"/>
684                  </svg>
685                </div>
686                <div class="dropzone-text">
687                  Drop multiple files to compare
688                  <small>Duplicate detection stays local</small>
689                </div>
690                <input type="file" id="compare-file-input" multiple hidden>
691              </div>
692
693              <div class="timestamp-side-stack">
694                <button class="btn btn-primary" id="compare-btn" disabled>Compare Files</button>
695              </div>
696            </div>
697
698            <div class="file-list" id="compare-file-list"></div>
699            <div id="compare-result"></div>
700          </div>
701
702          <!-- Receipt Tab -->
703          <div id="tab-receipt" class="tab-content timestamp-panel" role="tabpanel" aria-labelledby="tab-button-receipt" tabindex="0" hidden>
704            <div class="timestamp-panel-heading">
705              <span>05 / Receipt ledger</span>
706              <h3>History</h3>
707              <p class="tab-intro">Receipts saved in this browser only. <strong>No cloud sync.</strong> Use the backup below to move them to another device.</p>
708
709              <div class="cross-device-note" style="margin: 1rem 0; padding: 0.75rem 1rem; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-muted); font-size: 0.9rem;">
710                <strong>Cross-device backup (by design):</strong> Receipts live only in this browser. 
711                No cloud sync — this is intentional for privacy and zero-knowledge (we never see your data).
712                To use on another device: export an encrypted backup, then import on the other browser.
713                <a href="/about">Read why on the About page</a>.
714                <div style="margin-top: 0.5rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
715                  <button type="button" class="btn btn-secondary btn-small" id="receipt-export-backup-btn">Export encrypted backup</button>
716                  <button type="button" class="btn btn-secondary btn-small" id="receipt-export-plain-btn" title="Plain JSON (not encrypted) — only for your own devices">Export plain JSON</button>
717                  <label class="btn btn-secondary btn-small" style="cursor:pointer;">
718                    Import backup
719                    <input type="file" id="receipt-import-backup-input" accept=".json,application/json" hidden>
720                  </label>
721                </div>
722              </div>
723
724              <div id="bitcoin-toast" class="bitcoin-toast" role="status" aria-live="polite" hidden></div>
725            </div>
726
727            <div id="receipts-loading" style="text-align: center; padding: 2rem; color: var(--text-dim);">
728              <span class="spinner"></span> Loading your receipts...
729            </div>
730
731            <div id="receipts-list"></div>
732            <div id="receipt-detail" style="display: none; margin-top: 1rem;"></div>
733          </div>
734
735          <!-- Keys Tab -->
736          <div id="tab-keys" class="tab-content timestamp-panel" role="tabpanel" aria-labelledby="tab-button-keys" tabindex="0" hidden>
737            <div class="timestamp-panel-heading">
738              <span>06 / Local identity</span>
739              <h3>Browser keys</h3>
740              <p class="tab-intro">Manage the Ed25519 signing key stored locally in this browser.</p>
741            </div>
742
743            <div class="timestamp-keys-grid">
744              <div class="form-group">
745                <label for="pubkey-display">Your Public Key</label>
746                <input type="text" id="pubkey-display" readonly placeholder="No key generated yet" style="font-family: monospace; font-size: 0.75rem;">
747              </div>
748
749              <div class="key-actions">
750                <button class="btn btn-primary" id="generate-key-btn">
751                  Generate New Key
752                </button>
753                <button class="btn btn-secondary" id="export-key-btn">
754                  Export Keys
755                </button>
756              </div>
757
758              <div class="form-group">
759                <label for="import-key-input">Import Keys (JSON)</label>
760                <input type="file" id="import-key-input" accept=".json">
761              </div>
762
763              <div class="security-note">
764                <strong>Important:</strong> Your private key is stored in this browser only. 
765                Export and backup your keys + receipts. <a href="/about">Why this design?</a>
766              </div>
767
768              <div class="info-card" style="margin-top:1rem;">
769                <h4>Encrypted backup</h4>
770                <p style="font-size:0.85rem;color:var(--text-dim);">Exports keys + all your local receipts/signings. Best used via the <strong>History</strong> tab for receipt-focused backup. <a href="/about">Why local-only?</a></p>
771                <div class="key-actions" style="margin-top:0.75rem;">
772                  <button type="button" class="btn btn-secondary" id="export-backup-btn">Export backup</button>
773                  <label class="btn btn-secondary" style="cursor:pointer;">
774                    Import backup
775                    <input type="file" id="import-backup-input" accept=".json,application/json" hidden>
776                  </label>
777                </div>
778              </div>
779            </div>
780          </div>
781
782          <!-- Integrations Tab -->
783          <div id="tab-integrations" class="tab-content timestamp-panel timestamp-integrations-panel" role="tabpanel" aria-labelledby="tab-button-integrations" tabindex="0" hidden>
784            <div class="timestamp-panel-heading">
785              <span>07 / Developer surfaces</span>
786              <h3>Integrations and developer surfaces</h3>
787              <p>Connect OTRUST where teams already work: browser, CLI, API, bots, SDKs, and hosted Auth flows.</p>
788            </div>
789
790            <div class="timestamp-integrations-grid">
791              <a href="#timestamp-tool" class="feature-card" onclick="showTab('file'); return false;">
792                <div class="feature-icon">
793                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
794                    <circle cx="12" cy="12" r="10"/>
795                    <path d="M12 6v6l4 2"/>
796                  </svg>
797                </div>
798                <h3>Web Tool</h3>
799                <p>Timestamp files directly in your browser. Hash computed locally, never uploaded.</p>
800              </a>
801
802              <a href="/docs.html#cli" class="feature-card">
803                <div class="feature-icon">
804                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
805                    <rect x="3" y="3" width="18" height="18" rx="2"/>
806                    <path d="M7 8l4 4-4 4"/>
807                    <path d="M13 16h4"/>
808                  </svg>
809                </div>
810                <h3>CLI Tool</h3>
811                <p>Command-line interface for automation and scripting. Batch timestamp hundreds of files.</p>
812              </a>
813
814              <a href="#timestamp-tool" class="feature-card" onclick="showTab('verify'); return false;">
815                <div class="feature-icon">
816                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
817                    <path d="M9 12l2 2 4-4"/>
818                    <path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/>
819                  </svg>
820                </div>
821                <h3>Verification</h3>
822                <p>Verify timestamp receipts against OpenTimestamps and Bitcoin anchoring.</p>
823              </a>
824
825              <a href="/docs#selfhost" class="feature-card">
826                <div class="feature-icon">
827                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
828                    <rect x="2" y="3" width="20" height="14" rx="2"/>
829                    <path d="M8 21h8"/>
830                    <path d="M12 17v4"/>
831                  </svg>
832                </div>
833                <h3>Self-Hosting</h3>
834                <p>Run the open source OTRUST server, web UI, SDKs, and CLI from GitHub.</p>
835              </a>
836
837              <a href="https://chromewebstore.google.com/detail/otrust-blockchain-timesta/gadpcgaelaihbnnijgkcfmabdmkgpdol" target="_blank" rel="noopener noreferrer" class="feature-card">
838                <div class="feature-icon">
839                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
840                    <path d="M12 2L2 7l10 5 10-5-10-5z"/>
841                    <path d="M2 17l10 5 10-5"/>
842                    <path d="M2 12l10 5 10-5"/>
843                  </svg>
844                </div>
845                <h3>Browser Extension</h3>
846                <p>Timestamp any webpage with one click. Install from Chrome Web Store.</p>
847              </a>
848
849              <a href="/bookmarklet" class="feature-card">
850                <div class="feature-icon">
851                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
852                    <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
853                  </svg>
854                </div>
855                <h3>Bookmarklet</h3>
856                <p>Verify the current page without installing an extension. Drag to bookmarks bar.</p>
857              </a>
858
859              <a href="/embed" class="feature-card">
860                <div class="feature-icon">
861                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
862                    <rect x="3" y="3" width="7" height="7"/>
863                    <rect x="14" y="3" width="7" height="7"/>
864                    <rect x="3" y="14" width="7" height="7"/>
865                    <rect x="14" y="14" width="7" height="7"/>
866                  </svg>
867                </div>
868                <h3>Embed badge</h3>
869                <p>
869Show live OTRUST anchored-record stats on your website.</p>
870              </a>
871
872              <a href="https://chatgpt.com/g/g-694d4a7fa8f48191aeb1db85f87fae5a-otrust-timestamper" target="_blank" rel="noopener noreferrer" class="feature-card">
873                <div class="feature-icon">
874                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
875                    <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
876                    <path d="M8 9h8"/>
877                    <path d="M8 13h6"/>
878                  </svg>
879                </div>
880                <h3>ChatGPT Integration</h3>
881                <p>Timestamp conversations directly in ChatGPT. Just ask the GPT to timestamp your chat.</p>
882              </a>
883
884              <a href="/docs.html#email" class="feature-card">
885                <div class="feature-icon">
886                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
887                    <path d="M4 4h16v16H4z"/>
888                    <path d="M4 6l8 6 8-6"/>
889                  </svg>
890                </div>
891                <h3>Email Timestamper</h3>
892                <p>Send an email to timestamp&#64;otrust.eu. Get a receipt and verification links back.</p>
893              </a>
894
895              <a href="https://t.me/OTRUSTbot" target="_blank" rel="noopener noreferrer" class="feature-card">
896                <div class="feature-icon">
897                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
898                    <path d="M22 2L11 13"/>
899                    <path d="M22 2L15 22L11 13L2 9L22 2Z"/>
900                  </svg>
901                </div>
902                <h3>Telegram Bot</h3>
903                <p>Timestamp messages and files directly in Telegram. Works in groups too.</p>
904              </a>
905
906              <a href="/docs.html#whatsapp" class="feature-card">
907                <div class="feature-icon">
908                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
909                    <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
910                  </svg>
911                </div>
912                <h3>WhatsApp Bot</h3>
913                <p>Enterprise WhatsApp Business API integration. Contact us for setup.</p>
914              </a>
915
916              <a href="/sign-in" class="feature-card">
917                <div class="feature-icon">
918                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
919                    <path d="M12 2l7 4v6c0 4.4-2.8 8.3-7 9.7C7.8 20.3 5 16.4 5 12V6l7-4z"/>
920                    <path d="M9 12l2 2 4-4"/>
921                  </svg>
922                </div>
923                <h3>Auth integration preview</h3>
924                <p>Review the issuer-bound challenge and callback design. Production token issuance is disabled.</p>
925              </a>
926
927              <a href="/playground/" class="feature-card">
928                <div class="feature-icon">
929                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
930                    <path d="M16 18l6-6-6-6"/>
931                    <path d="M8 6l-6 6 6 6"/>
932                    <path d="M14 4l-4 16"/>
933                  </svg>
934                </div>
935                <h3>SDK Playground</h3>
936                <p>Interactive playground to test the TypeScript/JavaScript SDK. Try all API functions live.</p>
937              </a>
938
939              <a href="/notes/why-otrust" class="feature-card">
940                <div class="feature-icon">
941                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
942                    <path d="M4 19.5V4a2 2 0 0 1 2-2h9l5 5v12.5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"/>
943                    <path d="M14 2v6h6"/>
944                    <path d="M8 13h8"/>
945                    <path d="M8 17h5"/>
946                  </svg>
947                </div>
948                <h3>Technical Note</h3>
949                <p>Why OTRUST uses blind notarization, visible trust boundaries, and ID-first identity.</p>
950              </a>
951
952              <a href="/api-docs.html" class="feature-card">
953                <div class="feature-icon">
954                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
955                    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
956                    <polyline points="14 2 14 8 20 8"/>
957                    <line x1="16" y1="13" x2="8" y2="13"/>
958                    <line x1="16" y1="17" x2="8" y2="17"/>
959                    <line x1="10" y1="9" x2="8" y2="9"/>
960                  </svg>
961                </div>
962                <h3>REST API Docs</h3>
963                <p>Full API documentation with examples. OpenAPI spec available for code generation.</p>
964              </a>
965            </div>
966          </div>
967        </div>
968      </div>
969    </div>
970  </section>
971  </section>
972
973  </section>
974
975  </main>
976  
977  <div class="footer-wrapper">
978  <footer role="contentinfo">
979    <div class="footer-brand">OTRUST</div>
980    <div class="footer-copy">&copy; 2026 OTRUST</div>
981    <div class="footer-made">
981Made in Sweden, Europe</div>
982    <div class="footer-links">
983      <a href="/timestamp">Timestamp</a>
984      <a href="/sign">Sign</a>
985      <a href="/sign-in">Auth</a>
986      <a href="/docs">Docs</a>
987      <a href="/api-docs">API</a>
988      <a href="/transparency">Trust Log</a>
989      <a href="/notes/why-otrust">Technical Note</a>
990      <a href="/privacy-policy">Privacy</a>
991      <a href="/terms">Terms</a>
992      <a href="https://github.com/otrust-eu/opensource" rel="noopener noreferrer">GitHub</a>
993      <a href="/about">About</a>
994    </div>
995    <a class="footer-dot" href="/" aria-label="Back to top"></a>
996  </footer>
997  </div>
998  
999  
999<script nonce="XJW8b56PBiVK2PYTvTXDjQ==">
1000    (() => {
1001      const normalizedPath = window.location.pathname.replace(/\/+$/, '') || '/';
1002      const isTimestampMode = normalizedPath === '/timestamp' || normalizedPath === '/index.html';
1003      const page = isTimestampMode ? 'timestamp' : 'home';
1004      document.documentElement.setAttribute('data-otrust-page', page);
1005      document.addEventListener('DOMContentLoaded', () => {
1006        document.body.setAttribute('data-otrust-page', page);
1007        const cardGrid = document.getElementById('dashboard-card-grid');
1008        const toolShell = document.getElementById('dashboard-tool-shell');
1009        if (cardGrid) cardGrid.hidden = isTimestampMode;
1010        if (toolShell) toolShell.hidden = !isTimestampMode;
1011      }, { once: true });
1012    })();
1013  </script>
1013
1014  
1014<script type="module" nonce="XJW8b56PBiVK2PYTvTXDjQ==">
1015    const bytesToHex = (bytes) => Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes))
1016      .map((byte) => byte.toString(16).padStart(2, '0'))
1017      .join('');
1018
1019    const hexToBytes = (hex) => {
1020      const clean = String(hex || '').trim();
1021      if (!/^[a-f0-9]*$/i.test(clean) || clean.length % 2) throw new Error('Invalid hex value');
1022      const bytes = new Uint8Array(clean.length / 2);
1023      for (let i = 0; i < bytes.length; i += 1) {
1024        bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
1025      }
1026      return bytes;
1027    };
1028
1029    const toBytes = (value) => value instanceof Uint8Array ? value : new TextEncoder().encode(String(value));
1030    async function sha256Bytes(value) {
1031      return new Uint8Array(await crypto.subtle.digest('SHA-256', toBytes(value)));
1032    }
1033    async function sha256Hex(value) {
1034      return bytesToHex(await sha256Bytes(value));
1035    }
1036
1037    let ed25519Promise;
1038    async function loadEd25519() {
1039      if (!ed25519Promise) {
1040        ed25519Promise = import('https://esm.sh/@noble/[email protected]');
1041      }
1042      return ed25519Promise;
1043    }
1044    
1045    // Dark mode initialization and toggle
1046    function initTheme() {
1047      // Monochrome edition: always light.
1048      document.documentElement.removeAttribute('data-theme');
1049      localStorage.setItem('theme', 'light');
1050    }
1051    
1052    document.getElementById('theme-toggle')?.addEventListener('click', () => {
1053      const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
1054      if (isDark) {
1055        document.documentElement.removeAttribute('data-theme');
1056        localStorage.setItem('theme', 'light');
1057      } else {
1058        document.documentElement.setAttribute('data-theme', 'dark');
1059        localStorage.setItem('theme', 'dark');
1060      }
1061    });
1062    
1063    initTheme();
1064
1065    function syncDashboardMode() {
1066      const normalizedPath = window.location.pathname.replace(/\/+$/, '') || '/';
1067      const isTimestampMode = normalizedPath === '/timestamp' || normalizedPath === '/index.html';
1068      const page = isTimestampMode ? 'timestamp' : 'home';
1069      document.documentElement.setAttribute('data-otrust-page', page);
1070      document.body.setAttribute('data-otrust-page', page);
1071
1072      const cardGrid = document.getElementById('dashboard-card-grid');
1073      const toolShell = document.getElementById('dashboard-tool-shell');
1074      if (cardGrid) cardGrid.hidden = isTimestampMode;
1075      if (toolShell) toolShell.hidden = !isTimestampMode;
1076    }
1077
1078    syncDashboardMode();
1079    
1080    // Mobile menu toggle
1081    const mobileMenuBtn = document.getElementById('mobile-menu-btn');
1082    const navSecondary = document.getElementById('nav-secondary');
1083    
1084    mobileMenuBtn?.addEventListener('click', (e) => {
1085      e.stopPropagation();
1086      navSecondary.classList.toggle('open');
1087      mobileMenuBtn.textContent = navSecondary.classList.contains('open') ? '\u00D7' : '';
1088    });
1089    
1090    // Close menu when clicking outside
1091    document.addEventListener('click', (e) => {
1092      if (!e.target.closest('.nav-links') && !e.target.closest('.docs-submenu-bar')) {
1093        navSecondary?.classList.remove('open');
1094        if (mobileMenuBtn) mobileMenuBtn.textContent = '';
1095        // Close docs submenu when clicking outside
1096        docsSubmenuBar?.classList.remove('open');
1097        docsTrigger?.classList.remove('open');
1098      }
1099    });
1100    
1101    // Docs submenu bar toggle
1102    const docsTrigger = document.getElementById('docs-trigger');
1103    const docsSubmenuBar = document.getElementById('docs-submenu-bar');
1104    
1105    docsTrigger?.addEventListener('click', (e) => {
1106      e.preventDefault();
1107      e.stopPropagation();
1108      const isOpen = docsSubmenuBar.classList.toggle('open');
1109      docsTrigger.classList.toggle('open', isOpen);
1110    });
1111
1112    // Utility functions
1113    function escapeHtml(str) {
1114      if (!str) return '';
1115      return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1116    }
1117
1118    const NOTIFY_EMAIL_KEY = 'otrust_notify_email';
1119
1120    function getNotifyEmail(inputId, optInId) {
1121      const optIn = document.getElementById(optInId);
1122      if (optIn && !optIn.checked) return '';
1123      const value = document.getElementById(inputId)?.value?.trim() || '';
1124      if (value) localStorage.setItem(NOTIFY_EMAIL_KEY, value);
1125      return value;
1126    }
1127
1128    function initNotifyEmailFields() {
1129      const saved = localStorage.getItem(NOTIFY_EMAIL_KEY) || '';
1130      const pairs = [
1131        ['notify-email', 'notify-email-opt-in'],
1132        ['text-notify-email', 'text-notify-email-opt-in']
1133      ];
1134      pairs.forEach(([inputId, optInId]) => {
1135        const input = document.getElementById(inputId);
1136        const optIn = document.getElementById(optInId);
1137        if (!input) return;
1138        if (saved) input.value = saved;
1139        const syncSaved = () => {
1140          const value = input.value.trim();
1141          if (value) localStorage.setItem(NOTIFY_EMAIL_KEY, value);
1142        };
1143        input.addEventListener('change', syncSaved);
1144        input.addEventListener('blur', syncSaved);
1145        optIn?.addEventListener('change', () => {
1146          input.disabled = optIn && !optIn.checked;
1147        });
1148        if (optIn) input.disabled = !optIn.checked;
1149      });
1150    }
1151    initNotifyEmailFields();
1152    
1153    // State
1154    let files = [];
1155    let keys = null;
1156    
1157    // DOM Elements
1158    const dropzone = document.getElementById('dropzone');
1159    const fileInput = document.getElementById('file-input');
1160    const fileList = document.getElementById('file-list');
1161    const claimBtn = document.getElementById('claim-btn');
1162    const progressEl = document.getElementById('progress');
1163    const resultEl = document.getElementById('result');
1164    
1165    // Load or generate keys
1166    async function loadKeys() {
1167      const stored = localStorage.getItem('otrust_keys');
1168      if (stored) {
1169        keys = JSON.parse(stored);
1170      }
1171      updateKeyDisplay();
1172    }
1173    
1174    async function generateKeys() {
1175      const ed = await loadEd25519();
1176      const privateKey = ed.utils.randomPrivateKey();
1177      const publicKey = await ed.getPublicKeyAsync(privateKey);
1178      keys = {
1179        privateKey: bytesToHex(privateKey),
1180        publicKey: bytesToHex(publicKey)
1181      };
1182      localStorage.setItem('otrust_keys', JSON.stringify(keys));
1183      updateKeyDisplay();
1184    }
1185    
1186    function updateKeyDisplay() {
1187      const display = document.getElementById('pubkey-display');
1188      if (keys) {
1189        display.value = keys.publicKey;
1190        // Update history count badge
1191        updateHistoryCount();
1192      }
1193    }
1194    
1195    function updateHistoryCount() {
1196      const countBadge = document.getElementById('history-count');
1197      const count = window.otrustReceiptHistory?.getMyReceipts().length || 0;
1198      if (count > 0) {
1199        countBadge.textContent = count;
1200        countBadge.style.display = 'inline';
1201      } else {
1202        countBadge.style.display = 'none';
1203      }
1204    }
1205
1206    function rememberReceipt(entry) {
1207      window.otrustReceiptHistory?.addToMyReceipts(entry);
1208      updateHistoryCount();
1209    }
1210    
1211    loadKeys().catch((error) => {
1212      console.warn('Signing keys will be generated when needed:', error.message);
1213    });
1214    
1215    function switchTab(tabName) {
1216      // Whitelist allowed tab names to prevent XSS
1217      const allowedTabs = ['file', 'text', 'verify', 'compare', 'receipt', 'keys', 'integrations'];
1218      if (!allowedTabs.includes(tabName)) return;
1219
1220      const targetTab = document.querySelector(`.tab[data-tab="${tabName}"]`);
1221      const targetContent = document.getElementById('tab-' + tabName);
1222      
1223      // Only update if target exists and isn't already active
1224      if (!targetTab || !targetContent || targetTab.classList.contains('active')) return;
1225      
1226      // Fade out current, then switch tabs, then fade in new
1227      requestAnimationFrame(() => {
1228        // Remove active from all
1229        document.querySelectorAll('.tab').forEach(t => {
1230          t.classList.remove('active');
1231          t.setAttribute('aria-selected', 'false');
1232        });
1233        document.querySelectorAll('.tab-content').forEach(c => {
1234          c.classList.remove('active');
1235          c.hidden = true;
1236        });
1237        
1238        // Immediately add active to new tab/content
1239        targetTab.classList.add('active');
1240        targetTab.setAttribute('aria-selected', 'true');
1241        targetContent.hidden = false;
1242        targetContent.classList.add('active');
1243      });
1244    }
1245    
1246    // Expose showTab globally for onclick handlers
1247    window.showTab = switchTab;
1248    
1249    document.querySelectorAll('.tab[data-tab]').forEach(tab => {
1250      tab.addEventListener('click', () => {
1251        switchTab(tab.dataset.tab);
1252      });
1253    });
1254
1255    function enableDropzoneKeyboard(zone, input) {
1256      zone?.addEventListener('keydown', (event) => {
1257        if (event.key === 'Enter' || event.key === ' ') {
1258          event.preventDefault();
1259          input?.click();
1260        }
1261      });
1262    }
1263    
1264    // File handling
1265    enableDropzoneKeyboard(dropzone, fileInput);
1266    dropzone.addEventListener('click', () => fileInput.click());
1267    dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('dragover'); });
1268    dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover'));
1269    dropzone.addEventListener('drop', async e => {
1270      e.preventDefault();
1271      dropzone.classList.remove('dragover');
1272      await addFiles(e.dataTransfer.files);
1273    });
1274    fileInput.addEventListener('change', async () => {
1275      await addFiles(fileInput.files);
1276      fileInput.value = '';
1277    });
1278    
1279    async function addFiles(fileList) {
1280      for (const file of fileList) {
1281        const buffer = await file.arrayBuffer();
1282        const hash = await sha256Hex(new Uint8Array(buffer));
1283        
1284        // Check if this hash already exists (timestamp or sign request)
1285        let existingData = null;
1286        try {
1287          const lookupRes = await fetch('/lookup/' + hash);
1288          const lookupData = await lookupRes.json();
1289          if (lookupData.exists) {
1290            existingData = lookupData;
1291          }
1292        } catch (e) {
1293          console.error('Lookup failed:', e);
1294        }
1295        
1296        files.push({ name: file.name, size: file.size, hash, existingData });
1297        window.otrustTrackUsage?.('hash_computed');
1298      }
1299      renderFiles();
1300    }
1301    
1302    function renderFiles() {
1303      fileList.innerHTML = files.map((f, i) => {
1304        let warningHtml = '';
1305        if (f.existingData) {
1306          if (f.existingData.timestamp) {
1307            const ts = f.existingData.timestamp;
1308            warningHtml += `
1309              <div class="existing-warning">
1310                Timestamped on ${new Date(ts.created_at).toLocaleDateString()}
1311                ${ts.blockchain_confirmed ? 'Confirmed' : 'Pending'}
1312                <a href="/proof/${ts.receipt_id}" target="_blank" rel="noopener noreferrer" title="View this timestamp receipt">View receipt</a>
1313              </div>
1314            `;
1315          }
1316          if (f.existingData.sign_request) {
1317            const sr = f.existingData.sign_request;
1318            warningHtml += `
1319              <div class="existing-warning sign-warning">
1320                Already in signing session "${escapeHtml(sr.title)}" (${sr.status})
1321              </div>
1322            `;
1323          }
1324        }
1325        
1326        return `
1327          <div class="file-item${f.existingData?.exists ? ' already-timestamped' : ''}">
1328            <div>
1329              <div class="name">${escapeHtml(f.name)}</div>
1330              <div class="hash">${f.hash.substring(0, 16)}...</div>
1331              ${warningHtml}
1332            </div>
1333            <button class="remove" onclick="removeFile(${i})">&times;</button>
1334          </div>
1335        `;
1336      }).join('');
1337      claimBtn.disabled = files.length === 0;
1338      claimBtn.textContent = files.length > 0 
1339        ? `Create timestamp (${files.length} file${files.length > 1 ? 's' : ''})`
1340        : 'Create timestamp';
1341    }
1342    
1343    window.removeFile = function(i) {
1344      files.splice(i, 1);
1345      renderFiles();
1346    };
1347    
1348    // Claim process
1349    claimBtn.addEventListener('click', async () => {
1350      if (files.length === 0) return;
1351      
1352      claimBtn.disabled = true;
1353      progressEl.style.display = 'block';
1354      resultEl.innerHTML = '';
1355      
1356      try {
1357        // Step 1: Get challenge
1358        setStep('pow', 'active');
1359        if (!keys?.privateKey || !keys?.publicKey) await generateKeys();
1360        const challengeRes = await fetch('/challenge');
1361        const { challenge, difficulty } = await challengeRes.json();
1362        
1363        // Solve PoW
1364        const nonce = await solvePow(challenge, difficulty);
1365        setStep('pow', 'done');
1366        
1367        // Step 2: Sign
1368        setStep('sign', 'active');
1369        const ed = await loadEd25519();
1370        const claims = [];
1371        for (const file of files) {
1372          const sig = await ed.signAsync(hexToBytes(file.hash), hexToBytes(keys.privateKey));
1373          claims.push({
1374            hash: file.hash,
1375            signature: bytesToHex(sig),
1376            pubkey: keys.publicKey,
1377            filename: file.name || undefined
1378          });
1379        }
1380        setStep('sign', 'done');
1381        
1382        // Step 3: Submit
1383        setStep('submit', 'active');
1384        const notifyEmail = getNotifyEmail('notify-email', 'notify-email-opt-in');
1385        
1386        let res;
1387        if (claims.length === 1) {
1388          res = await fetch('/claim', {
1389            method: 'POST',
1390            headers: { 'Content-Type': 'application/json' },
1391            body: JSON.stringify({
1392              ...claims[0],
1393              pow: { challenge, nonce },
1394              notify_email: notifyEmail || undefined
1395            })
1396          });
1397        } else {
1398          res = await fetch('/claim/bulk', {
1399            method: 'POST',
1400            headers: { 'Content-Type': 'application/json' },
1401            body: JSON.stringify({
1402              claims,
1403              pow: { challenge, nonce },
1404              notify_email: notifyEmail || undefined
1405            })
1406          });
1407        }
1408        
1409        const data = await res.json();
1410        setStep('submit', 'done');
1411        
1412        // Show result
1413        if (res.ok) {
1414          // Handle "already registered" as a warning case
1415          if (data.status === 'already_registered') {
1416            rememberReceipt({
1417              receipt_id: data.receipt_id,
1418              hash: claims[0].hash,
1419              filename: files[0]?.name || claims[0].filename,
1420              timestamp: data.timestamp,
1421              blockchain_confirmed: !!data.blockchain_confirmed
1422            });
1423            resultEl.innerHTML = `
1424              <div class="result result-warning">
1425                <h4>Already timestamped</h4>
1426                <p style="color: var(--text-dim); font-size: 0.8rem; margin-bottom: 1rem;">This content was already registered on <strong>${new Date(data.timestamp).toLocaleDateString()}</strong>. No new timestamp created.</p>
1427                <div class="result-row"><span class="label">Receipt ID</span><span class="value">${data.receipt_id}</span></div>
1428                <div class="result-row"><span class="label">Blockchain</span><span class="value">${data.blockchain_confirmed ? 'Confirmed' : 'Pending confirmation'}</span></div>
1429              </div>`;
1430            files = [];
1431            renderFiles();
1432            claimBtn.disabled = false;
1433            setTimeout(() => { progressEl.style.display = 'none'; }, 2000);
1434            return;
1435          }
1436          
1437          if (claims.length === 1) {
1438            const claimFilename = files[0]?.name || '';
1439            rememberReceipt({
1440              receipt_id: data.receipt_id,
1441              hash: files[0]?.hash || claims[0].hash,
1442              filename: claimFilename || claims[0].filename,
1443              timestamp: data.timestamp,
1444              blockchain_confirmed: data.blockchain_status === 'confirmed'
1445            });
1446            const emailNote = notifyEmail
1447              ? `<p class="blockchain-pending-note">We'll email <strong>${escapeHtml(notifyEmail)}</strong> when Bitcoin confirms.</p>`
1448              : '';
1449            const pendingNote = data.blockchain_status !== 'confirmed'
1450              ? '<p class="blockchain-pending-note">Bitcoin confirmation via OpenTimestamps typically takes 1–2 hours. Save your receipt ID to verify later.</p>'
1451              : '';
1452            const verifyUrl = `${window.location.origin}/proof/${data.receipt_id}`;
1453            const qrHtml = `<div class="receipt-qr"><img src="/api/qr?size=120&data=${encodeURIComponent(verifyUrl)}" width="120" height="120" alt="QR code for verification URL"><p class="receipt-qr-caption">Scan to verify later</p></div>`;
1454            resultEl.innerHTML = `
1455              <div class="result">
1456                <h4>Timestamp created</h4>
1457                ${qrHtml}
1458                <div class="result-row"><span class="label">Receipt ID</span><span class="value">${data.receipt_id}</span></div>
1459                ${claimFilename ? `<div class="result-row"><span class="label">File</span><span class="value">${claimFilename}</span></div>` : ''}
1460                <div class="result-row"><span class="label">Timestamp</span><span class="value">${data.timestamp}</span></div>
1461                <div class="result-row"><span class="label">Blockchain</span><span class="value">${data.blockchain_status}</span></div>
1462                ${emailNote}${pendingNote}
1463                <div class="receipt-actions">
1464                  <button class="btn btn-secondary" onclick="downloadReceipt('${data.receipt_id}')">Download Receipt</button>
1465                  ${data.blockchain_status === 'confirmed' ? `<button class="btn btn-secondary" onclick="downloadCertificate('${data.receipt_id}', '${claims[0].hash}')">Download Certificate</button>` : ''}
1466                  <div id="share-actions-slot"></div>
1467                </div>
1468              </div>`;
1469            const shareOpts = {
1470              receiptId: data.receipt_id,
1471              hash: claims[0].hash,
1472              filename: claimFilename,
1473              blockchainStatus: data.blockchain_status,
1474              blockHeight: data.block_number,
1475              verifyUrl: verifyUrl,
1476              theme: 'minimal'
1477            };
1478            const shareSlot = document.getElementById('share-actions-slot');
1479            if (shareSlot && window.otrustShareProof) {
1480              shareSlot.innerHTML = window.otrustShareProof.shareActionsHtml(shareOpts);
1481              window.otrustShareProof.bindShareActions(shareSlot, shareOpts);
1482            }
1483          } else {
1484            (data.results || []).forEach((result, index) => {
1485              if (result.status !== 'created' && result.status !== 'duplicate') return;
1486              const claim = claims[result.index ?? index];
1487              if (!claim || !result.receipt_id) return;
1488              rememberReceipt({
1489                receipt_id: result.receipt_id,
1490                hash: claim.hash,
1491                filename: claim.filename,
1492                timestamp: result.timestamp || new Date().toISOString(),
1493                blockchain_confirmed: false
1494              });
1495            });
1496            const receiptIds = data.results?.filter(r => r.receipt_id).map(r => r.receipt_id) || [];
1497            const bulkEmailNote = notifyEmail && data.created > 0
1498              ? `<p class="blockchain-pending-note">We'll email <strong>${escapeHtml(notifyEmail)}</strong> as each receipt confirms on Bitcoin.</p>`
1499              : '';
1500            resultEl.innerHTML = `
1501              <div class="result">
1502                <h4>Batch processed</h4>
1503                ${bulkEmailNote}
1504                <div class="result-row"><span class="label">Created</span><span class="value">${data.created}</span></div>
1505                <div class="result-row"><span class="label">Duplicates</span><span class="value">${data.duplicates}</span></div>
1506                <div class="result-row"><span class="label">Errors</span><span class="value">${data.errors}</span></div>
1507                ${receiptIds.length > 0 ? `<div class="receipt-actions"><button class="btn btn-secondary" onclick="downloadBulkReceipts()">Download All Receipts</button></div>` : ''}
1508              </div>`;
1509            window.lastBulkResults = data.results;
1510          }
1511          files = [];
1512          renderFiles();
1513        } else {
1514          const errorMsg = escapeHtml(data.message || data.error || 'Something went wrong');
1515          resultEl.innerHTML = `
1516            <div style="padding: 1rem; background: #fef2f2; border-radius: 8px; margin-top: 1rem; border-left: 4px solid #dc2626;">
1517              <strong style="color: #dc2626;">${errorMsg}</strong>
1518              ${data.error === 'rate_limit_exceeded' ? '<p style="color: #666; font-size: 0.8rem; margin-top: 0.5rem;">Please wait a moment and try again.</p>' : ''}
1519            </div>`;
1520        }
1521        
1522      } catch (e) {
1523        resultEl.innerHTML = `<div style="color: #dc2626; padding: 1rem; background: #fef2f2; border-radius: 8px; margin-top: 1rem;">Error: ${escapeHtml(e.message)}</div>`;
1524      }
1525      
1526      claimBtn.disabled = false;
1527      setTimeout(() => { progressEl.style.display = 'none'; }, 2000);
1528    });
1529    
1530    function setStep(step, state) {
1531      const el = document.getElementById('step-' + step);
1532      el.className = 'progress-step ' + state;
1533      if (state === 'done') el.querySelector('.icon').textContent = '\u2713';
1534    }
1535    
1536    async function solvePow(challenge, difficulty) {
1537      let nonce = 0;
1538      while (true) {
1539        const nonceHex = nonce.toString(16).padStart(16, '0');
1540        const hash = await sha256Bytes(challenge + nonceHex);
1541        
1542        let zeroBits = 0;
1543        for (const byte of hash) {
1544          if (byte === 0) zeroBits += 8;
1545          else { zeroBits += Math.clz32(byte) - 24; break; }
1546          if (zeroBits >= difficulty) break;
1547        }
1548        
1549        if (zeroBits >= difficulty) return nonceHex;
1550        nonce++;
1551        if (nonce % 10000 === 0) await new Promise(r => setTimeout(r, 0));
1552      }
1553    }
1554    
1555    // Text claim
1556    document.getElementById('text-claim-btn').addEventListener('click', async () => {
1557      const text = document.getElementById('text-input').value.trim();
1558      if (!text) return alert('Enter some text');
1559      
1560      const btn = document.getElementById('text-claim-btn');
1561      btn.disabled = true;
1562      btn.innerHTML = '<span class="spinner"></span> Processing...';
1563      
1564      try {
1565        const hash = await sha256Hex(text);
1566        window.otrustTrackUsage?.('hash_computed');
1567        if (!keys?.privateKey || !keys?.publicKey) await generateKeys();
1568        
1569        const challengeRes = await fetch('/challenge');
1570        const { challenge, difficulty } = await challengeRes.json();
1571        const nonce = await solvePow(challenge, difficulty);
1572        
1573        const ed = await loadEd25519();
1574        const sig = await ed.signAsync(hexToBytes(hash), hexToBytes(keys.privateKey));
1575        const notifyEmail = getNotifyEmail('text-notify-email', 'text-notify-email-opt-in');
1576        
1577        const res = await fetch('/claim', {
1578          method: 'POST',
1579          headers: { 'Content-Type': 'application/json' },
1580          body: JSON.stringify({
1581            hash,
1582            signature: bytesToHex(sig),
1583            pubkey: keys.publicKey,
1584            pow: { challenge, nonce },
1585            notify_email: notifyEmail || undefined
1586          })
1587        });
1588        
1589        const data = await res.json();
1590        
1591        if (res.ok) {
1592          if (data.receipt_id) {
1593            rememberReceipt({
1594              receipt_id: data.receipt_id,
1595              hash,
1596              timestamp: data.timestamp,
1597              blockchain_confirmed: data.blockchain_status === 'confirmed' || !!data.blockchain_confirmed
1598            });
1599          }
1600          document.getElementById('text-result').innerHTML = `
1601            <div class="result">
1602              <h4>Timestamp created</h4>
1603              <div class="result-row"><span class="label">Hash</span><span class="value">${hash.substring(0, 24)}...</span></div>
1604              <div class="result-row"><span class="label">Receipt ID</span><span class="value">${escapeHtml(data.receipt_id)}</span></div>
1605              <div class="result-row"><span class="label">Timestamp</span><span class="value">${escapeHtml(data.timestamp)}</span></div>
1606            </div>`;
1607        } else if (data.status === 'already_registered' && data.receipt_id) {
1608          rememberReceipt({
1609            receipt_id: data.receipt_id,
1610            hash,
1611            timestamp: data.timestamp,
1612            blockchain_confirmed: !!data.blockchain_confirmed
1613          });
1614          document.getElementById('text-result').innerHTML = `
1615            <div class="result result-warning">
1616              <h4>Already timestamped</h4>
1617              <div class="result-row"><span class="label">Receipt ID</span><span class="value">${escapeHtml(data.receipt_id)}</span></div>
1618            </div>`;
1619        } else {
1620          document.getElementById('text-result').innerHTML = `<div style="color: #dc2626; padding: 1rem; background: #fef2f2; border-radius: 8px; margin-top: 1rem;">Error: ${escapeHtml(data.error || data.message)}</div>`;
1621        }
1622      } catch (e) {
1623        document.getElementById('text-result').innerHTML = `<div style="color: #dc2626; padding: 1rem; background: #fef2f2; border-radius: 8px; margin-top: 1rem;">Error: ${escapeHtml(e.message)}</div>`;
1624      }
1625      
1626      btn.disabled = false;
1627      btn.textContent = 'Claim This Text';
1628    });
1629    
1630    // Key management
1631    document.getElementById('generate-key-btn').addEventListener('click', async () => {
1632      if (confirm('Generate new keypair? Your old keys will be lost if not exported!')) {
1633        await generateKeys();
1634        alert('New keypair generated!');
1635      }
1636    });
1637    
1638    document.getElementById('export-key-btn').addEventListener('click', () => {
1639      if (!keys) return alert('No keys to export');
1640      const blob = new Blob([JSON.stringify({ ...keys, exportedAt: new Date().toISOString() }, null, 2)], { type: 'application/json' });
1641      const url = URL.createObjectURL(blob);
1642      const a = document.createElement('a');
1643      a.href = url;
1644      a.download = 'otrust-keys.json';
1645      a.click();
1646    });
1647    
1648    document.getElementById('import-key-input').addEventListener('change', async e => {
1649      const file = e.target.files[0];
1650      if (!file) return;
1651      try {
1652        const text = await file.text();
1653        const imported = JSON.parse(text);
1654        if (!imported.privateKey || !imported.publicKey) throw new Error('Invalid key file');
1655        keys = { privateKey: imported.privateKey, publicKey: imported.publicKey };
1656        localStorage.setItem('otrust_keys', JSON.stringify(keys));
1657        updateKeyDisplay();
1658        alert('Keys imported successfully!');
1659      } catch (err) {
1660        alert('Failed to import: ' + err.message);
1661      }
1662    });
1663    
1664    // ========== VERIFY TAB ==========
1665    const verifyDropzone = document.getElementById('verify-dropzone');
1666    const verifyFileInput = document.getElementById('verify-file-input');
1667    
1668    enableDropzoneKeyboard(verifyDropzone, verifyFileInput);
1669    verifyDropzone.addEventListener('click', () => verifyFileInput.click());
1670    verifyDropzone.addEventListener('dragover', e => { e.preventDefault(); verifyDropzone.classList.add('dragover'); });
1671    verifyDropzone.addEventListener('dragleave', () => verifyDropzone.classList.remove('dragover'));
1672    verifyDropzone.addEventListener('drop', async e => {
1673      e.preventDefault();
1674      verifyDropzone.classList.remove('dragover');
1675      if (e.dataTransfer.files[0]) {
1676        const buffer = await e.dataTransfer.files[0].arrayBuffer();
1677        const hash = await sha256Hex(new Uint8Array(buffer));
1678        window.otrustTrackUsage?.('hash_computed');
1679        document.getElementById('verify-hash-input').value = hash;
1680        doVerify(hash);
1681      }
1682    });
1683    verifyFileInput.addEventListener('change', async () => {
1684      if (verifyFileInput.files[0]) {
1685        const buffer = await verifyFileInput.files[0].arrayBuffer();
1686        const hash = await sha256Hex(new Uint8Array(buffer));
1687        window.otrustTrackUsage?.('hash_computed');
1688        document.getElementById('verify-hash-input').value = hash;
1689        doVerify(hash);
1690      }
1691    });
1692    
1693    document.getElementById('verify-btn').addEventListener('click', () => {
1694      const hash = document.getElementById('verify-hash-input').value.trim();
1695      if (!hash) return alert('Enter a hash or drop a file');
1696      doVerify(hash);
1697    });
1698    
1699    async function doVerify(hash) {
1700      const resultEl = document.getElementById('verify-result');
1701      resultEl.innerHTML = '<div style="text-align:center;padding:1rem;"><span class="spinner"></span> Checking...</div>';
1702      
1703      try {
1704        const res = await fetch('/verify', {
1705          method: 'POST',
1706          headers: { 'Content-Type': 'application/json' },
1707          body: JSON.stringify({ hash })
1708        });
1709        const data = await res.json();
1710        
1711        if (data.status === 'found') {
1712          const claim = data.claims[0];
1713          resultEl.innerHTML = '<div class="result">' +
1714            '<h4>Verified</h4>' +
1715            '<div class="result-row"><span class="label">Hash</span><span class="value">' + hash.substring(0, 20) + '...</span></div>' +
1716            '<div class="result-row"><span class="label">Timestamp</span><span class="value">' + claim.timestamp + '</span></div>' +
1717            '<div class="result-row"><span class="label">Receipt ID</span><span class="value">' + claim.receipt_id + '</span></div>' +
1718            '<div class="result-row"><span class="label">Blockchain</span><span class="value">' + (claim.blockchain_confirmed ? 'Confirmed' : 'Pending') + '</span></div>' +
1719            (data.claims.length > 1 ? '<div style="margin-top:0.5rem;font-size:0.8rem;color:var(--text-dim);">+ ' + (data.claims.length - 1) + ' more claim(s)</div>' : '') +
1720          '</div>';
1721        } else {
1722          resultEl.innerHTML = '<div style="background: #fef3c7; border: 1px solid #fcd34d; border-radius: 8px; padding: 1rem; margin-top: 1rem;">' +
1723            '<strong>Not Found</strong><br><span style="font-size:0.9rem;color:var(--text-dim);">This content has not been timestamped yet.</span></div>';
1724        }
1725      } catch (e) {
1726        resultEl.innerHTML = '<div style="color:#dc2626;padding:1rem;background:#fef2f2;border-radius:8px;margin-top:1rem;">Error: ' + e.message + '</div>';
1727      }
1728    }
1729    
1730    // ========== COMPARE TAB ==========
1731    let compareFiles = [];
1732    const compareDropzone = document.getElementById('compare-dropzone');
1733    const compareFileInput = document.getElementById('compare-file-input');
1734    const compareFileList = document.getElementById('compare-file-list');
1735    const compareBtn = document.getElementById('compare-btn');
1736    
1737    enableDropzoneKeyboard(compareDropzone, compareFileInput);
1738    compareDropzone.addEventListener('click', () => compareFileInput.click());
1739    compareDropzone.addEventListener('dragover', e => { e.preventDefault(); compareDropzone.classList.add('dragover'); });
1740    compareDropzone.addEventListener('dragleave', () => compareDropzone.classList.remove('dragover'));
1741    compareDropzone.addEventListener('drop', async e => {
1742      e.preventDefault();
1743      compareDropzone.classList.remove('dragover');
1744      await addCompareFiles(e.dataTransfer.files);
1745    });
1746    compareFileInput.addEventListener('change', async () => {
1747      await addCompareFiles(compareFileInput.files);
1748      compareFileInput.value = '';
1749    });
1750    
1751    async function addCompareFiles(fileList) {
1752      for (const file of fileList) {
1753        const buffer = await file.arrayBuffer();
1754        const hash = await sha256Hex(new Uint8Array(buffer));
1755        compareFiles.push({ name: file.name, hash });
1756        window.otrustTrackUsage?.('hash_computed');
1757      }
1758      renderCompareFiles();
1759    }
1760    
1761    function renderCompareFiles() {
1762      compareFileList.innerHTML = compareFiles.map((f, i) => 
1763        '<div class="file-item"><div><div class="name">' + escapeHtml(f.name) + '</div><div class="hash">' + f.hash.substring(0, 16) + '...</div></div>' +
1764        '<button class="remove" onclick="removeCompareFile(' + i + ')">&times;</button></div>'
1765      ).join('');
1766      compareBtn.disabled = compareFiles.length < 2;
1767      compareBtn.textContent = compareFiles.length > 0 ? 'Compare ' + compareFiles.length + ' Files' : 'Compare Files';
1768    }
1769    
1770    window.removeCompareFile = function(i) {
1771      compareFiles.splice(i, 1);
1772      renderCompareFiles();
1773    };
1774    
1775    compareBtn.addEventListener('click', async () => {
1776      if (compareFiles.length < 2) return;
1777      
1778      compareBtn.disabled = true;
1779      const resultEl = document.getElementById('compare-result');
1780      resultEl.innerHTML = '<div style="text-align:center;padding:1rem;"><span class="spinner"></span> Comparing...</div>';
1781      
1782      // Find duplicates locally
1783      const hashMap = {};
1784      compareFiles.forEach(f => {
1785        if (!hashMap[f.hash]) hashMap[f.hash] = [];
1786        hashMap[f.hash].push(f.name);
1787      });
1788      
1789      const duplicates = Object.entries(hashMap).filter(([h, names]) => names.length > 1);
1790      
1791      // Check registration status
1792      try {
1793        const res = await fetch('/verify/bulk', {
1794          method: 'POST',
1795          headers: { 'Content-Type': 'application/json' },
1796          body: JSON.stringify({ hashes: Object.keys(hashMap) })
1797        });
1798        const data = await res.json();
1799        
1800        let html = '<div style="margin-top:1rem;">';
1801        
1802        if (duplicates.length > 0) {
1803          html += '<div style="background:#fef3c7;border:1px solid #fcd34d;border-radius:8px;padding:1rem;margin-bottom:1rem;"><strong>Duplicates found</strong><ul style="margin:0.5rem 0 0 1rem;font-size:0.85rem;">';
1804          duplicates.forEach(([hash, names]) => {
1805            html += '<li>' + names.join(', ') + '</li>';
1806          });
1807          html += '</ul></div>';
1808        }
1809        
1810        const registered = data.results.filter(r => r.status === 'found').length;
1811        const unregistered = data.results.filter(r => r.status === 'not_found').length;
1812        
1813        html += '<div class="result"><h4>Results</h4>' +
1814          '<div class="result-row"><span class="label">Total Files</span><span class="value">' + compareFiles.length + '</span></div>' +
1815          '<div class="result-row"><span class="label">Unique Hashes</span><span class="value">' + Object.keys(hashMap).length + '</span></div>' +
1816          '<div class="result-row"><span class="label">Registered</span><span class="value">' + registered + '</span></div>' +
1817          '<div class="result-row"><span class="label">Not Registered</span><span class="value">' + unregistered + '</span></div>' +
1818          '</div></div>';
1819        
1820        resultEl.innerHTML = html;
1821      } catch (e) {
1822        resultEl.innerHTML = '<div style="color:#dc2626;padding:1rem;background:#fef2f2;border-radius:8px;margin-top:1rem;">Error: ' + escapeHtml(e.message) + '</div>';
1823      }
1824      
1825      compareBtn.disabled = false;
1826    });
1827    
1828    // ========== RECEIPT TAB (browser-local only) ==========
1829    let receiptPollTimer = null;
1830
1831    function showBitcoinToast(receiptId, blockHeight) {
1832      const toast = document.getElementById('bitcoin-toast');
1833      if (!toast) return;
1834      const msg = 'Bitcoin confirmed!';
1835      toast.innerHTML = `<strong>${msg}</strong> ${receiptId}${blockHeight ? ` · block ${blockHeight}` : ''}`;
1836      toast.hidden = false;
1837      toast.classList.add('visible');
1838      setTimeout(() => {
1839        toast.classList.remove('visible');
1840        toast.hidden = true;
1841      }, 8000);
1842    }
1843
1844    async function refreshPendingReceiptStatuses(receipts) {
1845      const pending = receipts.filter((r) => !r.blockchain_confirmed);
1846      await Promise.all(pending.slice(0, 20).map(async (receipt) => {
1847        try {
1848          const res = await fetch('/proof/' + receipt.receipt_id);
1849          if (!res.ok) return;
1850          const data = await res.json();
1851          if (data.blockchain?.confirmed) {
1852            window.otrustReceiptHistory?.updateReceiptInHistory(receipt.receipt_id, {
1853              blockchain_confirmed: true,
1854              blockchain_block: data.blockchain.block_height || null
1855            });
1856            showBitcoinToast(receipt.receipt_id, data.blockchain.block_height);
1857          }
1858        } catch {
1859          /* ignore */
1860        }
1861      }));
1862      return window.otrustReceiptHistory?.getMyReceipts() || receipts;
1863    }
1864
1865    function renderReceiptList(receipts) {
1866      const listEl = document.getElementById('receipts-list');
1867      if (!receipts.length) {
1868        listEl.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-dim);">No receipts in this browser yet. Create a timestamp to save one here.<br><small style="opacity:0.7">Why local-only? <a href="/about">Read about our design</a>. Use the backup buttons above to move between devices.</small></div>';
1869        updateHistoryCount();
1870        return;
1871      }
1872
1873      updateHistoryCount();
1874      listEl.innerHTML = '<div class="receipts-header"><strong>' + receipts.length + ' timestamp' + (receipts.length !== 1 ? 's' : '') + ' in this browser</strong></div>' +
1875        receipts.map(r => `
1876          <div class="receipt-item" data-receipt="${r.receipt_id}">
1877            <div class="receipt-info">
1878              <div class="receipt-id">${r.filename ? '<span class="receipt-filename">' + escapeHtml(r.filename) + '</span> <span class="receipt-id-small">' + r.receipt_id + '</span>' : r.receipt_id}</div>
1879              <div class="receipt-hash">${r.hash.substring(0, 24)}...</div>
1880            </div>
1881            <div class="receipt-meta">
1882              <div class="receipt-date">${new Date(r.timestamp).toLocaleDateString()}</div>
1883              <div class="receipt-status ${r.blockchain_confirmed ? 'confirmed' : 'pending'}">${r.blockchain_confirmed ? 'Confirmed' : 'Pending'}</div>
1884            </div>
1885          </div>
1886        `).join('');
1887
1888      listEl.querySelectorAll('.receipt-item').forEach(item => {
1889        item.addEventListener('click', () => showReceiptDetail(item.dataset.receipt));
1890      });
1891    }
1892
1893    function scheduleReceiptPoll() {
1894      clearInterval(receiptPollTimer);
1895      const hasPending = (window.otrustReceiptHistory?.getMyReceipts() || []).some((r) => !r.blockchain_confirmed);
1896      if (!hasPending) return;
1897      receiptPollTimer = setInterval(() => loadMyReceipts(), 90000);
1898    }
1899    
1900    async function loadMyReceipts() {
1901      const loadingEl = document.getElementById('receipts-loading');
1902      const listEl = document.getElementById('receipts-list');
1903      loadingEl.style.display = 'block';
1904      listEl.innerHTML = '';
1905
1906      let receipts = window.otrustReceiptHistory?.getMyReceipts() || [];
1907      if (receipts.some((r) => !r.blockchain_confirmed)) {
1908        receipts = await refreshPendingReceiptStatuses(receipts);
1909      }
1910
1911      loadingEl.style.display = 'none';
1912      renderReceiptList(receipts);
1913      scheduleReceiptPoll();
1914    }
1915    
1916    window.showReceiptDetail = async function(receiptId) {
1917      const detailEl = document.getElementById('receipt-detail');
1918      detailEl.style.display = 'block';
1919      detailEl.innerHTML = '<div style="text-align:center;padding:1rem;"><span class="spinner"></span></div>';
1920      detailEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1921      
1922      try {
1923        const res = await fetch('/proof/' + receiptId);
1924        const data = await res.json();
1925        
1926        if (data.error) {
1927          detailEl.innerHTML = '<div style="background:#fef3c7;border:1px solid #fcd34d;border-radius:8px;padding:1rem;"><strong>Could not load details</strong></div>';
1928          return;
1929        }
1930        
1931        const verifyUrl = window.location.origin + '/proof/' + receiptId;
1932        let html = '<div class="result"><h4>' + receiptId + '</h4>' +
1933          '<div class="receipt-qr"><img src="/api/qr?size=120&data=' + encodeURIComponent(verifyUrl) + '" width="120" height="120" alt="QR code for verification URL"><p class="receipt-qr-caption">Scan to verify</p></div>' +
1934          '<div class="result-row"><span class="label">Hash</span><span class="value" style="word-break:break-all;">' + (data.hash || 'N/A') + '</span></div>' +
1935          '<div class="result-row"><span class="label">Created</span><span class="value">' + (data.created_at || 'N/A') + '</span></div>' +
1936          '<div class="result-row"><span class="label">Blockchain</span><span class="value">' + (data.blockchain?.confirmed ? 'Block ' + data.blockchain.block_height : 'Pending — checking every 90s') + '</span></div>' +
1937          '<div class="receipt-actions">';
1938        
1939        if (data.download_url) {
1940          html += '<a href="' + data.download_url + '" class="btn btn-secondary" download>Download .ots</a>';
1941        }
1942        html += '<button class="btn btn-secondary" data-action="json" data-id="' + receiptId + '">JSON</button>';
1943        html += '<button class="btn btn-secondary" data-action="cert" data-id="' + receiptId + '" data-hash="' + (data.hash || '') + '">Cert</button>';
1944        html += '<button class="btn btn-secondary" data-action="close">Close</button>';
1945        html += '</div></div>';
1946        detailEl.innerHTML = html;
1947        
1948        // Add event listeners to buttons
1949        detailEl.querySelectorAll('button[data-action]').forEach(btn => {
1950          btn.addEventListener('click', async (e) => {
1951            const action = btn.dataset.action;
1952            if (action === 'json') {
1953              await window.downloadReceipt(btn.dataset.id);
1954            } else if (action === 'cert') {
1955              await window.downloadCertificate(btn.dataset.id, btn.dataset.hash);
1956            } else if (action === 'close') {
1957              detailEl.style.display = 'none';
1958            }
1959          });
1960        });
1961      } catch (e) {
1962        detailEl.innerHTML = '<div style="color:#dc2626;padding:1rem;background:#fef2f2;border-radius:8px;">Error: ' + escapeHtml(e.message) + '</div>';
1963      }
1964    };
1965    
1966    async function showReceiptDetail(receiptId) {
1967      await window.showReceiptDetail(receiptId);
1968    }
1969    
1970    // Make loadMyReceipts global for "back to my history" link
1971    window.loadMyReceipts = loadMyReceipts;
1972    
1973    // Load receipts when tab is clicked
1974    document.querySelector('[data-tab="receipt"]').addEventListener('click', () => loadMyReceipts());
1975    
1976    // ========== URL HASH HANDLING ==========
1977    function handleUrlHash() {
1978      const hash = window.location.hash;
1979      if (hash.startsWith('#history=')) {
1980        history.replaceState(null, '', window.location.pathname + window.location.search);
1981        switchTab('receipt');
1982        loadMyReceipts();
1983      } else if (hash.startsWith('#verify=')) {
1984        const hashToVerify = hash.slice(8);
1985        if (hashToVerify.length === 64 && /^[a-f0-9]+$/i.test(hashToVerify)) {
1986          switchTab('verify');
1987          document.getElementById('verify-hash-input').value = hashToVerify;
1988        }
1989      }
1990    }
1991    
1992    // Run on page load
1993    handleUrlHash();
1994
1995    // ========== LOCAL SAMPLE PROOF DEMO ==========
1996    const sampleProofBtn = document.getElementById('sample-proof-btn');
1997    const sampleProofOutput = document.getElementById('sample-proof-output');
1998
1999    sampleProofBtn?.addEventListener('click', async () => {
2000      const originalText = sampleProofBtn.textContent;
2001      sampleProofBtn.disabled = true;
2002      sampleProofBtn.textContent = 'Generating...';
2003
2004      try {
2005        const payload = `OTRUST local sample proof\n${new Date().toISOString()}\n${crypto.randomUUID()}`;
2006        const hash = await sha256Hex(payload);
2007        const ed = await loadEd25519();
2008        const privateKey = ed.utils.randomPrivateKey();
2009        const publicKey = await ed.getPublicKeyAsync(privateKey);
2010        const signature = await ed.signAsync(hexToBytes(hash), privateKey);
2011        const isValid = await ed.verifyAsync(signature, hexToBytes(hash), publicKey);
2012
2013        document.getElementById('sample-proof-id').textContent = `demo_${hash.slice(0, 12)}`;
2014        document.getElementById('sample-proof-hash').textContent = hash;
2015        document.getElementById('sample-proof-pubkey').textContent = bytesToHex(publicKey);
2016        document.getElementById('sample-proof-signature').textContent = bytesToHex(signature);
2017        document.getElementById('sample-proof-time').textContent = isValid ? new Date().toLocaleTimeString() : 'failed';
2018        sampleProofOutput?.classList.add('visible');
2019      } catch (error) {
2020        document.getElementById('sample-proof-time').textContent = error.message;
2021        sampleProofOutput?.classList.add('visible');
2022      } finally {
2023        sampleProofBtn.disabled = false;
2024        sampleProofBtn.textContent = originalText;
2025      }
2026    });
2027    
2028    // ========== DOWNLOAD FUNCTIONS ==========
2029    // These must be global for onclick handlers in dynamically generated HTML
2030    window.downloadReceipt = async function(receiptId) {
2031      try {
2032        const res = await fetch('/proof/' + receiptId);
2033        if (!res.ok) throw new Error('Receipt not found');
2034        const data = await res.json();
2035        
2036        const receipt = {
2037          receipt_id: receiptId,
2038          filename: data.filename || null,
2039          hash: data.hash,
2040          created_at: data.created_at,
2041          status: data.status,
2042          blockchain: data.blockchain,
2043          ots_proof: data.ots_proof,
2044          verification_url: window.location.origin + '/proof/' + receiptId
2045        };
2046        
2047        const blob = new Blob([JSON.stringify(receipt, null, 2)], { type: 'application/json' });
2048        const url = URL.createObjectURL(blob);
2049        const a = document.createElement('a');
2050        a.href = url;
2051        a.download = receiptId + '-receipt.json';
2052        document.body.appendChild(a);
2053        a.click();
2054        document.body.removeChild(a);
2055        URL.revokeObjectURL(url);
2056      } catch (e) {
2057        alert('Failed to download receipt: ' + e.message);
2058      }
2059    };
2060    
2061    window.downloadCertificate = async function(receiptId, hash) {
2062      try {
2063        const res = await fetch('/proof/' + receiptId);
2064        const data = await res.json();
2065        
2066        const filenameHtml = data.filename 
2067          ? `<div class="field">
2068        <div class="field-label">Document</div>
2069        <div class="field-value">${data.filename}</div>
2070      </div>` 
2071          : '';
2072        
2073        const certHtml = `<!DOCTYPE html>
2074<html>
2075<head>
2076  <meta charset="UTF-8">
2077  <title>OTRUST Certificate - ${receiptId}</title>
2078  <style>
2079    * { margin: 0; padding: 0; box-sizing: border-box; }
2080    body { font-family: 'Georgia', serif; background: #f8f8f5; padding: 2rem; }
2081    .certificate { max-width: 800px; margin: 0 auto; background: white; border: 3px double #0f766e; padding: 3rem; position: relative; }
2082    .certificate::before { content: ''; position: absolute; top: 10px; left: 10px; right: 10px; bottom: 10px; border: 1px solid #e5e5e0; pointer-events: none; }
2083    .header { text-align: center; margin-bottom: 2rem; }
2084    .logo { font-size: 1.5rem; font-weight: bold; color: #0f766e; letter-spacing: 0.1em; margin-bottom: 0.5rem; }
2085    .title { font-size: 2rem; color: #1a1a1a; margin: 1rem 0; }
2086    .subtitle { color: #666; font-style: italic; }
2087    .content { margin: 2rem 0; line-height: 1.8; }
2088    .field { margin: 1rem 0; }
2089    .field-label { font-size: 0.8rem; color: #888; text-transform: uppercase; letter-spacing: 0.05em; }
2090    .field-value { font-family: monospace; font-size: 0.9rem; word-break: break-all; background: #f5f5f5; padding: 0.5rem; border-radius: 4px; margin-top: 0.25rem; }
2091    .field-value.document { font-family: 'Georgia', serif; font-size: 1rem; background: #f0fdf4; border-left: 3px solid #0f766e; }
2092    .blockchain { margin-top: 2rem; padding-top: 2rem; border-top: 1px solid #e5e5e5; }
2093    .verified { color: #0f766e; font-weight: bold; }
2094    .pending { color: #d97706; }
2095    .footer { margin-top: 3rem; text-align: center; font-size: 0.8rem; color: #888; }
2096    .seal { width: 80px; height: 80px; border: 2px solid #0f766e; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 1rem auto; color: #0f766e; font-weight: bold; }
2097    @media print { body { background: white; padding: 0; } .certificate { border: none; box-shadow: none; } }
2098  </style>
2099</head>
2100<body>
2101  <div class="certificate">
2102    <div class="header">
2103      <div class="logo">? OTRUST</div>
2104      <h1 class="title">Certificate of Timestamp</h1>
2105      <p class="subtitle">Evidence of Existence & Authorship</p>
2106    </div>
2107    
2108    <div class="content">
2109      <p>This document certifies that the digital content identified by the hash below existed at the specified time and was registered on the OTRUST blind notary service with blockchain anchoring.</p>
2110      
2111      ${filenameHtml}
2112      
2113      <div class="field">
2114        <div class="field-label">Receipt ID</div>
2115        <div class="field-value">${receiptId}</div>
2116      </div>
2117      
2118      <div class="field">
2119        <div class="field-label">SHA-256 Hash</div>
2120        <div class="field-value">${data.hash || hash}</div>
2121      </div>
2122      
2123      <div class="field">
2124        <div class="field-label">Timestamp (UTC)</div>
2125        <div class="field-value">${data.created_at || new Date().toISOString()}</div>
2126      </div>
2127      
2128      <div class="blockchain">
2129        <div class="field">
2130          <div class="field-label">Blockchain Status</div>
2131          <div class="field-value ${data.blockchain?.confirmed ? 'verified' : 'pen
2131ding'}">
2132            ${data.blockchain?.confirmed 
2133              ? 'Confirmed in block #' + data.blockchain.block_height
2134              : 'Pending blockchain confirmation'}
2135          </div>
2136        </div>
2137      </div>
2138    </div>
2139    
2140    <div class="seal">?</div>
2141    
2142    <div class="footer">
2143      <p>Verify this certificate at: <strong>${window.location.origin}</strong></p>
2144      <p style="margin-top: 0.5rem;">OTRUST uses OpenTimestamps for decentralized blockchain-backed timestamps</p>
2145      <p style="margin-top: 1rem; font-size: 0.7rem;">Generated: ${new Date().toISOString()}</p>
2146    </div>
2147  </div>
2148</body>
2149</html>`;
2150        
2151        const blob = new Blob([certHtml], { type: 'text/html' });
2152        const url = URL.createObjectURL(blob);
2153        const a = document.createElement('a');
2154        a.href = url;
2155        a.download = receiptId + '-certificate.html';
2156        document.body.appendChild(a);
2157        a.click();
2158        document.body.removeChild(a);
2159        URL.revokeObjectURL(url);
2160      } catch (e) {
2161        alert('Failed to generate certificate: ' + e.message);
2162      }
2163    };
2164    
2165    document.getElementById('export-backup-btn')?.addEventListener('click', async () => {
2166      const password = prompt('Choose a backup password (min 8 characters):');
2167      if (!password) return;
2168      try {
2169        const backup = await window.otrustLocalBackup.exportEncryptedBackup(password);
2170        window.otrustLocalBackup.downloadBackupFile(backup);
2171      } catch (e) {
2172        alert('Export failed: ' + e.message);
2173      }
2174    });
2175
2176    document.getElementById('import-backup-input')?.addEventListener('change', async (e) => {
2177      const file = e.target.files?.[0];
2178      if (!file) return;
2179      const password = prompt('Enter backup password:');
2180      if (!password) return;
2181      try {
2182        const result = await window.otrustLocalBackup.importEncryptedBackup(file, password);
2183        updateHistoryCount();
2184        if (typeof loadMyReceipts === 'function') await loadMyReceipts();
2185        alert(`Imported ${result.receipts} receipts, ${result.signings} signings${result.hasKeys ? ', keys restored' : ''}.`);
2186      } catch (err) {
2187        alert('Import failed: ' + err.message);
2188      }
2189      e.target.value = '';
2190    });
2191
2192    // New prominent backup controls in the Receipt/History tab
2193    document.getElementById('receipt-export-backup-btn')?.addEventListener('click', async () => {
2194      const password = prompt('Create a strong backup password (min 8 chars).\n\nThis encrypted file can restore your receipts and keys on any other device/browser.');
2195      if (!password) return;
2196      try {
2197        const backup = await window.otrustLocalBackup.exportEncryptedBackup(password);
2198        window.otrustLocalBackup.downloadBackupFile(backup, `otrust-receipts-backup-${new Date().toISOString().slice(0,10)}.json`);
2199      } catch (e) {
2200        alert('Export failed: ' + e.message);
2201      }
2202    });
2203
2204    document.getElementById('receipt-import-backup-input')?.addEventListener('change', async (e) => {
2205      const file = e.target.files?.[0];
2206      if (!file) return;
2207      const password = prompt('Enter the password you used when exporting this backup:');
2208      if (!password) return;
2209      try {
2210        const result = await window.otrustLocalBackup.importEncryptedBackup(file, password);
2211        updateHistoryCount();
2212        if (typeof loadMyReceipts === 'function') await loadMyReceipts();
2213        alert(`Restored ${result.receipts} receipts${result.hasKeys ? ' + keys' : ''}.`);
2214      } catch (err) {
2215        alert('Import failed: ' + err.message);
2216      }
2217      e.target.value = '';
2218    });
2219
2220    // Plain JSON export (for advanced users moving between their own devices)
2221    document.getElementById('receipt-export-plain-btn')?.addEventListener('click', () => {
2222      const receipts = window.otrustReceiptHistory?.getMyReceipts() || [];
2223      if (!receipts.length) {
2224        alert('No receipts to export.');
2225        return;
2226      }
2227      const data = {
2228        exported_at: new Date().toISOString(),
2229        note: 'Plain (unencrypted) export of your local receipts. Keep this file secure.',
2230        receipts
2231      };
2232      const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
2233      const url = URL.createObjectURL(blob);
2234      const a = document.createElement('a');
2235      a.href = url;
2236      a.download = `otrust-receipts-${new Date().toISOString().slice(0,10)}.json`;
2237      document.body.appendChild(a);
2238      a.click();
2239      document.body.removeChild(a);
2240      URL.revokeObjectURL(url);
2241    });
2242
2243    window.downloadBulkReceipts = function() {
2244      if (!window.lastBulkResults) return;
2245      
2246      const receipts = window.lastBulkResults.filter(r => r.receipt_id).map(r => ({
2247        receipt_id: r.receipt_id,
2248        hash: r.hash,
2249        timestamp: r.timestamp,
2250        status: r.status
2251      }));
2252      
2253      const blob = new Blob([JSON.stringify(receipts, null, 2)], { type: 'application/json' });
2254      const url = URL.createObjectURL(blob);
2255      const a = document.createElement('a');
2256      a.href = url;
2257      a.download = 'otrust-bulk-receipts.json';
2258      a.click();
2259      URL.revokeObjectURL(url);
2260    };
2261  </script>
2261
2262  <style>
2263
2264    .bitcoin-toast { margin:0 0 1rem;padding:0.75rem 1rem;background:#ecfdf5;border:1px solid #a7f3d0;border-radius:8px;color:#2d5a3d;font-size:0.85rem;opacity:0;transition:opacity 0.3s; }
2265    .bitcoin-toast.visible { opacity:1; }
2266  </style>
2267  
2267<script src="/js/receipt-history.js?v=20260716" defer></script>
2267
2268  
2268<script src="/js/local-backup.js?v=20260716" defer></script>
2268
2269  
2269<script src="/js/share-proof.js?v=20260716" defer></script>
2269
2270
2271  
2271<script src="/js/usage-beacon.js?v=20260716" defer></script>
2271
2272  
2272<script src="/js/otrust-extras.js?v=20260716" defer></script>
2272
2273  
2273<script src="/otrust-polish.js?v=20260723-03" defer></script>
vendor: 1 bytes, line 2273
2273
2274<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" nonce="XJW8b56PBiVK2PYTvTXDjQ==" data-cf-beacon='{"version":"2024.11.0","token":"13ad866881b443da845582359d8f840a","r":1,"spa":2}' crossorigin="anonymous"></script>
2274
2275</body>
2276</html>
2277
2278
2279
2280
2281

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.