feat(extensions): add extension loader infrastructure (#3)
Some checks are pending
Docker Compose Actions Workflow / test (push) Waiting to run

## Summary

- Adds a modular extension system for Lightning.Pub enabling third-party plugins
- Provides isolated SQLite databases per extension for data safety
- Implements ExtensionContext API for accessing Lightning.Pub services (payments, Nostr, storage)
- Supports RPC method registration with automatic namespacing
- Includes HTTP route handling for protocols like LNURL
- Event routing for payment receipts and Nostr events
- Comprehensive documentation with architecture overview and working examples

## Key Components

- `src/extensions/types.ts` - Core extension interfaces
- `src/extensions/loader.ts` - Extension discovery, loading, and lifecycle management
- `src/extensions/context.ts` - Bridge between extensions and Lightning.Pub services
- `src/extensions/database.ts` - SQLite isolation with WAL mode
- `src/extensions/README.md` - Full documentation with examples

## ExtensionContext API

| Method | Description |
|--------|-------------|
| `getApplication()` | Get application info |
| `createInvoice()` | Create Lightning invoice |
| `payInvoice()` | Pay Lightning invoice |
| `getLnurlPayInfo()` | Get LNURL-pay info for a user (enables Lightning Address/zaps) |
| `sendEncryptedDM()` | Send Nostr DM (NIP-44) |
| `publishNostrEvent()` | Publish Nostr event |
| `registerMethod()` | Register RPC method |
| `onPaymentReceived()` | Subscribe to payment callbacks |
| `onNostrEvent()` | Subscribe to Nostr events |

## Test plan

- [x] Review extension loader code for correctness
- [x] Verify TypeScript compilation succeeds
- [x] Test extension discovery from `src/extensions/` directory
- [x] Test RPC method registration and routing
- [x] Test database isolation between extensions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: boufni95 <boufni95@gmail.com>
Co-authored-by: Patrick Mulligan <patjmulligan@protonmail.com>
Reviewed-on: #3
This commit is contained in:
padreug 2026-04-02 18:47:55 +00:00
parent 72c9872b23
commit 77e5772afd
47 changed files with 10187 additions and 4828 deletions

View file

@ -1431,6 +1431,9 @@ The nostr server will send back a message response, and inside the body there wi
- __failure_reason__: _string_ *this field is optional
- __operation_payment__: _[UserOperation](#UserOperation)_ *this field is optional
- __quote__: _[InvoiceSwapQuote](#InvoiceSwapQuote)_
- __refund_address__: _string_ *this field is optional
- __refund_at_unix__: _number_ *this field is optional
- __refund_tx_id__: _string_ *this field is optional
### InvoiceSwapQuote
- __address__: _string_

View file

@ -391,6 +391,9 @@ type InvoiceSwapOperation struct {
Failure_reason string `json:"failure_reason"`
Operation_payment *UserOperation `json:"operation_payment"`
Quote *InvoiceSwapQuote `json:"quote"`
Refund_address string `json:"refund_address"`
Refund_at_unix int64 `json:"refund_at_unix"`
Refund_tx_id string `json:"refund_tx_id"`
}
type InvoiceSwapQuote struct {
Address string `json:"address"`

View file

@ -2270,15 +2270,21 @@ export type InvoiceSwapOperation = {
failure_reason?: string
operation_payment?: UserOperation
quote: InvoiceSwapQuote
refund_address?: string
refund_at_unix?: number
refund_tx_id?: string
}
export type InvoiceSwapOperationOptionalField = 'completed_at_unix' | 'failure_reason' | 'operation_payment'
export const InvoiceSwapOperationOptionalFields: InvoiceSwapOperationOptionalField[] = ['completed_at_unix', 'failure_reason', 'operation_payment']
export type InvoiceSwapOperationOptionalField = 'completed_at_unix' | 'failure_reason' | 'operation_payment' | 'refund_address' | 'refund_at_unix' | 'refund_tx_id'
export const InvoiceSwapOperationOptionalFields: InvoiceSwapOperationOptionalField[] = ['completed_at_unix', 'failure_reason', 'operation_payment', 'refund_address', 'refund_at_unix', 'refund_tx_id']
export type InvoiceSwapOperationOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: InvoiceSwapOperationOptionalField[]
completed_at_unix_CustomCheck?: (v?: number) => boolean
failure_reason_CustomCheck?: (v?: string) => boolean
operation_payment_Options?: UserOperationOptions
quote_Options?: InvoiceSwapQuoteOptions
refund_address_CustomCheck?: (v?: string) => boolean
refund_at_unix_CustomCheck?: (v?: number) => boolean
refund_tx_id_CustomCheck?: (v?: string) => boolean
}
export const InvoiceSwapOperationValidate = (o?: InvoiceSwapOperation, opts: InvoiceSwapOperationOptions = {}, path: string = 'InvoiceSwapOperation::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
@ -2300,6 +2306,15 @@ export const InvoiceSwapOperationValidate = (o?: InvoiceSwapOperation, opts: Inv
if (quoteErr !== null) return quoteErr
if ((o.refund_address || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('refund_address')) && typeof o.refund_address !== 'string') return new Error(`${path}.refund_address: is not a string`)
if (opts.refund_address_CustomCheck && !opts.refund_address_CustomCheck(o.refund_address)) return new Error(`${path}.refund_address: custom check failed`)
if ((o.refund_at_unix || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('refund_at_unix')) && typeof o.refund_at_unix !== 'number') return new Error(`${path}.refund_at_unix: is not a number`)
if (opts.refund_at_unix_CustomCheck && !opts.refund_at_unix_CustomCheck(o.refund_at_unix)) return new Error(`${path}.refund_at_unix: custom check failed`)
if ((o.refund_tx_id || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('refund_tx_id')) && typeof o.refund_tx_id !== 'string') return new Error(`${path}.refund_tx_id: is not a string`)
if (opts.refund_tx_id_CustomCheck && !opts.refund_tx_id_CustomCheck(o.refund_tx_id)) return new Error(`${path}.refund_tx_id: custom check failed`)
return null
}