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">→</span> 255 </a> 256 <a class="bento-text-action" href="#flow">How it works <span aria-hidden="true">↓</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">→</span></span> 284 <span class="bento-meta">Local SHA-256 · 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">→</span></span> 303 <span class="bento-meta">Groth16 ·
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">→</span></span> 323 <span class="bento-meta">Ed25519 · 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">→</span></span> 344 <span class="bento-meta">Issuer required · 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…</dd></div> 420 <div><dt>Timestamp (UTC)</dt><dd>May 24, 2025 — 14:32:18</dd></div> 421 <div><dt>Signature</dt><dd>ed25519:7f31a1c…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">→</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">→</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">→</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 & 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">→</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">→</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">© 2026 OTRUST <span class="made-in-eu" aria-hidden="true">· 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@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">© 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); 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})">×</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 + ')">×</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.