feat: operator command channel via Nostr (manual dispense)
Add a Nostr-native operator command channel using Kind 21003 (CLINK Manage) events. Operators listed in OPERATOR_PUBKEYS can send encrypted commands to the machine. Phase 1 implements manual dispense: operator sends a dispense command, machine verifies sender, checks it's idle, performs a direct HAL dispense (bypassing state machine), and records the transaction. When ref_txid is provided, the referenced failed transaction is updated to status 'remediated', closing the loop on dispense errors. Changes: - CLINK types: add 'machine' resource, MachineDispenseRequest type - CLINK client: support operator pubkey list (string | string[]) - Runtime config: VITE_OPERATOR_PUBKEYS env var - Schema v4→v5: manual_dispense type, remediated_by column - Lightning services: wire onManagement callback - ATM store: handleManagementCommand with idle check + remediation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
88a52cf769
commit
e03782803b
11 changed files with 274 additions and 24 deletions
|
|
@ -69,8 +69,8 @@ export interface CLINKClientOptions {
|
|||
nostrClient: NostrClient
|
||||
/** Machine identity */
|
||||
identity: MachineIdentity
|
||||
/** Operator pubkey for management commands */
|
||||
operatorPubkey: string
|
||||
/** Operator pubkey(s) for management commands */
|
||||
operatorPubkey: string | string[]
|
||||
/** Relays to use for offers */
|
||||
relays: string[]
|
||||
/** Invoice generator function */
|
||||
|
|
@ -103,7 +103,7 @@ export type ManagementHandler = (
|
|||
export class CLINKClient {
|
||||
private nostrClient: NostrClient
|
||||
private identity: MachineIdentity
|
||||
private operatorPubkey: string
|
||||
private operatorPubkeys: string[]
|
||||
private relays: string[]
|
||||
private generateInvoice?: GenerateInvoice
|
||||
private payInvoice?: PayInvoice
|
||||
|
|
@ -117,7 +117,9 @@ export class CLINKClient {
|
|||
constructor(options: CLINKClientOptions) {
|
||||
this.nostrClient = options.nostrClient
|
||||
this.identity = options.identity
|
||||
this.operatorPubkey = options.operatorPubkey
|
||||
this.operatorPubkeys = Array.isArray(options.operatorPubkey)
|
||||
? options.operatorPubkey
|
||||
: [options.operatorPubkey]
|
||||
this.relays = options.relays
|
||||
this.generateInvoice = options.generateInvoice
|
||||
this.payInvoice = options.payInvoice
|
||||
|
|
@ -440,8 +442,8 @@ export class CLINKClient {
|
|||
* Handle management command (Kind 21003)
|
||||
*/
|
||||
private async handleManageEvent(event: Event): Promise<void> {
|
||||
// Only accept from operator
|
||||
if (event.pubkey !== this.operatorPubkey) {
|
||||
// Only accept from authorized operators
|
||||
if (!this.operatorPubkeys.includes(event.pubkey)) {
|
||||
console.warn('Ignoring management command from non-operator:', event.pubkey)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,11 +63,14 @@ export {
|
|||
type DebitFailureResponse,
|
||||
// Management types (Kind 21003)
|
||||
type ManagementRequest,
|
||||
type OfferManagementRequest,
|
||||
type MachineDispenseRequest,
|
||||
type ManagementResponse,
|
||||
type ManagementSuccessResponse,
|
||||
type ManagementFailureResponse,
|
||||
type ManagementAction,
|
||||
type ManagementResource,
|
||||
isMachineDispenseRequest,
|
||||
// Beacon types (Kind 30078)
|
||||
type ServiceBeacon,
|
||||
// Noffer types
|
||||
|
|
|
|||
|
|
@ -205,12 +205,12 @@ export interface OfferConfig {
|
|||
}
|
||||
|
||||
/** Management resource types */
|
||||
export type ManagementResource = 'offer'
|
||||
export type ManagementResource = 'offer' | 'machine'
|
||||
|
||||
/** Management request (Kind 21003) - per CLINK Manage spec */
|
||||
export interface ManagementRequest {
|
||||
/** Offer management request (resource='offer') */
|
||||
export interface OfferManagementRequest {
|
||||
/** Resource type being managed */
|
||||
resource: ManagementResource
|
||||
resource: 'offer'
|
||||
/** Action to perform */
|
||||
action: ManagementAction
|
||||
/** Pointer ID (optional, for multi-account routing) */
|
||||
|
|
@ -232,6 +232,24 @@ export interface ManagementRequest {
|
|||
}
|
||||
}
|
||||
|
||||
/** Machine dispense request (resource='machine', action='dispense') */
|
||||
export interface MachineDispenseRequest {
|
||||
resource: 'machine'
|
||||
action: 'dispense'
|
||||
/** Bills to dispense: [{denomination, count}] */
|
||||
bills: { denomination: number; count: number }[]
|
||||
/** Optional: txid of a failed transaction to mark as remediated */
|
||||
ref_txid?: string
|
||||
}
|
||||
|
||||
/** Management request (Kind 21003) - per CLINK Manage spec */
|
||||
export type ManagementRequest = OfferManagementRequest | MachineDispenseRequest
|
||||
|
||||
/** Type guard for machine dispense request */
|
||||
export function isMachineDispenseRequest(req: ManagementRequest): req is MachineDispenseRequest {
|
||||
return req.resource === 'machine' && req.action === 'dispense'
|
||||
}
|
||||
|
||||
/** Management success response (Kind 21003) - per CLINK Manage spec */
|
||||
export interface ManagementSuccessResponse {
|
||||
/** Success indicator */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue