Compare commits
5 commits
a2366c40f2
...
17727d3e31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17727d3e31 | ||
|
|
53945d7dcc | ||
|
|
a7fb92e26d | ||
|
|
8f38622395 | ||
|
|
5cc7f3998c |
3 changed files with 55 additions and 5 deletions
|
|
@ -189,6 +189,12 @@ 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. HTTP→HTTPS 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
|
||||||
|
|
@ -272,6 +278,11 @@ 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,
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,12 @@ function generateId(): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate username format
|
* Validate username format per NIP-05 spec
|
||||||
* - Lowercase alphanumeric and underscore only
|
* - Characters allowed: a-z, 0-9, hyphen (-), underscore (_), period (.)
|
||||||
* - 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) {
|
||||||
|
|
@ -67,6 +69,11 @@ 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)` }
|
||||||
}
|
}
|
||||||
|
|
@ -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` }
|
return { valid: false, error: `Username must be at most ${config.max_username_length} characters` }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only lowercase letters, numbers, and underscores
|
// NIP-05 spec: local-part MUST only use characters a-z0-9-_.
|
||||||
if (!/^[a-z][a-z0-9_]*$/.test(normalized)) {
|
// Must start with a letter, must not end with separator
|
||||||
return { valid: false, error: 'Username must start with a letter and contain only lowercase letters, numbers, and underscores' }
|
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
|
// Check reserved usernames
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,31 @@ 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
|
||||||
*/
|
*/
|
||||||
|
|
@ -217,6 +242,12 @@ 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[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue