first commit

This commit is contained in:
2026-04-29 02:35:00 +00:00
commit 2cb17df4c5
90 changed files with 7321 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
package docs
import (
_ "embed"
"encoding/json"
"net/http"
"gopkg.in/yaml.v3"
)
//go:embed openapi.yaml
var openapiYAML []byte
var openapiJSON []byte
func init() {
var raw any
if err := yaml.Unmarshal(openapiYAML, &raw); err != nil {
panic(err)
}
clean := convertMaps(raw)
b, err := json.Marshal(clean)
if err != nil {
panic(err)
}
openapiJSON = b
}
// convertMaps recursively converts map[interface{}]interface{} to map[string]interface{}.
func convertMaps(in any) any {
switch v := in.(type) {
case map[any]any:
m := make(map[string]any, len(v))
for k, val := range v {
m[toString(k)] = convertMaps(val)
}
return m
case map[string]any:
m := make(map[string]any, len(v))
for k, val := range v {
m[k] = convertMaps(val)
}
return m
case []any:
out := make([]any, len(v))
for i, item := range v {
out[i] = convertMaps(item)
}
return out
default:
return v
}
}
func toString(v any) string {
if s, ok := v.(string); ok {
return s
}
return ""
}
func ServeJSON(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "public, max-age=300")
_, _ = w.Write(openapiJSON)
}
const swaggerHTML = `<!DOCTYPE html>
<html>
<head>
<title>NIP-05 API</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: "/openapi.json",
dom_id: "#swagger-ui",
deepLinking: true,
});
};
</script>
</body>
</html>`
func ServeUI(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(swaggerHTML))
}

View File

@@ -0,0 +1,297 @@
openapi: 3.1.0
info:
title: NIP-05 API
description: Single-domain NIP-05 identity service with Lightning-paid registration.
version: 1.0.0
servers:
- url: /
tags:
- name: Public
description: Anonymous, infrastructure-level endpoints. Cacheable, no auth.
- name: User
description: Anonymous flows for end users — lookup, availability, payment.
- name: Admin
description: Privileged operations. Require an `X-API-Key` header.
components:
securitySchemes:
AdminAPIKey:
type: apiKey
in: header
name: X-API-Key
schemas:
Error:
type: object
properties:
error: { type: string }
detail: { type: string }
Pricing:
type: object
properties:
yearly_sats: { type: integer }
lifetime_sats: { type: integer }
lightning_enabled: { type: boolean }
Invoice:
type: object
properties:
payment_hash: { type: string }
payment_request: { type: string }
amount_sats: { type: integer }
expires_at: { type: string, format: date-time }
username: { type: string }
is_renewal: { type: boolean }
InvoiceStatus:
type: object
properties:
payment_hash: { type: string }
status: { type: string, enum: [pending, paid, expired] }
username: { type: string }
User:
type: object
properties:
pubkey: { type: string }
npub: { type: string }
username: { type: string }
subscription_type: { type: string, enum: [yearly, lifetime] }
is_active: { type: boolean }
expires_at: { type: string, format: date-time, nullable: true }
deactivated_at: { type: string, format: date-time, nullable: true }
UserLookup:
type: object
properties:
pubkey: { type: string }
npub: { type: string }
is_whitelisted: { type: boolean }
username: { type: string }
expires_at: { type: string, format: date-time, nullable: true }
in_grace: { type: boolean }
reserved_username: { type: string }
expired_at: { type: string, format: date-time }
paths:
/.well-known/nostr.json:
get:
tags: [Public]
summary: NIP-05 lookup
parameters:
- in: query
name: name
schema: { type: string }
responses:
'200':
description: NIP-05 names map
content:
application/json:
schema:
type: object
properties:
names:
type: object
additionalProperties: { type: string }
relays:
type: object
additionalProperties:
type: array
items: { type: string }
/healthz:
get:
tags: [Public]
summary: Health check
responses:
'200': { description: OK }
'503': { description: Down }
/v1/pricing:
get:
tags: [Public]
summary: Pricing info
responses:
'200':
description: Pricing
content:
application/json:
schema: { $ref: '#/components/schemas/Pricing' }
/v1/invoices:
post:
tags: [User]
summary: Create payment invoice
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [pubkey]
properties:
pubkey: { type: string, description: "Hex pubkey or npub" }
username:
type: string
description: "Optional. Auto-generated from the pubkey if omitted."
subscription_type:
type: string
enum: [yearly, lifetime]
description: "Optional. Defaults to lifetime."
years:
type: integer
minimum: 1
description: "Optional. Defaults to 1 when subscription_type is yearly; ignored for lifetime."
responses:
'200':
description: Invoice
content:
application/json:
schema: { $ref: '#/components/schemas/Invoice' }
'400': { description: Validation error, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
'403': { description: Forbidden — user already has lifetime access, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
'409': { description: Conflict — username unavailable or pending invoice already exists, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
'503': { description: Lightning unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
/v1/invoices/{payment_hash}:
get:
tags: [User]
summary: Invoice status
parameters:
- in: path
name: payment_hash
required: true
schema: { type: string }
responses:
'200':
description: Status
content:
application/json:
schema: { $ref: '#/components/schemas/InvoiceStatus' }
'404': { description: Not found }
/v1/users/{pubkey}:
get:
tags: [User]
summary: Lookup user by pubkey (npub or hex)
parameters:
- in: path
name: pubkey
required: true
schema: { type: string }
responses:
'200':
description: User lookup
content:
application/json:
schema: { $ref: '#/components/schemas/UserLookup' }
'404': { description: Never registered }
/v1/usernames/{name}/available:
get:
tags: [User]
summary: Username availability
parameters:
- in: path
name: name
required: true
schema: { type: string }
responses:
'200':
description: Availability
content:
application/json:
schema:
type: object
properties:
username: { type: string }
available: { type: boolean }
/v1/admin/users:
post:
tags: [Admin]
summary: Add user (admin)
security: [{ AdminAPIKey: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [pubkey, username, subscription_type]
properties:
pubkey: { type: string }
username: { type: string }
subscription_type: { type: string, enum: [yearly, lifetime] }
years: { type: integer }
responses:
'201':
description: Created
content: { application/json: { schema: { $ref: '#/components/schemas/User' } } }
'401': { description: Unauthorized }
get:
tags: [Admin]
summary: List users (admin)
security: [{ AdminAPIKey: [] }]
parameters:
- in: query
name: active
schema: { type: boolean }
- in: query
name: q
schema: { type: string }
responses:
'200':
description: User list
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/User' }
'401': { description: Unauthorized }
/v1/admin/users/{pubkey}:
put:
tags: [Admin]
summary: Update username (admin)
security: [{ AdminAPIKey: [] }]
parameters:
- in: path
name: pubkey
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [username]
properties:
username: { type: string }
responses:
'200': { description: Updated }
'401': { description: Unauthorized }
'404': { description: Not found }
'409': { description: Conflict }
delete:
tags: [Admin]
summary: Delete user (admin)
security: [{ AdminAPIKey: [] }]
parameters:
- in: path
name: pubkey
required: true
schema: { type: string }
responses:
'200': { description: Deleted }
'401': { description: Unauthorized }
'404': { description: Not found }
/v1/admin/users/{pubkey}/extend:
post:
tags: [Admin]
summary: Extend subscription (admin)
security: [{ AdminAPIKey: [] }]
parameters:
- in: path
name: pubkey
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
years: { type: integer }
subscription_type: { type: string, enum: [yearly, lifetime] }
responses:
'200': { description: Extended }
'401': { description: Unauthorized }
'404': { description: Not found }