diff --git a/static/js/index.js b/static/js/index.js index f510e9c..52d3a3b 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -191,6 +191,14 @@ window.app = Vue.createApp({ saving: false, data: {} }, + pairDialog: { + show: false, + saving: false, + machine: null, + relays: '', + durationHours: null, + result: null + }, machineDetail: { show: false, loading: false, @@ -781,6 +789,93 @@ window.app = Vue.createApp({ }) }, + // ----------------------------------------------------------------- + // Pair / revoke spire (S0 / #9, #12) + // ----------------------------------------------------------------- + openPairDialog(machine) { + this.pairDialog.machine = machine + this.pairDialog.relays = '' + this.pairDialog.durationHours = null + this.pairDialog.result = null + this.pairDialog.show = true + }, + + async submitPair() { + const relays = (this.pairDialog.relays || '') + .split(/[\s,]+/) + .map(s => s.trim()) + .filter(Boolean) + if (!relays.length) { + Quasar.Notify.create({ + type: 'negative', + message: 'At least one relay is required' + }) + return + } + const body = {relays} + if (this.pairDialog.durationHours) { + body.duration_hours = Number(this.pairDialog.durationHours) + } + this.pairDialog.saving = true + try { + const {data} = await LNbits.api.request( + 'POST', + `${MACHINES_PATH}/${this.pairDialog.machine.id}/pair`, + null, + body + ) + this.pairDialog.result = data + // The bunker-minted key becomes the machine identity; reflect it + + // the paired state in the row immediately. + const m = this.machines.find(x => x.id === this.pairDialog.machine.id) + if (m) { + m.machine_npub = data.spire_pubkey_hex + m.bunker_spire_key_name = data.bunker_key_name + m.paired_at = new Date().toISOString() + } + Quasar.Notify.create({ + type: 'positive', + message: 'Spire paired — hand the seed URL to the device' + }) + } catch (e) { + this._notifyError(e, 'Pairing failed') + } finally { + this.pairDialog.saving = false + } + }, + + confirmRevokeMachine(machine) { + Quasar.Dialog.create({ + title: 'Revoke spire access?', + message: + `This cuts ${machine.name || machine.machine_npub.slice(0, 12)}'s` + + ' signing access at the bunker — the spire can no longer submit' + + ' cash-outs until you re-pair it. Continue?', + html: true, + cancel: true, + persistent: true + }).onOk(async () => { + try { + const {data} = await LNbits.api.request( + 'POST', + `${MACHINES_PATH}/${machine.id}/revoke`, + null + ) + const m = this.machines.find(x => x.id === machine.id) + if (m) m.paired_at = null + Quasar.Notify.create({ + type: data.revoked_count >= 1 ? 'positive' : 'warning', + message: + data.revoked_count >= 1 + ? 'Spire access revoked' + : 'Nothing was bound (the spire never connected)' + }) + } catch (e) { + this._notifyError(e, 'Revoke failed') + } + }) + }, + // ----------------------------------------------------------------- // Machine detail dialog (P9b) // ----------------------------------------------------------------- diff --git a/templates/spirekeeper/index.html b/templates/spirekeeper/index.html index b58714e..7bdebd3 100644 --- a/templates/spirekeeper/index.html +++ b/templates/spirekeeper/index.html @@ -131,6 +131,17 @@
+ + paired + Bunker key minted; paired ${ new Date(props.row.paired_at).toLocaleString() } + + + not paired + Edit + + ${ props.row.paired_at ? 'Re-pair (new seed URL)' : 'Pair (seed URL)' } + + + Revoke spire access + @@ -821,6 +843,97 @@ + + + + + + +
Pair spire
+ + +
+ + + +

+ Mints a dedicated signing key for + + inside the operator bunker and issues a one-shot seed URL. The + spire's key never touches its disk; its cash-outs route to this + machine's wallet. Re-pairing issues a fresh seed. +

+ + + + +
+ + + + + + + + + + Paired. Scan this on the spire at first boot, or paste the seed URL + into provision-atm. Shown once — copy it now. + + +
+ +
+ + + + + +
+ Spire identity: + + + Copy npub + +
+
+ + + +
+
+