Browse Source

Persist signer logins

imwald
Nuša Pukšič 3 months ago
parent
commit
071d863258
  1. 9
      assets/controllers/amber_connect_controller.js
  2. 18
      assets/controllers/nostr_comment_controller.js
  3. 28
      assets/controllers/nostr_index_sign_controller.js
  4. 19
      assets/controllers/nostr_single_sign_controller.js
  5. 17
      assets/controllers/reading_list_dropdown_controller.js
  6. 58
      assets/controllers/signer_manager.js

9
assets/controllers/amber_connect_controller.js

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { Controller } from '@hotwired/stimulus';
import { getPublicKey, SimplePool } from 'nostr-tools';
import { BunkerSigner } from "nostr-tools/nip46";
import { setRemoteSignerSession, clearRemoteSignerSession } from './signer_manager.js';
export default class extends Controller {
static targets = ['qr', 'status'];
@ -19,6 +20,7 @@ export default class extends Controller { @@ -19,6 +20,7 @@ export default class extends Controller {
disconnect() {
try { this._signer?.close?.(); } catch (_) {}
try { this._pool?.close?.([]); } catch (_) {}
clearRemoteSignerSession();
}
async _init() {
@ -98,6 +100,13 @@ export default class extends Controller { @@ -98,6 +100,13 @@ export default class extends Controller {
headers: { 'Authorization': 'Nostr ' + btoa(JSON.stringify(signed)) }
});
if (resp.ok) {
// Persist remote signer session for reuse after reload
setRemoteSignerSession({
privkey: this._localSecretKey,
uri: this._uri,
relays: this._relays,
secret: this._secret
});
this._setStatus('Authenticated. Reloading…');
setTimeout(() => window.location.reload(), 500);
} else {

18
assets/controllers/nostr_comment_controller.js

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { Controller } from '@hotwired/stimulus';
import { getSigner } from './signer_manager.js';
// NIP-22 Comment Publishing Controller
// Usage: Attach to a form with data attributes for root/parent context
@ -28,8 +29,11 @@ export default class extends Controller { @@ -28,8 +29,11 @@ export default class extends Controller {
this.showError('Missing CSRF token');
return;
}
if (!window.nostr) {
this.showError('Nostr extension not found');
let signer;
try {
signer = await getSigner();
} catch (e) {
this.showError('No Nostr signer available. Please connect Amber or install a Nostr signer extension.');
return;
}
@ -52,10 +56,10 @@ export default class extends Controller { @@ -52,10 +56,10 @@ export default class extends Controller {
}
// Create NIP-22 event
const nostrEvent = await this.createNip22Event(formData);
const nostrEvent = await this.createNip22Event(formData, signer);
this.showStatus('Requesting signature from Nostr extension...');
const signedEvent = await window.nostr.signEvent(nostrEvent);
this.showStatus('Requesting signature from Nostr signer...');
const signedEvent = await signer.signEvent(nostrEvent);
this.showStatus('Publishing comment...');
await this.sendToBackend(signedEvent, formData);
@ -98,9 +102,9 @@ export default class extends Controller { @@ -98,9 +102,9 @@ export default class extends Controller {
return !(/[<>\*\_\`\[\]#]/.test(text));
}
async createNip22Event({ content, root, parent }) {
async createNip22Event({ content, root, parent }, signer) {
// Get user's public key
const pubkey = await window.nostr.getPublicKey();
const pubkey = await signer.getPublicKey();
const created_at = Math.floor(Date.now() / 1000);
// Build tags according to NIP-22
const tags = [];

28
assets/controllers/nostr_index_sign_controller.js

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { Controller } from '@hotwired/stimulus';
import { getSigner } from './signer_manager.js';
export default class extends Controller {
static targets = ['status', 'publishButton', 'computedPreview'];
@ -50,63 +51,53 @@ export default class extends Controller { @@ -50,63 +51,53 @@ export default class extends Controller {
async signAndPublish(event) {
event.preventDefault();
if (!window.nostr) {
this.showError('Nostr extension not found');
let signer;
try {
signer = await getSigner();
} catch (e) {
this.showError('No Nostr signer available. Please connect Amber or install a Nostr signer extension.');
return;
}
if (!this.publishUrlValue || !this.csrfTokenValue) {
this.showError('Missing config');
return;
}
this.publishButtonTarget.disabled = true;
try {
const pubkey = await window.nostr.getPublicKey();
const pubkey = await signer.getPublicKey();
const catSkeletons = JSON.parse(this.categoryEventsValue || '[]');
const magSkeleton = JSON.parse(this.magazineEventValue || '{}');
const categoryCoordinates = [];
// 1) Publish each category index
for (let i = 0; i < catSkeletons.length; i++) {
const evt = catSkeletons[i];
this.ensureCreatedAt(evt);
this.ensureContent(evt);
evt.pubkey = pubkey;
const slug = this.extractSlug(evt.tags);
if (!slug) throw new Error('Category missing slug (d tag)');
this.showStatus(`Signing category ${i + 1}/${catSkeletons.length}`);
const signed = await window.nostr.signEvent(evt);
const signed = await signer.signEvent(evt);
this.showStatus(`Publishing category ${i + 1}/${catSkeletons.length}`);
await this.publishSigned(signed);
// Coordinate for the category index (kind:pubkey:slug)
const coord = `30040:${pubkey}:${slug}`;
categoryCoordinates.push(coord);
}
// 2) Build magazine event with 'a' tags referencing cats
this.showStatus('Preparing magazine index…');
this.ensureCreatedAt(magSkeleton);
this.ensureContent(magSkeleton);
magSkeleton.pubkey = pubkey;
// Remove any pre-existing 'a' to avoid duplicates, then add new ones
magSkeleton.tags = (magSkeleton.tags || []).filter(t => t[0] !== 'a');
categoryCoordinates.forEach(c => magSkeleton.tags.push(['a', c]));
// 3) Sign and publish magazine
this.showStatus('Signing magazine index…');
const signedMag = await window.nostr.signEvent(magSkeleton);
const signedMag = await signer.signEvent(magSkeleton);
this.showStatus('Publishing magazine index…');
await this.publishSigned(signedMag);
this.showSuccess('Published magazine and categories successfully');
} catch (e) {
console.error(e);
this.showError(e.message || 'Publish failed');
@ -163,4 +154,3 @@ export default class extends Controller { @@ -163,4 +154,3 @@ export default class extends Controller {
}
}
}

19
assets/controllers/nostr_single_sign_controller.js

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { Controller } from '@hotwired/stimulus';
import { getSigner } from './signer_manager.js';
export default class extends Controller {
static targets = ['status', 'publishButton', 'computedPreview'];
@ -18,9 +19,10 @@ export default class extends Controller { @@ -18,9 +19,10 @@ export default class extends Controller {
try {
const skeleton = JSON.parse(this.eventValue || '{}');
let pubkey = '<pubkey>';
if (window.nostr && typeof window.nostr.getPublicKey === 'function') {
try { pubkey = await window.nostr.getPublicKey(); } catch (_) {}
}
try {
const signer = await getSigner();
pubkey = await signer.getPublicKey();
} catch (_) {}
const preview = JSON.parse(JSON.stringify(skeleton));
preview.pubkey = pubkey;
// Update content from textarea if present
@ -37,8 +39,11 @@ export default class extends Controller { @@ -37,8 +39,11 @@ export default class extends Controller {
async signAndPublish(event) {
event.preventDefault();
if (!window.nostr) {
this.showError('Nostr extension not found');
let signer;
try {
signer = await getSigner();
} catch (e) {
this.showError('No Nostr signer available. Please connect Amber or install a Nostr signer extension.');
return;
}
if (!this.publishUrlValue || !this.csrfTokenValue) {
@ -48,7 +53,7 @@ export default class extends Controller { @@ -48,7 +53,7 @@ export default class extends Controller {
this.publishButtonTarget.disabled = true;
try {
const pubkey = await window.nostr.getPublicKey();
const pubkey = await signer.getPublicKey();
const skeleton = JSON.parse(this.eventValue || '{}');
// Update content from textarea before signing
const textarea = this.element.querySelector('textarea');
@ -60,7 +65,7 @@ export default class extends Controller { @@ -60,7 +65,7 @@ export default class extends Controller {
skeleton.pubkey = pubkey;
this.showStatus('Signing feedback…');
const signed = await window.nostr.signEvent(skeleton);
const signed = await signer.signEvent(skeleton);
this.showStatus('Publishing…');
await this.publishSigned(signed);

17
assets/controllers/reading_list_dropdown_controller.js

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { Controller } from '@hotwired/stimulus';
import { getSigner } from './signer_manager.js';
export default class extends Controller {
static targets = ['dropdown', 'status', 'menu'];
@ -64,24 +65,24 @@ export default class extends Controller { @@ -64,24 +65,24 @@ export default class extends Controller {
const slug = event.currentTarget.dataset.slug;
const title = event.currentTarget.dataset.title;
if (!window.nostr) {
this.showError('Nostr extension not found. Please install a Nostr signer extension.');
let signer;
try {
signer = await getSigner();
} catch (e) {
this.showError('No Nostr signer available. Please connect Amber or install a Nostr signer extension.');
return;
}
try {
this.showStatus(`Adding to "${title}"...`);
// Parse the existing lists data
// Build the event skeleton for the updated reading list
const lists = JSON.parse(this.listsValue || '[]');
const selectedList = lists.find(l => l.slug === slug);
if (!selectedList) {
this.showError('Reading list not found');
return;
}
// Check if article is already in the list
if (selectedList.articles && selectedList.articles.includes(this.coordinateValue)) {
this.showSuccess(`Already in "${title}"`);
setTimeout(() => {
@ -90,13 +91,11 @@ export default class extends Controller { @@ -90,13 +91,11 @@ export default class extends Controller {
}, 2000);
return;
}
// Build the event skeleton for the updated reading list
const eventSkeleton = await this.buildReadingListEvent(selectedList);
// Sign the event
this.showStatus(`Signing update to "${title}"...`);
const signedEvent = await window.nostr.signEvent(eventSkeleton);
const signedEvent = await signer.signEvent(eventSkeleton);
// Publish the event
this.showStatus(`Publishing update...`);

58
assets/controllers/signer_manager.js

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
// Shared signer manager for Nostr signers (remote and extension)
import { SimplePool } from 'nostr-tools';
import { BunkerSigner } from 'nostr-tools/nip46';
const REMOTE_SIGNER_KEY = 'amber_remote_signer';
let remoteSigner = null;
let remoteSignerPromise = null;
let remoteSignerPool = null;
export async function getSigner() {
// If remote signer session is active, use it
const session = getRemoteSignerSession();
if (session) {
if (remoteSigner) return remoteSigner;
if (remoteSignerPromise) return remoteSignerPromise;
remoteSignerPromise = createRemoteSigner(session).then(signer => {
remoteSigner = signer;
return signer;
});
return remoteSignerPromise;
}
// Fallback to browser extension
if (window.nostr && typeof window.nostr.signEvent === 'function') {
return window.nostr;
}
throw new Error('No signer available');
}
export function setRemoteSignerSession(session) {
localStorage.setItem(REMOTE_SIGNER_KEY, JSON.stringify(session));
}
export function clearRemoteSignerSession() {
localStorage.removeItem(REMOTE_SIGNER_KEY);
remoteSigner = null;
remoteSignerPromise = null;
if (remoteSignerPool) {
try { remoteSignerPool.close?.([]); } catch (_) {}
remoteSignerPool = null;
}
}
export function getRemoteSignerSession() {
const raw = localStorage.getItem(REMOTE_SIGNER_KEY);
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
async function createRemoteSigner(session) {
remoteSignerPool = new SimplePool();
return await BunkerSigner.fromURI(session.privkey, session.uri, { pool: remoteSignerPool });
}
Loading…
Cancel
Save