Move 9 development/testing .mjs scripts out of the package root into dev/ to keep the published package clean. Update relative imports and dev.sh reference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
270 lines
8.7 KiB
JavaScript
270 lines
8.7 KiB
JavaScript
/**
|
|
* ATM Debit Authorization Agent
|
|
*
|
|
* This script demonstrates how an ATM can act as the authorization authority
|
|
* for CLINK debit requests, similar to an "admin macaroon" for Lightning.
|
|
*
|
|
* Flow:
|
|
* 1. Generate keypair for ATM (or load from secure storage)
|
|
* 2. Link keypair to Lightning.Pub user account
|
|
* 3. Subscribe to live debit requests
|
|
* 4. Auto-approve requests (within configured limits)
|
|
*
|
|
* Prerequisites:
|
|
* - Get a linking token from Lightning.Pub HTTP API
|
|
* - Run: curl -X POST "http://localhost:1776/api/app/user/npub/token/reset" \
|
|
* -H "Authorization: Bearer $APP_TOKEN" \
|
|
* -H "Content-Type: application/json" \
|
|
* -d '{"user_identifier": "YOUR_USER_IDENTIFIER"}'
|
|
*/
|
|
|
|
import { Relay } from 'nostr-tools/relay'
|
|
import { finalizeEvent, getPublicKey, generateSecretKey } from 'nostr-tools'
|
|
import * as nip44v1 from './nip44v1.mjs'
|
|
|
|
// Configuration
|
|
const LINKING_TOKEN = process.argv[2]
|
|
const LIGHTNING_PUB_PUBKEY = '6c59284e3da31b776cb1c06324c25f4a0b0308177af9f8aec5ebef07b44c3fdf'
|
|
const RELAY_URL = process.env.RELAY_URL || 'ws://localhost:7777'
|
|
|
|
// Generate ATM keypair (in production, this would be stored securely)
|
|
const ATM_PRIVATE_KEY = generateSecretKey()
|
|
const ATM_PUBLIC_KEY = getPublicKey(ATM_PRIVATE_KEY)
|
|
const ATM_PRIVATE_KEY_HEX = Buffer.from(ATM_PRIVATE_KEY).toString('hex')
|
|
|
|
if (!LINKING_TOKEN) {
|
|
console.log('ATM Debit Authorization Agent')
|
|
console.log('==============================')
|
|
console.log('')
|
|
console.log('Usage: node atm-debit-agent.mjs <linking-token>')
|
|
console.log('')
|
|
console.log('Get a linking token:')
|
|
console.log(' curl -X POST "http://localhost:1776/api/app/user/npub/token/reset" \\')
|
|
console.log(' -H "Authorization: Bearer $APP_TOKEN" \\')
|
|
console.log(' -H "Content-Type: application/json" \\')
|
|
console.log(' -d \'{"user_identifier": "YOUR_USER_IDENTIFIER"}\'')
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log('=== ATM Debit Authorization Agent ===')
|
|
console.log('')
|
|
console.log('ATM Pubkey:', ATM_PUBLIC_KEY)
|
|
console.log('Lightning.Pub Pubkey:', LIGHTNING_PUB_PUBKEY)
|
|
console.log('Linking Token:', LINKING_TOKEN.substring(0, 16) + '...')
|
|
console.log('')
|
|
|
|
async function main() {
|
|
// Connect to relay
|
|
console.log('Connecting to relay...')
|
|
const relay = await Relay.connect(RELAY_URL)
|
|
console.log('Connected!')
|
|
console.log('')
|
|
|
|
// Create conversation key for NIP-44 v1 encryption (used by Kind 21000 RPC)
|
|
const conversationKey = nip44v1.getConversationKey(ATM_PRIVATE_KEY_HEX, LIGHTNING_PUB_PUBKEY)
|
|
|
|
// Step 1: Link NPub through token
|
|
console.log('Step 1: Linking ATM keypair to user account...')
|
|
|
|
const linkRequest = {
|
|
rpcName: 'LinkNPubThroughToken',
|
|
authIdentifier: ATM_PUBLIC_KEY,
|
|
body: {
|
|
token: LINKING_TOKEN,
|
|
},
|
|
}
|
|
|
|
const linkEvent = finalizeEvent(
|
|
{
|
|
kind: 21000,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [['p', LIGHTNING_PUB_PUBKEY]],
|
|
content: nip44v1.encrypt(JSON.stringify(linkRequest), conversationKey),
|
|
},
|
|
ATM_PRIVATE_KEY
|
|
)
|
|
|
|
// Subscribe for response
|
|
let linkingComplete = false
|
|
const linkSub = relay.subscribe(
|
|
[
|
|
{
|
|
kinds: [21000],
|
|
authors: [LIGHTNING_PUB_PUBKEY],
|
|
'#p': [ATM_PUBLIC_KEY],
|
|
since: Math.floor(Date.now() / 1000) - 5,
|
|
},
|
|
],
|
|
{
|
|
onevent(evt) {
|
|
try {
|
|
const decrypted = nip44v1.decrypt(evt.content, conversationKey)
|
|
const response = JSON.parse(decrypted)
|
|
console.log('Link response:', JSON.stringify(response))
|
|
if (response.status === 'OK') {
|
|
linkingComplete = true
|
|
console.log('Keypair linked successfully!')
|
|
}
|
|
} catch (err) {
|
|
console.log('Failed to decrypt link response:', err.message)
|
|
}
|
|
},
|
|
}
|
|
)
|
|
|
|
await relay.publish(linkEvent)
|
|
console.log(
|
|
'Link request sent (event id:',
|
|
linkEvent.id.substring(0, 16) + '...), waiting for confirmation...'
|
|
)
|
|
|
|
// Wait for linking to complete
|
|
for (let i = 0; i < 10 && !linkingComplete; i++) {
|
|
await new Promise((r) => setTimeout(r, 1000))
|
|
if (i % 3 === 2) console.log('Still waiting for link confirmation...')
|
|
}
|
|
linkSub.close()
|
|
|
|
if (!linkingComplete) {
|
|
console.log('Warning: Did not receive linking confirmation, continuing anyway...')
|
|
}
|
|
console.log('')
|
|
|
|
// Step 2: Subscribe to live debit requests
|
|
console.log('Step 2: Subscribing to live debit requests...')
|
|
|
|
const subscribeRequest = {
|
|
rpcName: 'GetLiveDebitRequests',
|
|
authIdentifier: ATM_PUBLIC_KEY,
|
|
body: {},
|
|
}
|
|
|
|
const subEvent = finalizeEvent(
|
|
{
|
|
kind: 21000,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [['p', LIGHTNING_PUB_PUBKEY]],
|
|
content: nip44v1.encrypt(JSON.stringify(subscribeRequest), conversationKey),
|
|
},
|
|
ATM_PRIVATE_KEY
|
|
)
|
|
|
|
// Subscribe for debit requests and responses
|
|
console.log('Listening for debit requests...')
|
|
console.log('(Scan the ndebit QR code with ShockWallet to test)')
|
|
console.log('')
|
|
|
|
const debitSub = relay.subscribe(
|
|
[
|
|
{
|
|
kinds: [21000],
|
|
authors: [LIGHTNING_PUB_PUBKEY],
|
|
'#p': [ATM_PUBLIC_KEY],
|
|
since: Math.floor(Date.now() / 1000) - 5,
|
|
},
|
|
],
|
|
{
|
|
async onevent(evt) {
|
|
try {
|
|
const decrypted = nip44v1.decrypt(evt.content, conversationKey)
|
|
const message = JSON.parse(decrypted)
|
|
|
|
// Check if this is a debit request (has request_id and debit fields)
|
|
if (message.requestId === 'GetLiveDebitRequests' && message.debit) {
|
|
console.log('')
|
|
console.log('========================================')
|
|
console.log('Received debit request!')
|
|
console.log(' Request ID:', message.request_id)
|
|
console.log(' From npub:', message.npub)
|
|
console.log(' Debit type:', message.debit.type)
|
|
|
|
if (message.debit.type === 'invoice' && message.debit.invoice) {
|
|
console.log(' Invoice:', message.debit.invoice.substring(0, 50) + '...')
|
|
|
|
// Auto-approve by responding with INVOICE type
|
|
console.log('')
|
|
console.log('Auto-approving debit request...')
|
|
|
|
const approveRequest = {
|
|
rpcName: 'RespondToDebit',
|
|
authIdentifier: ATM_PUBLIC_KEY,
|
|
body: {
|
|
npub: message.npub,
|
|
request_id: message.request_id,
|
|
response: {
|
|
type: 'invoice',
|
|
invoice: message.debit.invoice,
|
|
},
|
|
},
|
|
}
|
|
|
|
const approveEvent = finalizeEvent(
|
|
{
|
|
kind: 21000,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [['p', LIGHTNING_PUB_PUBKEY]],
|
|
content: nip44v1.encrypt(JSON.stringify(approveRequest), conversationKey),
|
|
},
|
|
ATM_PRIVATE_KEY
|
|
)
|
|
|
|
await relay.publish(approveEvent)
|
|
console.log('Approval sent! (event id:', approveEvent.id.substring(0, 16) + '...)')
|
|
} else if (message.debit.type === 'budget') {
|
|
console.log(' Budget request - ignoring for now')
|
|
} else if (message.debit.type === 'fullAccess') {
|
|
console.log(' Full access request - ignoring for now')
|
|
}
|
|
|
|
console.log('========================================')
|
|
console.log('')
|
|
} else if (message.rpcName) {
|
|
// This is a response to our RPC request
|
|
console.log('RPC response:', message.rpcName, ':', message.status || 'received')
|
|
} else {
|
|
// Log other messages for debugging
|
|
console.log('Message received:', JSON.stringify(message).substring(0, 100))
|
|
}
|
|
} catch (err) {
|
|
// Ignore decryption failures (may be messages for other clients)
|
|
if (!err.message.includes('Unsupported')) {
|
|
console.log('Error processing message:', err.message)
|
|
}
|
|
}
|
|
},
|
|
}
|
|
)
|
|
|
|
await relay.publish(subEvent)
|
|
console.log('Subscription request sent (event id:', subEvent.id.substring(0, 16) + '...)')
|
|
console.log('')
|
|
|
|
// Keep running
|
|
console.log('ATM Debit Agent running. Press Ctrl+C to exit.')
|
|
console.log('')
|
|
|
|
// Handle graceful shutdown
|
|
process.on('SIGINT', () => {
|
|
console.log('\nShutting down...')
|
|
debitSub.close()
|
|
relay.close()
|
|
process.exit(0)
|
|
})
|
|
|
|
// Keep alive with heartbeat
|
|
let heartbeatCount = 0
|
|
while (true) {
|
|
await new Promise((r) => setTimeout(r, 10000))
|
|
heartbeatCount++
|
|
if (heartbeatCount % 6 === 0) {
|
|
// Every minute
|
|
console.log('Still listening... (' + heartbeatCount * 10 + 's)')
|
|
}
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('Error:', err.message)
|
|
console.error(err.stack)
|
|
process.exit(1)
|
|
})
|