forked from coracle/caravel
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c75ac9ed5 |
+17
-1
@@ -60,7 +60,13 @@ See [spec](spec) for more details
|
|||||||
|
|
||||||
## API Routes
|
## API Routes
|
||||||
|
|
||||||
All routes are NIP-98 protected.
|
Most API routes are NIP-98 protected.
|
||||||
|
|
||||||
|
Public exceptions:
|
||||||
|
|
||||||
|
- `GET /plans`
|
||||||
|
- `GET /plans/:id`
|
||||||
|
- `POST /stripe/webhook` (validated with Stripe signatures instead)
|
||||||
|
|
||||||
- `GET /identity` — get auth identity (`pubkey`, `is_admin`)
|
- `GET /identity` — get auth identity (`pubkey`, `is_admin`)
|
||||||
- `GET /tenants` — list tenants (admin)
|
- `GET /tenants` — list tenants (admin)
|
||||||
@@ -73,3 +79,13 @@ All routes are NIP-98 protected.
|
|||||||
- `PUT /relays/:id` — update relay (admin or relay tenant)
|
- `PUT /relays/:id` — update relay (admin or relay tenant)
|
||||||
- `POST /relays/:id/deactivate` — deactivate relay (admin or relay tenant)
|
- `POST /relays/:id/deactivate` — deactivate relay (admin or relay tenant)
|
||||||
- `GET /invoices` — list invoices (`?tenant=<pubkey>` allowed for admin only)
|
- `GET /invoices` — list invoices (`?tenant=<pubkey>` allowed for admin only)
|
||||||
|
|
||||||
|
## API Auth Model
|
||||||
|
|
||||||
|
Caravel intentionally uses a session-style variant of NIP-98 for client-to-backend API auth.
|
||||||
|
|
||||||
|
- Frontend signs one kind `27235` event with `u = VITE_API_URL` and caches that header for about 10 minutes.
|
||||||
|
- Backend verifies event kind, signature, and that `u` contains configured `HOST`.
|
||||||
|
- Backend intentionally does not bind auth to exact request URL/method/query, and does not enforce payload hash, timestamp freshness window, or replay cache.
|
||||||
|
- Goal: reduce repeated wallet signing prompts and avoid cookie-based sessions.
|
||||||
|
- Tradeoff: this is weaker request-intent binding than strict NIP-98 semantics.
|
||||||
|
|||||||
+5
-3
@@ -184,9 +184,11 @@ Notes:
|
|||||||
## `extract_auth_pubkey(&self, headers: &HeaderMap) -> Result<String>`
|
## `extract_auth_pubkey(&self, headers: &HeaderMap) -> Result<String>`
|
||||||
|
|
||||||
- Parses `Authorization` header
|
- Parses `Authorization` header
|
||||||
- Validates event kind and signature using `nostr_sdk`
|
- Validates event kind (`27235`) and signature using `nostr_sdk`
|
||||||
- Validates event `u` against `HOST` (not the request path. Non-standard, but correct)
|
- Validates event `u` contains configured `HOST`
|
||||||
- Does not validate `method` tag
|
- Intentionally does **not** enforce exact request URL/method/query matching
|
||||||
|
- Intentionally does **not** validate `payload` tag/hash, `created_at` freshness window, or replay nonce/cache
|
||||||
|
- This is a deliberate session-style tradeoff to reduce repeated signer prompts in the client
|
||||||
- Returns pubkey if header all checks pass
|
- Returns pubkey if header all checks pass
|
||||||
|
|
||||||
Refer to https://github.com/nostr-protocol/nips/blob/master/98.md for details. Use `nostr_sdk` functionality where possible.
|
Refer to https://github.com/nostr-protocol/nips/blob/master/98.md for details. Use `nostr_sdk` functionality where possible.
|
||||||
|
|||||||
@@ -209,6 +209,9 @@ impl Api {
|
|||||||
return Err(ApiError::Unauthorized(anyhow!("missing u tag")));
|
return Err(ApiError::Unauthorized(anyhow!("missing u tag")));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Intentional session-style variant of NIP-98 for Caravel API auth.
|
||||||
|
// We validate signer identity plus host affinity, and do not bind to exact
|
||||||
|
// request URL/method or maintain replay state here.
|
||||||
if !self.host.is_empty() && !got_u.contains(&self.host) {
|
if !self.host.is_empty() && !got_u.contains(&self.host) {
|
||||||
return Err(ApiError::Unauthorized(anyhow!(
|
return Err(ApiError::Unauthorized(anyhow!(
|
||||||
"authorization host mismatch"
|
"authorization host mismatch"
|
||||||
|
|||||||
+5
-2
@@ -51,8 +51,11 @@ npm run preview
|
|||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
- Tenant requests use NIP-98 tokens derived from the logged-in user
|
- Tenant requests use an intentional session-style variant of NIP-98:
|
||||||
- Admin routes require a pubkey listed in `PLATFORM_ADMIN_PUBKEYS` on the backend
|
- The client signs one kind `27235` event with `u = VITE_API_URL`.
|
||||||
|
- The resulting `Authorization` header is cached for about 10 minutes to avoid repeated signer prompts.
|
||||||
|
- The backend validates signer identity + host affinity rather than exact URL/method binding per request.
|
||||||
|
- Admin routes require a pubkey listed in `ADMINS` on the backend.
|
||||||
|
|
||||||
## Routes
|
## Routes
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,8 @@ export async function makeAuth(): Promise<string | undefined> {
|
|||||||
kind: 27235,
|
kind: 27235,
|
||||||
content: "",
|
content: "",
|
||||||
created_at: Math.floor(now / 1000),
|
created_at: Math.floor(now / 1000),
|
||||||
|
// Intentional session-style auth: sign the API base URL once, then reuse
|
||||||
|
// the header briefly to avoid prompting the signer on every request.
|
||||||
tags: [["u", API_URL]],
|
tags: [["u", API_URL]],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user