- 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>
199 lines
9.3 KiB
HTML
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>
|