Compare commits

..

2 commits

Author SHA1 Message Date
Patrick Mulligan
a2366c40f2 feat(nip05): add Lightning Address support for zaps
Some checks are pending
Docker Compose Actions Workflow / test (push) Waiting to run
Adds /.well-known/lnurlp/:username endpoint that:
1. Looks up username in NIP-05 database
2. Gets LNURL-pay info from Lightning.Pub for that user
3. Returns standard LUD-16 response for wallet compatibility

This makes NIP-05 addresses (alice@domain) work seamlessly as
Lightning Addresses for receiving payments and NIP-57 zaps.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-01 13:15:43 -04:00
Patrick Mulligan
cf85a2fd7c feat(extensions): add NIP-05 identity extension
Implements Nostr NIP-05 for human-readable identity verification:
- Username claiming and management (username@domain)
- /.well-known/nostr.json endpoint per spec
- Optional relay hints in JSON response
- Admin controls for identity management

RPC methods:
- nip05.claim - Claim a username
- nip05.release - Release your username
- nip05.updateRelays - Update relay hints
- nip05.getMyIdentity - Get your identity
- nip05.lookup - Look up by username
- nip05.lookupByPubkey - Look up by pubkey
- nip05.listIdentities - List all (admin)
- nip05.deactivate/reactivate - Admin controls

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-01 13:15:43 -04:00
3 changed files with 5 additions and 55 deletions

View file

@ -189,12 +189,6 @@ export default class Nip05Extension implements Extension {
* "relays": { "<pubkey hex>": ["wss://..."] } * "relays": { "<pubkey hex>": ["wss://..."] }
* } * }
*/ */
/**
* NIP-05 spec: "The /.well-known/nostr.json endpoint MUST NOT return any
* HTTP redirects." This extension always returns direct 200/4xx/5xx responses.
* Deployment note: ensure reverse proxies do not add 3xx redirects on this path
* (e.g. HTTPHTTPS or trailing-slash redirects).
*/
private async handleNostrJson(req: HttpRequest): Promise<HttpResponse> { private async handleNostrJson(req: HttpRequest): Promise<HttpResponse> {
try { try {
// Get application ID from request context // Get application ID from request context
@ -278,11 +272,6 @@ export default class Nip05Extension implements Extension {
description: `Pay to ${username}` description: `Pay to ${username}`
}) })
// NIP-57: ensure zap support fields are present for wallet compatibility
if (!lnurlPayInfo.allowsNostr || !lnurlPayInfo.nostrPubkey) {
this.ctx.log('warn', `LNURL-pay response for ${username} missing zap fields (allowsNostr=${lnurlPayInfo.allowsNostr}, nostrPubkey=${!!lnurlPayInfo.nostrPubkey}). Zaps will not work.`)
}
return { return {
status: 200, status: 200,
body: lnurlPayInfo, body: lnurlPayInfo,

View file

@ -55,12 +55,10 @@ function generateId(): string {
} }
/** /**
* Validate username format per NIP-05 spec * Validate username format
* - Characters allowed: a-z, 0-9, hyphen (-), underscore (_), period (.) * - Lowercase alphanumeric and underscore only
* - Must start with a letter * - Must start with a letter
* - Must not end with a hyphen, underscore, or period
* - Length within bounds * - Length within bounds
* - Special case: "_" alone is the root identifier (_@domain)
*/ */
function validateUsername(username: string, config: Required<Nip05Config>): UsernameValidation { function validateUsername(username: string, config: Required<Nip05Config>): UsernameValidation {
if (!username) { if (!username) {
@ -69,11 +67,6 @@ function validateUsername(username: string, config: Required<Nip05Config>): User
const normalized = username.toLowerCase().trim() const normalized = username.toLowerCase().trim()
// Special case: root identifier "_" per NIP-05
if (normalized === '_') {
return { valid: true }
}
if (normalized.length < config.min_username_length) { if (normalized.length < config.min_username_length) {
return { valid: false, error: `Username must be at least ${config.min_username_length} character(s)` } return { valid: false, error: `Username must be at least ${config.min_username_length} character(s)` }
} }
@ -82,10 +75,9 @@ function validateUsername(username: string, config: Required<Nip05Config>): User
return { valid: false, error: `Username must be at most ${config.max_username_length} characters` } return { valid: false, error: `Username must be at most ${config.max_username_length} characters` }
} }
// NIP-05 spec: local-part MUST only use characters a-z0-9-_. // Only lowercase letters, numbers, and underscores
// Must start with a letter, must not end with separator if (!/^[a-z][a-z0-9_]*$/.test(normalized)) {
if (!/^[a-z][a-z0-9._-]*[a-z0-9]$/.test(normalized) && !/^[a-z]$/.test(normalized)) { return { valid: false, error: 'Username must start with a letter and contain only lowercase letters, numbers, and underscores' }
return { valid: false, error: 'Username must start with a letter, end with a letter or number, and contain only a-z, 0-9, hyphens, underscores, and periods' }
} }
// Check reserved usernames // Check reserved usernames

View file

@ -191,31 +191,6 @@ export interface ExtensionContext {
log(level: 'debug' | 'info' | 'warn' | 'error', message: string, ...args: any[]): void log(level: 'debug' | 'info' | 'warn' | 'error', message: string, ...args: any[]): void
} }
/**
* HTTP route handler types
* Used by extensions that expose HTTP endpoints (e.g. LNURL, .well-known)
*/
export interface HttpRequest {
method: string
path: string
params: Record<string, string>
query: Record<string, string>
headers: Record<string, string>
body?: any
}
export interface HttpResponse {
status: number
body: any
headers?: Record<string, string>
}
export interface HttpRoute {
method: 'GET' | 'POST'
path: string
handler: (req: HttpRequest) => Promise<HttpResponse>
}
/** /**
* Extension interface - what extensions must implement * Extension interface - what extensions must implement
*/ */
@ -242,12 +217,6 @@ export interface Extension {
* Return true if extension is healthy * Return true if extension is healthy
*/ */
healthCheck?(): Promise<boolean> healthCheck?(): Promise<boolean>
/**
* Get HTTP routes exposed by this extension
* The main HTTP server will mount these routes
*/
getHttpRoutes?(): HttpRoute[]
} }
/** /**