diff --git a/frontend/index.html b/frontend/index.html
index 66e5144..1d3cca9 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,6 +4,11 @@
podsteadr
+
+
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