Merge remote-tracking branch 'gitea-ai/main' into gsd/phase-13-aiui-functional-conversational-node-control-and-content-surf

This commit is contained in:
archipelago
2026-08-08 10:10:33 -04:00
3 changed files with 167 additions and 1 deletions
+102
View File
@@ -3704,6 +3704,48 @@ app.post('/rpc/v1', (req, res) => {
})
}
// Lightning credential rotation (Settings → Lightning credentials).
// Stateful so the dev preview can exercise the whole arc without a node:
// request a rotation and the steps advance on each poll, ending with the
// BTCPay reconnect. Digests only — the real daemon never returns a
// macaroon and neither does this.
case 'lnd.macaroon-status': {
return res.json({
result: {
installed: true,
admin_macaroon_sha256: '52219e90aeba8ac6a98fdac1cc754fe0a2e8407ae6758ffe2cf92039503f5575',
issued_at: '2026-08-08 06:03:11',
identity_pubkey: '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456',
channels_open: 5,
channels_pending: 1,
lnd_error: null,
btcpay_uses_internal_lnd: true,
// Flip to false to see the "BTCPay is holding an old credential"
// warning this feature exists to prevent.
btcpay_credential_current: true,
rotation: macaroonRotationSnapshot(),
},
})
}
case 'lnd.rotate-macaroons': {
if (!params?.password) {
return res.json({ error: { code: -1, message: 'Node password required to rotate Lightning credentials' } })
}
if (params.password !== userState.passwordHash && params.password !== MOCK_PASSWORD) {
return res.json({ error: { code: -1, message: 'Password verification failed' } })
}
if (macaroonRotation.running) {
return res.json({ error: { code: -1, message: 'A macaroon rotation is already running on this node' } })
}
startMacaroonRotation()
return res.json({ result: { status: 'started' } })
}
case 'lnd.macaroon-rotation-progress': {
return res.json({ result: macaroonRotationSnapshot() })
}
case 'lnd.gettransactions': {
const pending = walletState.transactions.filter(tx => tx.direction === 'incoming' && tx.num_confirmations < 3).length
return res.json({
@@ -6075,6 +6117,66 @@ const walletState = sessionBucketProxy('walletState')
const userState = sessionBucketProxy('userState')
const mockState = sessionBucketProxy('mockState')
// ── Lightning macaroon rotation (mock) ──────────────────────────────────────
// Mirrors the daemon's `RotationProgress`: same step keys and the same five
// states, so the Settings section can be driven end-to-end without a node.
// Advances one step per poll rather than on a timer, which keeps it
// deterministic and makes each intermediate state actually observable.
const MACAROON_ROTATION_STEPS = [
['preflight', 'Check LND is healthy and record what must survive', '5 channel(s) open, 1 pending — these must be identical afterwards'],
['backup', 'Back up the current macaroon material', '8 file(s) copied to /var/lib/archipelago/lnd/macaroon-rotation-20260808T120000Z'],
['stop', 'Stop Lightning', null],
['remove', 'Remove the old root key and issued macaroons', '8 file(s) removed'],
['start', 'Start Lightning and unlock the wallet', 'Lightning is up with freshly minted credentials'],
['verify', 'Confirm the node and its channels are unchanged', 'same node, same 5 channel(s)'],
['btcpay', 'Reconnect BTCPay Server to the new credentials', 'Connection string updated. BTCPay restarts itself within a minute or two to pick it up.'],
]
const macaroonRotation = { running: false, done: 0, ok: null, startedAt: null, finishedAt: null }
function startMacaroonRotation() {
macaroonRotation.running = true
macaroonRotation.done = 0
macaroonRotation.ok = null
macaroonRotation.startedAt = new Date().toISOString()
macaroonRotation.finishedAt = null
}
function macaroonRotationSnapshot() {
if (macaroonRotation.running) {
macaroonRotation.done += 1
if (macaroonRotation.done >= MACAROON_ROTATION_STEPS.length) {
macaroonRotation.done = MACAROON_ROTATION_STEPS.length
macaroonRotation.running = false
macaroonRotation.ok = true
macaroonRotation.finishedAt = new Date().toISOString()
}
}
const started = macaroonRotation.startedAt !== null
return {
running: macaroonRotation.running,
ok: macaroonRotation.ok,
started_at: macaroonRotation.startedAt,
finished_at: macaroonRotation.finishedAt,
error: null,
steps: MACAROON_ROTATION_STEPS.map(([key, label, detail], i) => {
let state = 'pending'
if (started) {
if (i < macaroonRotation.done) state = 'done'
else if (i === macaroonRotation.done && macaroonRotation.running) state = 'running'
}
return { key, label, state, detail: state === 'done' ? detail : null }
}),
backup_path: started ? '/var/lib/archipelago/lnd/macaroon-rotation-20260808T120000Z' : null,
identity_pubkey: started ? '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' : null,
channels_before: started ? 5 : null,
channels_after: macaroonRotation.ok ? 5 : null,
new_admin_macaroon_sha256: macaroonRotation.ok
? '8c19b99de4a8f5c3145d8189500089829174909ca09b48f55e0464239bd8d412'
: null,
}
}
// Seed for the per-session Tor services demo state (tor.list-services /
// tor.create-service / tor.delete-service round-trip against this).
function defaultTorServices() {