Googstr/public/index.html
Jason Tudisco fcfaec675a Add NIP-46 signer, optional NIP-07 extension, passkey protection, backups
- NIP-46 remote signer in the page (bunker:// and nostrconnect:// pairing)
- Chrome extension (NIP-07) with per-site permission prompts, unlocked by
  the site via externally_connectable; downloadable from the page
- Passkey (WebAuthn PRF) encryption of the Drive key file
- NIP-49 backups: download or save to Drive; reveal nsec
- Fix: oldest key file wins when two devices sign up at once
- Fix: never overwrite an existing kind-0 because a relay was slow
- Fix: EOSE tracking; relays reconnect with backoff; verify event signatures
- One-click re-sign-in (consent only the first time)
- Vendored, pinned nostr-tools; CSP; no third-party scripts but Google's

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:33:02 -06:00

199 lines
9.3 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- The nostr key lives in this page's memory, so no third-party scripts
except Google's sign-in client. Everything else is served from here. -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' https://accounts.google.com/gsi/client; style-src 'self' 'unsafe-inline' https://accounts.google.com/gsi/style; connect-src 'self' https://www.googleapis.com https://accounts.google.com wss:; img-src 'self' data: https:; frame-src https://accounts.google.com; object-src 'none'; base-uri 'none'; form-action 'none'"
/>
<title>Goostr — Google to nostr</title>
<link rel="icon" href="data:," />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main>
<header>
<h1>Goostr</h1>
<p class="tag">Your Google account is your nostr identity.</p>
</header>
<div id="setup-banner" class="banner-warn hidden">
<strong>Setup required.</strong> Edit <code>public/config.js</code> and
paste your Google OAuth client ID into <code>CLIENT_ID</code>. See the
README for the 2-minute setup.
</div>
<div id="ext-unlock-banner" class="banner-ok hidden"></div>
<section id="view-loading" class="view">
<p id="loading-text">Checking session…</p>
</section>
<!-- Sign in -->
<section id="view-signin" class="view hidden">
<h2>Create or unlock your nostr identity</h2>
<p class="lead">
We'll save a secret key in your Google Drive's app data — a private
folder only this app can read. The key is your nostr identity. Sign
in on any device to use the same identity.
</p>
<button id="signin-go" class="google">
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<path d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844a4.14 4.14 0 0 1-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615z" fill="#4285F4"/>
<path d="M9 18c2.43 0 4.467-.806 5.956-2.184l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z" fill="#34A853"/>
<path d="M3.964 10.706A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.706V4.962H.957A8.997 8.997 0 0 0 0 9c0 1.452.348 2.827.957 4.038l3.007-2.332z" fill="#FBBC05"/>
<path d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.962L3.964 7.294C4.672 5.167 6.656 3.58 9 3.58z" fill="#EA4335"/>
</svg>
Sign in with Google
</button>
<button id="signin-passkey" class="primary hidden">Unlock with passkey</button>
<p id="signin-status" class="status" role="status"></p>
</section>
<!-- Logged in -->
<section id="view-home" class="view hidden">
<div class="profile">
<img id="me-pic" alt="" />
<div>
<h2 id="me-name"></h2>
<p class="npub" id="me-npub"></p>
</div>
<button id="sign-out" class="ghost">Sign out</button>
</div>
<div class="composer">
<textarea id="note-text" aria-label="New note" placeholder="Write something. It will be signed by your nostr key and published." maxlength="500"></textarea>
<button id="note-send" class="primary">Publish</button>
</div>
<div class="relays-bar">
<span class="label">Relays</span>
<span id="relay-dots"></span>
</div>
<h3>Your notes</h3>
<table id="notes-table">
<thead><tr><th>When</th><th>Note</th></tr></thead>
<tbody></tbody>
</table>
<p id="notes-status" class="status">Loading…</p>
</section>
<!-- Use elsewhere -->
<section id="view-connect" class="view hidden">
<h2>Use this identity in other nostr apps</h2>
<div class="card">
<h4>Browser extension <span class="pill">desktop Chrome · Edge · Brave</span></h4>
<p class="muted">
Optional. Adds "Login with extension" (NIP-07) to any nostr website.
Each site asks your permission first. Nothing needs to stay open.
</p>
<p id="ext-status" class="status"></p>
<div id="ext-install" class="hidden">
<a id="ext-store" class="button primary hidden" target="_blank" rel="noopener">Add to Chrome</a>
<a id="ext-download" class="button primary hidden" href="./goostr-extension.zip" download>Download the extension (.zip)</a>
<ol id="ext-steps" class="steps hidden">
<li>Unzip the download.</li>
<li>Open <span class="mono">chrome://extensions</span> and turn on <b>Developer mode</b> (top right).</li>
<li>Click <b>Load unpacked</b> and pick the unzipped <span class="mono">extension</span> folder.</li>
<li>Come back here and click <b>Check again</b>.</li>
</ol>
<button id="ext-recheck" class="ghost">Check again</button>
</div>
<button id="ext-connect" class="primary hidden">Unlock extension with this identity</button>
<button id="ext-lock" class="ghost hidden">Lock extension</button>
</div>
<div class="card">
<h4>Remote signer <span class="pill">NIP-46 · any app, any device</span></h4>
<p class="muted">
Apps that support "bunker" or "Nostr Connect" login (Coracle, Nostrudel,
Amethyst, …) send signing requests here over relays.
<b>This tab has to stay open</b> while you use them.
</p>
<div class="row">
<span id="bunker-state" class="status"></span>
<button id="bunker-toggle" class="ghost"></button>
</div>
<p class="sub">Connect an app with a bunker link</p>
<button id="bunker-new" class="primary">Create connection link</button>
<div id="bunker-link-box" class="hidden">
<div class="row">
<input id="bunker-link" type="text" readonly aria-label="Bunker link" class="mono" />
<button id="bunker-copy" class="ghost">Copy</button>
</div>
<p class="muted small">Paste it into the app's "bunker" / "remote signer" login. It works once.</p>
</div>
<p class="sub">…or paste the app's nostrconnect:// link</p>
<div class="row">
<input id="nc-link" type="text" placeholder="nostrconnect://…" aria-label="nostrconnect link" class="mono" />
<button id="nc-go" class="ghost">Connect</button>
</div>
<p id="bunker-status" class="status"></p>
<p class="sub">Connected apps</p>
<ul id="bunker-clients" class="list"></ul>
<p id="bunker-no-clients" class="muted small">None yet.</p>
<p class="sub">Recent activity</p>
<ul id="bunker-log" class="list small"></ul>
<p id="bunker-no-log" class="muted small">Nothing yet.</p>
</div>
</section>
<!-- Key & backup -->
<section id="view-key" class="view hidden">
<h2>Your key</h2>
<div class="card">
<h4>Protection</h4>
<p id="key-protection"></p>
<p class="muted small" id="pk-help">
A passkey (Face ID, fingerprint or device PIN) encrypts the copy in
Google Drive, so your Google password alone can't unlock it. Passkeys
only work on this site's domain. <b>Make a backup below first</b> —
if you lose the passkey, the backup is the only way back.
</p>
<button id="pk-enable" class="primary hidden">Protect with a passkey</button>
<button id="pk-disable" class="ghost hidden">Remove passkey protection</button>
<p id="pk-status" class="status"></p>
</div>
<div class="card">
<h4>Backup</h4>
<p class="muted">
A password-encrypted copy (NIP-49 <span class="mono">ncryptsec</span>)
that works in most nostr apps. It keeps your identity safe even if
this Goostr site or its Google project goes away.
</p>
<input id="bk-pw1" type="password" placeholder="Backup password (8+ characters)" autocomplete="new-password" aria-label="Backup password" />
<input id="bk-pw2" type="password" placeholder="Repeat password" autocomplete="new-password" aria-label="Repeat backup password" />
<div class="row">
<button id="bk-download" class="primary">Download backup</button>
<button id="bk-drive" class="ghost">Save to Google Drive</button>
</div>
<p id="bk-status" class="status"></p>
</div>
<div class="card">
<h4>Private key (nsec)</h4>
<p class="muted">Anyone who sees this can post as you. Only reveal it to move to another signer.</p>
<button id="nsec-reveal" class="ghost">Reveal nsec</button>
<div id="nsec-box" class="hidden">
<p id="nsec-value" class="mono secret"></p>
<button id="nsec-copy" class="ghost">Copy</button>
</div>
</div>
</section>
</main>
<script type="module" src="./app.js"></script>
</body>
</html>