Compare commits

..

5 commits

Author SHA1 Message Date
Patrick Mulligan
17727d3e31 fix(nip05): add redirect prevention docs and zap field validation
Some checks are pending
Docker Compose Actions Workflow / test (push) Waiting to run
Gap #5: Document NIP-05 spec requirement that /.well-known/nostr.json
MUST NOT return HTTP redirects. The extension already complies (always
returns direct responses), but reverse proxy deployments need awareness.

Gap #7: Log a warning when getLnurlPayInfo() response is missing
allowsNostr or nostrPubkey fields required by NIP-57 for zap support.
This surfaces misconfiguration early instead of silently breaking zaps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:25:19 -04:00
Patrick Mulligan
53945d7dcc fix(nip05): allow hyphens and periods in usernames per NIP-05 spec
NIP-05 spec states local-part MUST only use characters a-z0-9-_.
The previous regex /^[a-z][a-z0-9_]*$/ rejected hyphens and periods.
Updated to /^[a-z][a-z0-9._-]*[a-z0-9]$/ and added support for the
root identifier "_" (_@domain) as described in the spec.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:25:06 -04:00
Patrick Mulligan
a7fb92e26d feat(nip05): add Lightning Address support for zaps
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:24:45 -04:00
Patrick Mulligan
8f38622395 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:24:45 -04:00
Patrick Mulligan
5cc7f3998c fix(extensions): add HTTP route types and getHttpRoutes to Extension interface
Some checks are pending
Docker Compose Actions Workflow / test (push) Waiting to run
HttpRoute, HttpRequest, and HttpResponse types were used by extensions
(withdraw, nip05) but not defined in the shared types.ts. Adds them
here so extensions import from the shared module instead of defining
locally. Also adds getHttpRoutes() as an optional method on the
Extension interface for extensions that expose HTTP endpoints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:24:22 -04:00
3 changed files with 55 additions and 5 deletions

View file

@ -189,6 +189,12 @@ export default class Nip05Extension implements Extension {
* "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> {
try {
// Get application ID from request context
@ -272,6 +278,11 @@ export default class Nip05Extension implements Extension {
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 {
status: 200,
body: lnurlPayInfo,

View file

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

View file

@ -191,6 +191,31 @@ export interface ExtensionContext {
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
*/
@ -217,6 +242,12 @@ export interface Extension {
* Return true if extension is healthy
*/
healthCheck?(): Promise<boolean>
/**
* Get HTTP routes exposed by this extension
* The main HTTP server will mount these routes
*/
getHttpRoutes?(): HttpRoute[]
}
/**