From 133558d923d3f85665030f69589d9e1e7e2b084b Mon Sep 17 00:00:00 2001 From: ssmithx Date: Sun, 2 Aug 2026 14:44:00 +0000 Subject: [PATCH] feat(auth): bridge NIP-07 sign-in to Archipelago's identity manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vendors Archipelago's NIP-07 provider shim (neode-ui/public/ nostr-provider.js) and loads it in index.html's . It's a no-op outside an Archipelago iframe (the shim's own window === window.top guard), so this is always safe to include. When podsteadr is opened from the Archipelago dashboard (registered there as an external identity-aware app — see the companion archy change on branch feat/podsteadr-external-nostr-identity), the parent frame lets the user pick one of their node's stored nostr identities and posts window.nostr signing requests through to it. The shim then runs the existing NIP-98 flow against our own auth (nip07.ts, auth.ts, routes/auth.ts) exactly as if a browser extension had signed it — nothing on the server needed to change. Configured for our actual auth shape via data-* attrs the shim reads from its own
diff --git a/frontend/public/nostr-provider.js b/frontend/public/nostr-provider.js new file mode 100644 index 0000000..45e62ec --- /dev/null +++ b/frontend/public/nostr-provider.js @@ -0,0 +1,217 @@ +/** + * NIP-07 Nostr Provider Shim — Archipelago + * + * Vendored from archy/neode-ui/public/nostr-provider.js (generalized version). + * Provides window.nostr (NIP-07) for iframe apps launched inside the + * Archipelago shell, bridging signing requests via postMessage to the + * parent frame, which relays them to the Archipelago node's identity + * manager. Auto sign-in: does NIP-98 auth against this app's own backend, + * then reloads so the app picks up the valid session. + * + * Not vendored via an Archipelago manifest hook (podsteadr isn't an + * orchestrator-managed package — see neode-ui's EXTERNAL_URLS / + * WEB_ONLY_APP-style "external web app" registration instead), so this + * copy won't auto-update with archy's OTA releases. Re-sync by hand from + * archy/neode-ui/public/nostr-provider.js if that file changes. + */ +(function () { + 'use strict'; + if (window.__archipelagoNostr) return; + window.__archipelagoNostr = true; + if (window === window.top) return; + + var pending = {}, nextId = 1; + + function request(method, params) { + return new Promise(function (resolve, reject) { + var id = nextId++; + pending[id] = { resolve: resolve, reject: reject }; + window.parent.postMessage({ type: 'nostr-request', id: id, method: method, params: params || {} }, '*'); + setTimeout(function () { if (pending[id]) { pending[id].reject(new Error('NIP-07 timeout')); delete pending[id]; } }, 30000); + }); + } + + window.addEventListener('message', function (e) { + if (!e.data || e.data.type !== 'nostr-response') return; + var h = pending[e.data.id]; if (!h) return; delete pending[e.data.id]; + e.data.error ? h.reject(new Error(e.data.error)) : h.resolve(e.data.result); + }); + + window.nostr = { + getPublicKey: function () { return request('getPublicKey'); }, + signEvent: function (ev) { return request('signEvent', { event: ev }); }, + sign: function (ev) { return request('signEvent', { event: ev }); }, + getRelays: function () { return request('getRelays'); }, + nip04: { + encrypt: function (pk, pt) { return request('nip04.encrypt', { pubkey: pk, plaintext: pt }); }, + decrypt: function (pk, ct) { return request('nip04.decrypt', { pubkey: pk, ciphertext: ct }); }, + }, + nip44: { + encrypt: function (pk, pt) { return request('nip44.encrypt', { pubkey: pk, plaintext: pt }); }, + decrypt: function (pk, ct) { return request('nip44.decrypt', { pubkey: pk, ciphertext: ct }); }, + }, + }; + + // --- Loading Overlay --- + var overlay = null; + + function showLoader(message) { + if (overlay) return; + overlay = document.createElement('div'); + overlay.id = 'archipelago-auth-overlay'; + overlay.innerHTML = + '
' + + '' + + '' + + '' + + '' + + '
' + (message || 'Signing in...') + '
' + + '
'; + overlay.style.cssText = 'position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.7);backdrop-filter:blur(8px);'; + var style = document.createElement('style'); + style.textContent = '@keyframes archy-spin{to{transform:rotate(360deg)}}'; + document.head.appendChild(style); + document.body.appendChild(overlay); + } + + function updateLoader(message) { + if (!overlay) return; + var txt = overlay.querySelector('div > div'); + if (txt) txt.textContent = message; + } + + function hideLoader() { + if (overlay) { overlay.remove(); overlay = null; } + } + + // --- Per-app config (data-* attrs on the injected