mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
feat(auth): npub-native registration (POST /auth/nostr/register) - Nostr-native identity
Make npub the identity anchor: a wallet can register from its seed-derived Nostr key alone, no BTC signature. Breaks the link chicken-and-egg (nostr_login is create-never; nostr_link needs a prior BTC-authed JWT). Additive - /auth/extension (BTC) and the link flow keep working for legacy backends during the transition. - get_or_create_user_by_nostr_pubkey (db twin of the BTC path): keys/inserts on nostr_pubkey (raw, never peppered), pubkey_hash NULL. THE load-bearing dedup: looks up by seed_fingerprint and MERGES the npub onto an existing (BTC-anchored or link-less) row rather than splitting one wallet into two identities; a different npub already on that row fails closed (409). backfill_optionals shared helper. - POST /auth/nostr/register + GET /auth/nostr/register-challenge: verify a NIP-98 signed-action over a subject-less single-use nonce (replay-safe), then re-run the abuse gates keyed on the npub (a create path must not bypass PoW/invite/ payment); pay-to-register invoices still bind to the BTC key carried in keys[]. Empty-body descriptor contract (mirrors /auth/nostr/link); binding the key list into the proof is a documented hardening follow-up. - Housekeeping: get_user_count counts npub-only users; record_login_event labels the nostr path. 3 integration KATs (create-by-npub + idempotent, replay rejected, seed_fingerprint MERGE dedup) pass against a real DB; 211 lib tests pass; openapi regenerated.
This commit is contained in:
+134
@@ -269,6 +269,65 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/auth/nostr/register": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "npub-native registration. Verifies a NIP-98 signed-action over a server nonce,\nre-runs the abuse gates keyed on the npub (a create path must not bypass the\nPoW/invite/payment gates that `/auth/extension` enforces), then get-or-creates\nby npub with a `seed_fingerprint` MERGE so a wallet that already has a row\n(BTC-anchored, or previously linked) is never split into two identities.",
|
||||
"description": "SECURITY NOTE: the proof binds an EMPTY-body descriptor (the same contract as\n`/auth/nostr/link`), so replay is prevented by the single-use nonce and npub\ncontrol is proven, but the chain `keys` are TLS-bound rather than\nproof-bound. Binding the key list into the signed payload is a documented\nhardening follow-up (defends a TLS-breaking active MITM swapping the keys).",
|
||||
"operationId": "nostr_register",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NostrRegisterRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Wallet registered or authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AuthResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid proof or nonce"
|
||||
},
|
||||
"409": {
|
||||
"description": "Seed already registered under a different Nostr identity"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/nostr/register-challenge": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Issue a single-use nonce for npub-native registration. UNAUTHENTICATED: no\nuser exists yet, so (unlike the link challenge) the nonce is subject-less; the\nregister signed-action binds it purely as replay protection.",
|
||||
"operationId": "nostr_register_challenge",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Single-use register nonce",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NostrLinkChallengeResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/payment-invoice": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -3088,6 +3147,81 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"NostrRegisterRequest": {
|
||||
"type": "object",
|
||||
"description": "Register (or resolve) a wallet keyed by its **Nostr identity**: the\nself-sovereign, npub-native create path. Unlike [`nostr_login`] (create-never)\nand [`nostr_link`] (needs a prior BTC-authed JWT), this MINTS a user from the\nnpub alone: no BTC signature is ever required. The npub is the identity anchor;\nthe chain `keys` still ship (for tip addresses + restore) but are NOT the auth\nproof.",
|
||||
"required": [
|
||||
"nostr_token",
|
||||
"nonce",
|
||||
"keys"
|
||||
],
|
||||
"properties": {
|
||||
"altcha_solution": {
|
||||
"type": "object"
|
||||
},
|
||||
"invite_code": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"keys": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetPublicKey"
|
||||
},
|
||||
"description": "Chain public keys (for tip addresses + restore). A `btc` key is expected\nfor pay-to-register invoice binding, but it is no longer the identity."
|
||||
},
|
||||
"nonce": {
|
||||
"type": "string",
|
||||
"description": "The server-issued single-use nonce (from `/auth/nostr/register-challenge`)."
|
||||
},
|
||||
"nostr_token": {
|
||||
"type": "string",
|
||||
"description": "`Nostr <base64(event)>` signed-action token proving control of the npub AND\nbinding the server nonce + the `nostr_register` purpose."
|
||||
},
|
||||
"payment_invoice_id": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"seed_fingerprint": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"username": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"wallet_birthday": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int64",
|
||||
"description": "Wallet creation time (unix seconds), to bound chain scans."
|
||||
},
|
||||
"wow_start_height": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int64"
|
||||
},
|
||||
"xmr_start_height": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"OkResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
Reference in New Issue
Block a user