fix(tor): resolve app ports dynamically + auto-create hidden service on install

'Add Service' only worked for the 19 apps in the hardcoded
known_service_port map — everything else failed with 'Unknown app'.
tor.create-service and tor.toggle-app now fall back to the app's live
launch address from the scanner state, so any manifest-driven app gets
a .onion without a per-app entry (the static map still wins for
protocol services like bitcoin, which must expose 8333, not a UI port).
toggle-app also no longer writes a broken port-0 HiddenServicePort for
unknown apps.

Every successful package.install now auto-creates the app's hidden
service (detached, best-effort, retries while the scanner derives the
port; skips protocol services and existing entries).

The demo mock gains stateful tor.create-service/tor.delete-service so
the demo round-trips instead of erroring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-15 05:56:22 -04:00
co-authored by Claude Fable 5
parent 5d4d40e9e8
commit 0f5ea9ae15
4 changed files with 125 additions and 16 deletions
+44 -7
View File
@@ -2636,20 +2636,45 @@ app.post('/rpc/v1', (req, res) => {
// Tor & Peer Networking
// =========================================================================
case 'tor.list-services': {
mockState.torServices = mockState.torServices || defaultTorServices()
return res.json({
result: {
tor_running: true,
services: [
{ name: 'archipelago', local_port: 5678, onion_address: 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion', enabled: true, unauthenticated: false, protocol: false },
{ name: 'bitcoin', local_port: 8333, onion_address: 'btcmockxj4k5pnw7hv8qz9jcbr2dwefys6ockqzf1mzjlvxot5ioad.onion', enabled: true, unauthenticated: false, protocol: false },
{ name: 'lnd', local_port: 9735, onion_address: 'lndmockab3c4def5ghi6jkl7mno8pqr9stu0vwx1yz2ab3c4def5ghi.onion', enabled: true, unauthenticated: false, protocol: false },
{ name: 'electrs', local_port: 50001, onion_address: 'elecmockyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba1xyz9wvu8tsr7q.onion', enabled: true, unauthenticated: true, protocol: false },
{ name: 'nostr-relay', local_port: 7777, onion_address: null, enabled: false, unauthenticated: true, protocol: false },
],
services: mockState.torServices,
},
})
}
case 'tor.create-service': {
mockState.torServices = mockState.torServices || defaultTorServices()
const name = params?.name
if (!name) {
return res.json({ error: { code: -1, message: 'Missing name' } })
}
if (mockState.torServices.some(s => s.name === name)) {
return res.json({ error: { code: -1, message: `Service '${name}' already exists` } })
}
const stem = `${name.toLowerCase().replace(/[^a-z0-9]/g, '')}demo`
const onion = (stem + 'x7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioadmnopqrstuv').slice(0, 56) + '.onion'
const svc = { name, local_port: params?.local_port || 8080, onion_address: onion, enabled: true, unauthenticated: false, protocol: false }
mockState.torServices = [...mockState.torServices, svc]
console.log(`[Tor] Created hidden service: ${name}${onion}`)
return res.json({ result: { created: true, name, onion_address: onion } })
}
case 'tor.delete-service': {
mockState.torServices = mockState.torServices || defaultTorServices()
const name = params?.name
if (name === 'archipelago') {
return res.json({ error: { code: -1, message: "Cannot delete the node's own Tor service" } })
}
if (!mockState.torServices.some(s => s.name === name)) {
return res.json({ error: { code: -1, message: `Service '${name}' not found` } })
}
mockState.torServices = mockState.torServices.filter(s => s.name !== name)
return res.json({ result: { deleted: true, name } })
}
case 'node.tor-address': {
return res.json({
result: {
@@ -5119,6 +5144,18 @@ const mockData = sessionBucketProxy('mockData')
const walletState = sessionBucketProxy('walletState')
const userState = sessionBucketProxy('userState')
const mockState = sessionBucketProxy('mockState')
// Seed for the per-session Tor services demo state (tor.list-services /
// tor.create-service / tor.delete-service round-trip against this).
function defaultTorServices() {
return [
{ name: 'archipelago', local_port: 5678, onion_address: 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion', enabled: true, unauthenticated: false, protocol: false },
{ name: 'bitcoin', local_port: 8333, onion_address: 'btcmockxj4k5pnw7hv8qz9jcbr2dwefys6ockqzf1mzjlvxot5ioad.onion', enabled: true, unauthenticated: false, protocol: false },
{ name: 'lnd', local_port: 9735, onion_address: 'lndmockab3c4def5ghi6jkl7mno8pqr9stu0vwx1yz2ab3c4def5ghi.onion', enabled: true, unauthenticated: false, protocol: false },
{ name: 'electrs', local_port: 50001, onion_address: 'elecmockyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba1xyz9wvu8tsr7q.onion', enabled: true, unauthenticated: true, protocol: false },
{ name: 'nostr-relay', local_port: 7777, onion_address: null, enabled: false, unauthenticated: true, protocol: false },
]
}
const bitcoinRelayMockState = sessionBucketProxy('bitcoinRelayMockState')
// Demo session lifecycle: keyed by the `demo_sid` cookie, capped, idle-reaped.