Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>TTTPS Proof-of-Time Explorer</title> | |
| <style> | |
| :root { | |
| --bg: #0d1117; | |
| --panel: #161b22; | |
| --border: #30363d; | |
| --text: #e6edf3; | |
| --muted: #8b949e; | |
| --accent: #2f81f7; | |
| --green: #1a7f37; | |
| --yellow: #9a6700; | |
| --red: #cf222e; | |
| --gray: #6e7781; | |
| --blue: #1f6feb; | |
| --mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas, monospace; | |
| } | |
| @media (prefers-color-scheme: light) { | |
| :root { | |
| --bg: #ffffff; | |
| --panel: #f6f8fa; | |
| --border: #d0d7de; | |
| --text: #1f2328; | |
| --muted: #57606a; | |
| } | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; | |
| line-height: 1.55; | |
| } | |
| .wrap { max-width: 860px; margin: 0 auto; padding: 32px 20px 64px; } | |
| h1 { font-size: 1.55rem; margin-bottom: 4px; } | |
| h2 { font-size: 1.15rem; margin: 0 0 10px; } | |
| .sub { color: var(--muted); font-size: 0.95rem; margin-bottom: 24px; } | |
| .intro { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 18px 20px; margin-bottom: 24px; font-size: 0.94rem; } | |
| .intro code { background: rgba(127,127,127,0.15); padding: 1px 5px; border-radius: 4px; font-family: var(--mono); font-size: 0.88em; } | |
| .intro a { color: var(--accent); } | |
| textarea { | |
| width: 100%; min-height: 100px; resize: vertical; | |
| background: var(--panel); color: var(--text); border: 1px solid var(--border); | |
| border-radius: 8px; padding: 12px; font-size: 0.95rem; font-family: inherit; | |
| } | |
| .row { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; align-items: center; } | |
| button { | |
| background: var(--accent); color: white; border: none; border-radius: 8px; | |
| padding: 10px 18px; font-size: 0.95rem; font-weight: 600; cursor: pointer; | |
| } | |
| button.secondary { background: var(--panel); color: var(--text); border: 1px solid var(--border); } | |
| button:disabled { opacity: 0.5; cursor: not-allowed; } | |
| button:hover:not(:disabled) { filter: brightness(1.08); } | |
| .panel { | |
| background: var(--panel); border: 1px solid var(--border); border-radius: 10px; | |
| padding: 18px 20px; margin-top: 16px; font-size: 0.92rem; | |
| } | |
| .field-table { width: 100%; border-collapse: collapse; margin-top: 8px; } | |
| .field-table td { padding: 6px 4px; border-bottom: 1px solid var(--border); vertical-align: top; } | |
| .field-table td:first-child { color: var(--muted); white-space: nowrap; padding-right: 14px; width: 220px; } | |
| .mono { font-family: var(--mono); word-break: break-all; font-size: 0.87em; } | |
| .badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 0.82em; font-weight: 700; color: white; } | |
| .badge.green { background: var(--green); } | |
| .badge.yellow { background: var(--yellow); } | |
| .badge.red { background: var(--red); } | |
| .badge.gray { background: var(--gray); } | |
| .badge.blue { background: var(--blue); } | |
| .note { color: var(--muted); font-size: 0.88em; margin-top: 10px; } | |
| .raw { margin-top: 10px; } | |
| .raw summary { cursor: pointer; color: var(--muted); font-size: 0.85em; } | |
| .raw pre { | |
| background: rgba(127,127,127,0.08); border: 1px solid var(--border); border-radius: 6px; | |
| padding: 10px; overflow-x: auto; font-size: 0.8em; margin-top: 6px; | |
| } | |
| .status-msg { font-size: 0.9rem; color: var(--muted); margin-top: 10px; } | |
| .status-msg.err { color: var(--red); } | |
| footer { margin-top: 40px; color: var(--muted); font-size: 0.82rem; border-top: 1px solid var(--border); padding-top: 16px; } | |
| footer a { color: var(--accent); } | |
| footer .badges { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; } | |
| hr { border: none; border-top: 1px solid var(--border); margin: 28px 0; } | |
| #verifyInput { width: 100%; font-family: var(--mono); font-size: 0.88rem; padding: 8px 10px; background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 8px; } | |
| /* --- Hero --- */ | |
| .hero { margin-bottom: 28px; } | |
| .hero h1 { font-size: 1.9rem; margin-bottom: 8px; } | |
| .hero .tagline { font-size: 1.02rem; color: var(--text); opacity: 0.85; max-width: 640px; margin-bottom: 18px; } | |
| .stats-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 4px; } | |
| .stat { | |
| background: var(--panel); border: 1px solid var(--border); border-radius: 10px; | |
| padding: 10px 16px; min-width: 150px; flex: 1; | |
| } | |
| .stat .num { font-size: 1.15rem; font-weight: 700; } | |
| .stat .lbl { font-size: 0.78em; color: var(--muted); margin-top: 2px; } | |
| /* --- Architecture flow --- */ | |
| .flow { | |
| display: flex; align-items: center; gap: 0; flex-wrap: wrap; margin: 14px 0 6px; | |
| } | |
| .flow-box { | |
| background: var(--panel); border: 1px solid var(--border); border-radius: 10px; | |
| padding: 12px 14px; font-size: 0.85rem; text-align: center; flex: 1; min-width: 150px; | |
| } | |
| .flow-box .t { font-weight: 700; margin-bottom: 3px; } | |
| .flow-box .d { color: var(--muted); font-size: 0.82em; } | |
| .flow-arrow { color: var(--muted); font-size: 1.3rem; padding: 0 10px; flex: 0 0 auto; } | |
| @media (max-width: 640px) { | |
| .flow { flex-direction: column; align-items: stretch; } | |
| .flow-arrow { text-align: center; padding: 4px 0; transform: rotate(90deg); } | |
| } | |
| /* --- Quickstart tabs --- */ | |
| .tabs { margin-top: 10px; } | |
| .tab-bar { display: flex; gap: 6px; flex-wrap: wrap; border-bottom: 1px solid var(--border); padding-bottom: 0; } | |
| .tab-btn { | |
| background: transparent; color: var(--muted); border: none; border-bottom: 2px solid transparent; | |
| border-radius: 0; padding: 9px 14px; font-size: 0.86rem; font-weight: 600; cursor: pointer; | |
| } | |
| .tab-btn.active { color: var(--text); border-bottom-color: var(--accent); } | |
| .tab-btn:hover:not(.active) { color: var(--text); filter: none; } | |
| .tab-panel { display: none; padding-top: 14px; } | |
| .tab-panel.active { display: block; } | |
| .tab-status { margin-bottom: 10px; } | |
| pre.code { | |
| background: rgba(127,127,127,0.08); border: 1px solid var(--border); border-radius: 8px; | |
| padding: 14px; overflow-x: auto; font-size: 0.82rem; font-family: var(--mono); line-height: 1.5; | |
| white-space: pre; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <!-- ============ HERO ============ --> | |
| <div class="hero"> | |
| <h1>TTTPS Proof-of-Time Explorer</h1> | |
| <div class="tagline"> | |
| A <strong>Layer 7 AI control-plane primitive</strong>: cryptographic proof-of-time | |
| receipts for LLM and agent outputs. Any framework's output gets hashed, sealed with a | |
| signed timestamp, and independently verifiable after the fact — without ever | |
| blocking or altering the original response. | |
| </div> | |
| <div class="stats-row"> | |
| <div class="stat"> | |
| <div class="num">8.6ms</div> | |
| <div class="lbl">measured warm p50 for a full <code>POST /v1/anchor</code> round trip against | |
| the live public API (p90 12.0ms, n=30). Cold p50 33.2ms / p90 45.1ms, n=30. 61/61 HTTP 200. | |
| Client in the same region; your network path will differ.</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="num">Fail-open, always</div> | |
| <div class="lbl">a sealing failure never blocks or mutates your LLM response — | |
| it degrades to a status flag. Demonstrated by blocking Roughtime UDP until the quorum | |
| broke: <code>/v1/anchor</code> stayed 3/3 HTTP 200 and <code>time_source</code> downgraded | |
| to <code>local</code> rather than lying.</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="num">8 frameworks</div> | |
| <div class="lbl">LangChain · LiteLLM · LlamaIndex · smolagents · | |
| vLLM · Ollama · DeepEval · Ragas. Which are installable today is | |
| listed on the API page, not duplicated here.</div> | |
| </div> | |
| </div> | |
| <div class="row" style="margin-top:14px;"> | |
| <a href="https://kpp.kenosian.com/tttps-api.html" target="_blank" rel="noopener" style="text-decoration:none;"> | |
| <button>Full API reference & framework matrix →</button> | |
| </a> | |
| <span class="note" style="margin-top:0;"> | |
| Endpoint-by-endpoint spec, every integration's real install command, and which ones are | |
| PyPI-published vs. locally verified — kept in sync with what's actually shipped, | |
| not aspirational. | |
| </span> | |
| </div> | |
| </div> | |
| <!-- ============ ARCHITECTURE ============ --> | |
| <h2>How it fits into your stack</h2> | |
| <div class="flow"> | |
| <div class="flow-box"> | |
| <div class="t">Your app / agent</div> | |
| <div class="d">LangChain, LiteLLM, LlamaIndex, smolagents, vLLM, or Ollama</div> | |
| </div> | |
| <div class="flow-arrow">→</div> | |
| <div class="flow-box"> | |
| <div class="t">OpenTTT integration</div> | |
| <div class="d">sha256(output) computed locally, sent async, fail-open</div> | |
| </div> | |
| <div class="flow-arrow">→</div> | |
| <div class="flow-box"> | |
| <div class="t">KPP Provenance API</div> | |
| <div class="d"><code>kpp.kenosian.com</code> — signs + timestamps the hash</div> | |
| </div> | |
| <div class="flow-arrow">→</div> | |
| <div class="flow-box"> | |
| <div class="t">Verifiable receipt</div> | |
| <div class="d"><code>receipt_id</code> + signature, independently re-checkable anytime</div> | |
| </div> | |
| </div> | |
| <div class="note" style="margin-bottom:28px;"> | |
| Your original LLM/agent response is never touched or delayed — only its hash leaves | |
| your process, asynchronously, and only a receipt comes back. | |
| </div> | |
| <hr> | |
| <!-- ============ QUICKSTART ============ --> | |
| <h2>Quickstart — pick your framework</h2> | |
| <div class="note" style="margin-top:-6px; margin-bottom:2px;"> | |
| Every integration attaches a cryptographic audit-trail timestamp. None of them certify | |
| legal or regulatory compliance (EU AI Act, FDA, etc.) on their own. | |
| </div> | |
| <div class="tabs"> | |
| <div class="tab-bar" id="tabBar"> | |
| <button class="tab-btn active" data-tab="langchain">LangChain</button> | |
| <button class="tab-btn" data-tab="litellm">LiteLLM</button> | |
| <button class="tab-btn" data-tab="llamaindex">LlamaIndex</button> | |
| <button class="tab-btn" data-tab="smolagents">smolagents</button> | |
| <button class="tab-btn" data-tab="vllm">vLLM</button> | |
| <button class="tab-btn" data-tab="ollama">Ollama</button> | |
| </div> | |
| <div class="tab-panel active" id="tab-langchain"> | |
| <div class="tab-status"> | |
| <span class="badge blue">PyPI: v0.3.0 published</span> | |
| </div> | |
| <div class="note" style="margin-top:0;"> | |
| <code>pip install langchain-openttt</code> installs the published v0.3.0 package, which | |
| ships both <code>TTTPSTimestampCallbackHandler</code> (internal PoT server) and | |
| <code>TTTPSTimestampCallbackHandlerKPPLive</code> (this public self-serve API) — | |
| verified importable from the live PyPI wheel in an isolated venv. It is also registered in | |
| LangChain's own docs repo rather than mentioned by a third party: | |
| <a href="https://github.com/langchain-ai/docs/pull/5257" target="_blank" rel="noopener">langchain-ai/docs #5257</a> | |
| adds it to the integrations YAML and is <strong>merged</strong> into the LangChain External | |
| Integrations Registry. | |
| </div> | |
| <pre class="code">pip install langchain-openttt # v0.3.0 | |
| import os | |
| os.environ["KPP_API_KEY"] = "kpp_prov_..." # mint free: POST https://kpp.kenosian.com/v1/keys | |
| from langchain_openttt import TTTPSTimestampCallbackHandlerKPPLive | |
| resp = llm.invoke("hi", config={"callbacks": [TTTPSTimestampCallbackHandlerKPPLive()]}) | |
| # resp.generation_info["tttps_receipt"] | |
| # -> {"status": "ok", "receipt_id": "...", "receipt": "...", "time": "...", ...}</pre> | |
| </div> | |
| <div class="tab-panel" id="tab-litellm"> | |
| <div class="tab-status"> | |
| <span class="badge blue">PyPI: v0.1.0 published</span> | |
| </div> | |
| <div class="note" style="margin-top:0;"> | |
| Docs recipe (inline <code>CustomLogger</code>) shipped upstream via | |
| <a href="https://github.com/BerriAI/litellm-docs/pull/760" target="_blank" rel="noopener">BerriAI/litellm-docs #760</a> | |
| (<code>docs/observability/tttps_provenance.md</code>, open). | |
| </div> | |
| <pre class="code">pip install litellm-openttt # v0.1.0 | |
| import litellm | |
| from litellm_openttt import TTTPSTimestampLoggerKPPLive | |
| litellm.callbacks = [TTTPSTimestampLoggerKPPLive()] | |
| resp = litellm.completion( | |
| model="gpt-3.5-turbo", | |
| messages=[{"role": "user", "content": "hi"}], | |
| ) | |
| print(resp.tttps_receipt) | |
| # {"status": "ok", "receipt_id": "...", "receipt": "...", "time": "...", ...}</pre> | |
| </div> | |
| <div class="tab-panel" id="tab-llamaindex"> | |
| <div class="tab-status"> | |
| <span class="badge blue">PyPI: v0.1.0 published</span> | |
| </div> | |
| <div class="note" style="margin-top:0;"> | |
| No upstream PR here yet. <code>run-llama/llama_index</code>'s own CONTRIBUTING.md states | |
| new integration packages aren't accepted into the monorepo and must already be published | |
| to PyPI independently first. That precondition is now met, so a docs PR is the next step | |
| rather than a blocked one. | |
| </div> | |
| <pre class="code">pip install llamaindex-openttt # v0.1.0 | |
| from llama_index.core import Settings | |
| from llama_index.core.callbacks import CallbackManager | |
| from llamaindex_openttt import TTTPSTimestampCallbackHandlerKPPLive | |
| Settings.callback_manager = CallbackManager([TTTPSTimestampCallbackHandlerKPPLive()]) | |
| resp = llm.complete("hi") | |
| print(resp.additional_kwargs["tttps_receipt"]) | |
| # {"status": "ok", "receipt_id": "...", "receipt": "...", "time": "...", ...}</pre> | |
| </div> | |
| <div class="tab-panel" id="tab-smolagents"> | |
| <div class="tab-status"> | |
| <span class="badge blue">PyPI: v0.1.0 published</span> | |
| </div> | |
| <div class="note" style="margin-top:0;"> | |
| Example shipped upstream via | |
| <a href="https://github.com/huggingface/smolagents/pull/2597" target="_blank" rel="noopener">huggingface/smolagents #2597</a> | |
| (<code>examples/mcp_tttps_provenance.py</code>, open). Full walkthrough in the | |
| <a href="https://github.com/huggingface/smolagents/discussions/2600" target="_blank" rel="noopener">Show and tell discussion #2600</a>. | |
| smolagents can also reach the same tools over MCP with no adapter package at all. | |
| </div> | |
| <pre class="code">pip install smolagents-openttt # v0.1.0 | |
| from smolagents import CodeAgent | |
| from smolagents_openttt import ttts_pot_step_callback_kpp_live | |
| agent = CodeAgent( | |
| tools=[], | |
| model=your_model, | |
| step_callbacks=[ttts_pot_step_callback_kpp_live], | |
| ) | |
| result = agent.run("your task") | |
| for step in agent.memory.steps: | |
| print(getattr(step, "tttps_receipt", None)) | |
| # {"status": "ok", "receipt_id": "...", "receipt": "...", "time": "...", ...}</pre> | |
| </div> | |
| <div class="tab-panel" id="tab-vllm"> | |
| <div class="tab-status"> | |
| <span class="badge blue">PyPI: v0.1.0 published</span> | |
| </div> | |
| <div class="note" style="margin-top:0;"> | |
| Registers via vLLM's official <code>--middleware</code> flag (confirmed live against | |
| vLLM's own <code>build_app()</code> with a real server and a completed | |
| <code>/v1/completions</code> round trip). The example shipped upstream via | |
| <a href="https://github.com/vllm-project/vllm/pull/50998" target="_blank" rel="noopener">vllm-project/vllm #50998</a> | |
| (<code>examples/observability/tttps/</code>, open) calls the public | |
| <code>kpp.kenosian.com</code> API, so external readers can reproduce it. | |
| </div> | |
| <pre class="code">pip install vllm-openttt # v0.1.0 | |
| export KPP_API_KEY=kpp_prov_... # mint free: POST https://kpp.kenosian.com/v1/keys | |
| vllm serve facebook/opt-125m \ | |
| --middleware vllm_openttt.TTTPSMiddleware | |
| # curl http://localhost:8000/v1/completions ... | |
| # -> response header: X-TTTPS-Receipt: {"status": "ok", "receipt_id": "...", "receipt": "...", ...}</pre> | |
| </div> | |
| <div class="tab-panel" id="tab-ollama"> | |
| <div class="tab-status"> | |
| <span class="badge blue">PyPI: v0.1.0 published</span> | |
| </div> | |
| <div class="note" style="margin-top:0;"> | |
| Ollama has no plugin/middleware extension point (confirmed by reading | |
| <code>server/routes.go</code>), so this is a small reverse proxy in front of it. It | |
| already targets the public <code>kpp.kenosian.com</code> API. Both non-streaming and | |
| streaming (NDJSON) responses are supported — streaming gets a receipt attached to | |
| its final <code>done:true</code> line only, with zero added per-token latency. Ollama's | |
| README does have a real “Community Integrations” list open to PRs, but this | |
| proxy isn't in its own public repo yet, so there's nothing citable to link there yet | |
| — tracked, not forgotten. | |
| </div> | |
| <pre class="code">KPP_API_KEY=kpp_prov_... python ollama_proxy.py --port 11435 | |
| # point your Ollama client at localhost:11435 instead of 11434 | |
| curl localhost:11435/api/generate -d '{"model":"qwen2.5:0.5b","prompt":"hi","stream":false}' | |
| # -> ollama's normal JSON, plus a "tttps_receipt" field and | |
| # X-TTTPS-Receipt / X-TTTPS-Status response headers</pre> | |
| </div> | |
| </div> | |
| <hr> | |
| <!-- ============ LIVE INSPECTOR (existing, untouched logic) ============ --> | |
| <h2>Live Inspector — try it yourself, right now</h2> | |
| <div class="sub">Live demo of a proposal specification, not a ratified standard.</div> | |
| <div class="intro"> | |
| This section calls the real, public <code>/v1/anchor</code> and <code>/v1/verify</code> | |
| endpoints directly from your browser — nothing here is simulated, and there is no | |
| backend server for this demo other than your own browser and the API itself, served from | |
| <a href="https://kpp.kenosian.com" target="_blank" rel="noopener">kpp.kenosian.com</a>. | |
| <br><br> | |
| <strong>What a receipt is, and is not:</strong> a receipt cryptographically attests | |
| <em>when</em> a content hash was sealed, its <em>order</em> relative to other seals, and | |
| that it is <em>tamper-evident</em> after the fact. It does <strong>not</strong> attest | |
| that the content itself is correct, true, or valid — that is a separate, orthogonal | |
| claim TTTPS does not make. | |
| <br><br> | |
| The text you type below is hashed with sha256 <strong>in your own browser</strong> before | |
| anything is sent anywhere. Only the hash ever leaves your machine. | |
| </div> | |
| <textarea id="contentInput" placeholder="Type or paste anything — an LLM completion, a document, a log line..."></textarea> | |
| <div class="row"> | |
| <button id="anchorBtn">Anchor (seal this content)</button> | |
| <span id="anchorStatus" class="status-msg"></span> | |
| </div> | |
| <div id="anchorPanel" class="panel" style="display:none;"></div> | |
| <hr> | |
| <div class="row" style="align-items:flex-start;"> | |
| <div style="flex:1; min-width:240px;"> | |
| <label for="verifyInput" style="display:block; margin-bottom:6px; color:var(--muted); font-size:0.88em;">receipt_id</label> | |
| <input id="verifyInput" type="text" placeholder="paste or auto-filled from a seal above"> | |
| </div> | |
| <button id="verifyBtn" class="secondary" style="margin-top:20px;">Verify this receipt_id</button> | |
| </div> | |
| <span id="verifyStatus" class="status-msg"></span> | |
| <div id="verifyPanel" class="panel" style="display:none;"></div> | |
| <footer> | |
| Backend: <code>https://kpp.kenosian.com</code> · API spec: proposal, not a ratified standard · | |
| Source endpoints: <code>POST /v1/keys</code>, <code>POST /v1/anchor</code>, <code>POST /v1/verify</code> | |
| · This is a static page; your browser talks to the API directly (CORS-enabled). | |
| <div class="badges"> | |
| <a href="https://kpp.kenosian.com/verify.html" target="_blank" rel="noopener"><img src="https://img.shields.io/endpoint?url=https://kpp.kenosian.com/v1/badge/status.json" alt="TTTPS receipts sealed" style="vertical-align:middle;"></a> | |
| <a href="https://pypi.org/project/langchain-openttt/" target="_blank" rel="noopener"><span class="badge blue">PyPI: langchain-openttt</span></a> | |
| <a href="https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/" target="_blank" rel="noopener"><span class="badge gray">IETF Internet-Draft: draft-helmprotocol-tttps (proposal, not ratified)</span></a> | |
| <a href="https://github.com/Helm-Protocol/OpenTTT" target="_blank" rel="noopener"><span class="badge gray">GitHub: Helm-Protocol/OpenTTT</span></a> | |
| </div> | |
| </footer> | |
| </div> | |
| <script> | |
| const KPP_BASE = "https://kpp.kenosian.com"; | |
| const KEYS_URL = KPP_BASE + "/v1/keys"; | |
| const ANCHOR_URL = KPP_BASE + "/v1/anchor"; | |
| const VERIFY_URL = KPP_BASE + "/v1/verify"; | |
| // Design note (mirrors the server-side equivalent used in the Gradio prototype | |
| // of this demo, kept for reference): mint ONE demo API key per browser session | |
| // on first use, cache it in memory for the lifetime of this page, and reuse it | |
| // for every anchor click in this tab. Minting a fresh key on every single click | |
| // would burn through the 1,000-seal free quota per key far faster than | |
| // necessary for a demo. Because this is a static page with no shared backend | |
| // process, "per session" here naturally means "per browser tab" rather than | |
| // "per Space process" — each visitor gets their own key, so no visitor can | |
| // exhaust another visitor's quota. | |
| let demoKeyPromise = null; | |
| function getDemoKey() { | |
| if (!demoKeyPromise) { | |
| demoKeyPromise = fetch(KEYS_URL, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ email: "tttps-hf-space-demo@kenosian.com", use_case: "public HF static Space demo (tttps-proof-explorer)" }) | |
| }).then(async (r) => { | |
| if (!r.ok) throw new Error("key mint failed: HTTP " + r.status + " " + (await r.text())); | |
| return r.json(); | |
| }); | |
| } | |
| return demoKeyPromise; | |
| } | |
| async function sha256Hex(text) { | |
| const enc = new TextEncoder().encode(text); | |
| const digest = await crypto.subtle.digest("SHA-256", enc); | |
| return Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, "0")).join(""); | |
| } | |
| function badgeHtml(timeSource) { | |
| if (timeSource === "roughtime_chain") { | |
| return '<span class="badge green">✓ roughtime_chain (independently witnessed)</span>'; | |
| } | |
| if (timeSource) { | |
| return '<span class="badge yellow">⚠ ' + escapeHtml(timeSource) + ' (not independently witnessed)</span>'; | |
| } | |
| return '<span class="badge gray">? unknown</span>'; | |
| } | |
| function escapeHtml(s) { | |
| return String(s).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); | |
| } | |
| const contentInput = document.getElementById("contentInput"); | |
| const anchorBtn = document.getElementById("anchorBtn"); | |
| const anchorStatus = document.getElementById("anchorStatus"); | |
| const anchorPanel = document.getElementById("anchorPanel"); | |
| const verifyInput = document.getElementById("verifyInput"); | |
| const verifyBtn = document.getElementById("verifyBtn"); | |
| const verifyStatus = document.getElementById("verifyStatus"); | |
| const verifyPanel = document.getElementById("verifyPanel"); | |
| anchorBtn.addEventListener("click", async () => { | |
| const text = contentInput.value; | |
| anchorPanel.style.display = "none"; | |
| anchorStatus.className = "status-msg"; | |
| if (!text || !text.trim()) { | |
| anchorStatus.textContent = "Enter some text above first."; | |
| return; | |
| } | |
| anchorBtn.disabled = true; | |
| anchorStatus.textContent = "Minting demo key / sealing…"; | |
| try { | |
| const keyData = await getDemoKey(); | |
| const contentHash = await sha256Hex(text); | |
| const resp = await fetch(ANCHOR_URL, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ content_hash: contentHash, key: keyData.api_key }) | |
| }); | |
| if (resp.status === 402) { | |
| anchorStatus.className = "status-msg err"; | |
| anchorStatus.textContent = "Free seal quota exhausted (402) for this browser session's demo key. Reload the page to mint a new one."; | |
| anchorBtn.disabled = false; | |
| return; | |
| } | |
| if (!resp.ok) { | |
| const errText = await resp.text(); | |
| anchorStatus.className = "status-msg err"; | |
| anchorStatus.textContent = "/v1/anchor returned HTTP " + resp.status + ": " + errText; | |
| anchorBtn.disabled = false; | |
| return; | |
| } | |
| const data = await resp.json(); | |
| anchorStatus.textContent = ""; | |
| verifyInput.value = data.receipt_id || ""; | |
| anchorPanel.innerHTML = ` | |
| <strong>Receipt sealed</strong> | |
| <table class="field-table"> | |
| <tr><td>receipt_id</td><td class="mono">${escapeHtml(data.receipt_id)}</td></tr> | |
| <tr><td>content_hash (sha256, computed in your browser)</td><td class="mono">${escapeHtml(contentHash)}</td></tr> | |
| <tr><td>sealed time (UTC)</td><td class="mono">${escapeHtml(data.time)}</td></tr> | |
| <tr><td>time source</td><td>${badgeHtml(data.time_source)}</td></tr> | |
| <tr><td>receipt (HMAC signature)</td><td class="mono">${escapeHtml(data.receipt)}</td></tr> | |
| <tr><td>backdating bound</td><td class="mono">${escapeHtml(data.backdating_p)}</td></tr> | |
| <tr><td>demo key quota (this browser session)</td><td>${data.used} / ${data.quota} used (${data.remaining} remaining)</td></tr> | |
| </table> | |
| <div class="note">${escapeHtml(data.note || "A receipt proves when, order, and tamper-evidence — not correctness.")}</div> | |
| <details class="raw"><summary>Raw /v1/anchor JSON response</summary><pre>${escapeHtml(JSON.stringify(data, null, 2))}</pre></details> | |
| `; | |
| anchorPanel.style.display = "block"; | |
| } catch (err) { | |
| anchorStatus.className = "status-msg err"; | |
| anchorStatus.textContent = "Error: " + err.message; | |
| } finally { | |
| anchorBtn.disabled = false; | |
| } | |
| }); | |
| verifyBtn.addEventListener("click", async () => { | |
| const receiptId = verifyInput.value.trim(); | |
| verifyPanel.style.display = "none"; | |
| verifyStatus.className = "status-msg"; | |
| if (!receiptId) { | |
| verifyStatus.textContent = "Seal something first, or paste a receipt_id above."; | |
| return; | |
| } | |
| verifyBtn.disabled = true; | |
| verifyStatus.textContent = "Verifying…"; | |
| try { | |
| const resp = await fetch(VERIFY_URL, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ receipt_id: receiptId }) | |
| }); | |
| if (!resp.ok) { | |
| const errText = await resp.text(); | |
| verifyStatus.className = "status-msg err"; | |
| verifyStatus.textContent = "/v1/verify returned HTTP " + resp.status + ": " + errText; | |
| verifyBtn.disabled = false; | |
| return; | |
| } | |
| const data = await resp.json(); | |
| verifyStatus.textContent = ""; | |
| const verifiedBadge = data.verified | |
| ? '<span class="badge green">verified: true</span>' | |
| : '<span class="badge red">verified: false</span>'; | |
| verifyPanel.innerHTML = ` | |
| <strong>Verify result</strong> ${verifiedBadge} | |
| <table class="field-table"> | |
| <tr><td>receipt_id</td><td class="mono">${escapeHtml(data.receipt_id)}</td></tr> | |
| <tr><td>content_hash</td><td class="mono">${escapeHtml(data.content_hash || "n/a")}</td></tr> | |
| <tr><td>sealed time (UTC)</td><td class="mono">${escapeHtml(data.time || "n/a")}</td></tr> | |
| <tr><td>time source</td><td>${badgeHtml(data.time_source)}</td></tr> | |
| <tr><td>reason</td><td>${escapeHtml(data.reason)}</td></tr> | |
| </table> | |
| <details class="raw"><summary>Raw /v1/verify JSON response</summary><pre>${escapeHtml(JSON.stringify(data, null, 2))}</pre></details> | |
| `; | |
| verifyPanel.style.display = "block"; | |
| } catch (err) { | |
| verifyStatus.className = "status-msg err"; | |
| verifyStatus.textContent = "Error: " + err.message; | |
| } finally { | |
| verifyBtn.disabled = false; | |
| } | |
| }); | |
| // --- Quickstart tabs (new, additive; does not touch anything above) --- | |
| (function () { | |
| const bar = document.getElementById("tabBar"); | |
| if (!bar) return; | |
| bar.addEventListener("click", (e) => { | |
| const btn = e.target.closest(".tab-btn"); | |
| if (!btn) return; | |
| document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active")); | |
| document.querySelectorAll(".tab-panel").forEach(p => p.classList.remove("active")); | |
| btn.classList.add("active"); | |
| const panel = document.getElementById("tab-" + btn.dataset.tab); | |
| if (panel) panel.classList.add("active"); | |
| }); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |