Compare commits

..

1 Commits

Author SHA1 Message Date
userAdityaa b82d6de80e chore: replace placeholder letter badges with actual SVG logos 2026-04-17 22:53:38 +05:45
16 changed files with 55 additions and 166 deletions
+2 -2
View File
@@ -53,7 +53,7 @@ The rest of the defaults work as-is. `ROBOT_*`, `LIVEKIT_*`, billing, and Stripe
cp frontend/.env.template frontend/.env
```
The defaults (`VITE_API_URL=http://127.0.0.1:2892`) point at the backend and work out of the box.
The defaults (`VITE_API_URL=http://127.0.0.1:3000`) point at the backend and work out of the box.
### 4. Install dependencies and run
@@ -62,7 +62,7 @@ cd frontend && bun install && cd ..
just dev
```
This starts the backend (with auto-reload on file changes) at `http://127.0.0.1:2892` and the frontend at `http://127.0.0.1:5173`.
This starts the backend (with auto-reload on file changes) at `http://127.0.0.1:3000` and the frontend at `http://127.0.0.1:5173`.
## Project docs
+3 -3
View File
@@ -1,6 +1,6 @@
# Server
HOST=127.0.0.1
PORT=2892
PORT=3000
ALLOW_ORIGINS= # Optional comma-separated allowed CORS origins; empty = permissive
# Auth
@@ -28,5 +28,5 @@ LIVEKIT_API_SECRET=
# Billing
NWC_URL= # Nostr Wallet Connect URL for generating Lightning invoices
STRIPE_SECRET_KEY= # Required Stripe API secret key (sk_...)
STRIPE_WEBHOOK_SECRET=whsec_test_00000000000000000000000000 # Webhook signing secret (use real value in production)
STRIPE_SECRET_KEY= # Stripe API secret key (sk_...)
STRIPE_WEBHOOK_SECRET= # Secret for verifying Stripe webhook signatures (whsec_...)
+1 -1
View File
@@ -26,6 +26,6 @@ WORKDIR /app
COPY --from=build /app/target/release/backend /app/backend
EXPOSE 2892
EXPOSE 3000
CMD ["/app/backend"]
+22 -24
View File
@@ -30,29 +30,27 @@ backend/
Environment variables:
| Variable | Description | Default |
| ------------------------ | ----------------------------------------------------------------------- | ------------------------------------ |
| `DATABASE_URL` | SQLite URL. Relative paths are resolved under `backend/`. | `sqlite://<backend>/data/caravel.db` |
| `HOST` | API bind host (also used for NIP-98 `u` host check) | `127.0.0.1` |
| `PORT` | API bind port | `2892` |
| `ADMINS` | Comma-separated admin pubkeys (hex) | _optional_ |
| `ALLOW_ORIGINS` | Comma-separated CORS origins. If empty, CORS is permissive. | _optional_ |
| `ZOOID_API_URL` | Zooid API base URL used by infra worker | _required for infra sync_ |
| `ZOOID_API_SECRET` | Nostr secret key used for authentication of requests to the zooid API | _required_ |
| `RELAY_DOMAIN` | Base domain appended to relay subdomains | empty |
| `LIVEKIT_URL` | LiveKit URL sent to zooid when relay livekit is enabled | _optional_ |
| `LIVEKIT_API_KEY` | LiveKit API key sent to zooid | _optional_ |
| `LIVEKIT_API_SECRET` | LiveKit API secret sent to zooid | _optional_ |
| `NWC_URL` | Platform NWC URL used to generate BOLT11 invoices | _required for invoice generation_ |
| `STRIPE_SECRET_KEY` | Stripe API secret key used for billing API operations | _required_ |
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret used to verify `Stripe-Signature` headers | _required_ |
| `ROBOT_SECRET` | Robot Nostr secret key | _required_ |
| `ROBOT_NAME` | Robot display name (kind `0`) | _optional_ |
| `ROBOT_DESCRIPTION` | Robot description (kind `0`) | _optional_ |
| `ROBOT_PICTURE` | Robot picture URL (kind `0`) | _optional_ |
| `ROBOT_OUTBOX_RELAYS` | Comma-separated relays published as kind `10002` | _required_ |
| `ROBOT_INDEXER_RELAYS` | Comma-separated relays used for recipient relay discovery | _required_ |
| `ROBOT_MESSAGING_RELAYS` | Comma-separated relays published as kind `10050` | _required_ |
| Variable | Description | Default |
|---|---|---|
| `DATABASE_URL` | SQLite URL. Relative paths are resolved under `backend/`. | `sqlite://<backend>/data/caravel.db` |
| `HOST` | API bind host (also used for NIP-98 `u` host check) | `127.0.0.1` |
| `PORT` | API bind port | `3000` |
| `ADMINS` | Comma-separated admin pubkeys (hex) | _optional_ |
| `ALLOW_ORIGINS` | Comma-separated CORS origins. If empty, CORS is permissive. | _optional_ |
| `ZOOID_API_URL` | Zooid API base URL used by infra worker | _required for infra sync_ |
| `ZOOID_API_SECRET` | Nostr secret key used for authentication of requests to the zooid API | _required_ |
| `RELAY_DOMAIN` | Base domain appended to relay subdomains | empty |
| `LIVEKIT_URL` | LiveKit URL sent to zooid when relay livekit is enabled | _optional_ |
| `LIVEKIT_API_KEY` | LiveKit API key sent to zooid | _optional_ |
| `LIVEKIT_API_SECRET` | LiveKit API secret sent to zooid | _optional_ |
| `NWC_URL` | Platform NWC URL used to generate BOLT11 invoices | _required for invoice generation_ |
| `ROBOT_SECRET` | Robot Nostr secret key | _required_ |
| `ROBOT_NAME` | Robot display name (kind `0`) | _optional_ |
| `ROBOT_DESCRIPTION` | Robot description (kind `0`) | _optional_ |
| `ROBOT_PICTURE` | Robot picture URL (kind `0`) | _optional_ |
| `ROBOT_OUTBOX_RELAYS` | Comma-separated relays published as kind `10002` | _required_ |
| `ROBOT_INDEXER_RELAYS` | Comma-separated relays used for recipient relay discovery | _required_ |
| `ROBOT_MESSAGING_RELAYS` | Comma-separated relays published as kind `10050` | _required_ |
Relay list env vars are comma-separated and trimmed. If a relay has no `ws://` or `wss://` scheme, `wss://` is prepended.
@@ -68,7 +66,7 @@ Public exceptions:
- `GET /plans`
- `GET /plans/:id`
- `POST /stripe/webhook` (validated with Stripe signatures)
- `POST /stripe/webhook` (validated with Stripe signatures instead)
- `GET /identity` — get auth identity (`pubkey`, `is_admin`)
- `GET /tenants` — list tenants (admin)
-1
View File
@@ -178,7 +178,6 @@ Notes:
- Reads raw request body and `Stripe-Signature` header
- Calls `billing.handle_webhook(payload, signature)`
- Returns `200` on success, `400` on signature verification failure
- Startup requires non-empty `STRIPE_WEBHOOK_SECRET`
--- Utilities
+1 -3
View File
@@ -5,7 +5,6 @@ Billing encapsulates logic related to synchronizing state with Stripe, processin
Members:
- `nwc_url: String` - a nostr wallet connect URL used to **create** bolt11 invoices (i.e. receive payments), from `NWC_URL`
- `stripe_secret_key: String` - Stripe API key used for billing API operations, from `STRIPE_SECRET_KEY`
- `stripe_webhook_secret: String` - secret for verifying Stripe webhook signatures, from `STRIPE_WEBHOOK_SECRET`
- `query: Query`
- `command: Command`
@@ -14,8 +13,6 @@ Members:
## `pub fn new(query: Query, command: Command, robot: Robot) -> Self`
- Reads environment and populates members
- Panics if `STRIPE_SECRET_KEY` is missing/empty
- Panics if `STRIPE_WEBHOOK_SECRET` is missing/empty
## `pub fn start(&self)`
@@ -112,3 +109,4 @@ Skip invoices with `amount_due` of 0.
- Look up tenant by `stripe_customer_id`
- Clear `stripe_subscription_id` via `command.clear_tenant_subscription`
+3 -4
View File
@@ -139,7 +139,7 @@ impl Api {
api: Arc::new(self),
};
let router = Router::new()
Router::new()
.route("/identity", get(get_identity))
.route("/plans", get(list_plans))
.route("/plans/:id", get(get_plan))
@@ -158,9 +158,8 @@ impl Api {
"/tenants/:pubkey/stripe/session",
get(create_stripe_session),
)
.route("/stripe/webhook", post(stripe_webhook));
router.with_state(state)
.route("/stripe/webhook", post(stripe_webhook))
.with_state(state)
}
fn extract_auth_pubkey(&self, headers: &HeaderMap) -> std::result::Result<String, ApiError> {
+6 -105
View File
@@ -95,9 +95,6 @@ impl Billing {
panic!("missing STRIPE_SECRET_KEY environment variable");
}
let stripe_webhook_secret = std::env::var("STRIPE_WEBHOOK_SECRET").unwrap_or_default();
if stripe_webhook_secret.trim().is_empty() {
panic!("missing STRIPE_WEBHOOK_SECRET environment variable");
}
let btc_quote_api_base =
std::env::var("BTC_PRICE_API_BASE").unwrap_or_else(|_| COINBASE_SPOT_API.to_string());
Self {
@@ -952,8 +949,7 @@ mod tests {
use sqlx::SqlitePool;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use std::str::FromStr;
use std::sync::OnceLock;
use tokio::sync::Mutex;
use std::sync::{Mutex, OnceLock};
fn env_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
@@ -968,14 +964,6 @@ mod tests {
}
}
#[allow(unused_unsafe)]
fn set_stripe_webhook_secret(value: Option<&str>) {
match value {
Some(v) => unsafe { std::env::set_var("STRIPE_WEBHOOK_SECRET", v) },
None => unsafe { std::env::remove_var("STRIPE_WEBHOOK_SECRET") },
}
}
struct StripeSecretKeyGuard {
previous: Option<String>,
}
@@ -994,24 +982,6 @@ mod tests {
}
}
struct StripeWebhookSecretGuard {
previous: Option<String>,
}
impl StripeWebhookSecretGuard {
fn set(value: Option<&str>) -> Self {
let previous = std::env::var("STRIPE_WEBHOOK_SECRET").ok();
set_stripe_webhook_secret(value);
Self { previous }
}
}
impl Drop for StripeWebhookSecretGuard {
fn drop(&mut self) {
set_stripe_webhook_secret(self.previous.as_deref());
}
}
async fn test_pool() -> SqlitePool {
let connect_options = SqliteConnectOptions::from_str("sqlite::memory:")
.expect("valid sqlite memory url")
@@ -1033,9 +1003,8 @@ mod tests {
#[tokio::test]
async fn billing_new_panics_without_stripe_secret_key() {
let _lock = env_lock().lock().await;
let _secret_env = StripeSecretKeyGuard::set(None);
let _webhook_env = StripeWebhookSecretGuard::set(Some("whsec_test_dummy"));
let _lock = env_lock().lock().expect("acquire env lock");
let _env = StripeSecretKeyGuard::set(None);
let pool = test_pool().await;
let query = Query::new(pool.clone());
@@ -1065,76 +1034,9 @@ mod tests {
}
#[tokio::test]
async fn billing_new_panics_without_stripe_webhook_secret() {
let _lock = env_lock().lock().await;
let _secret_env = StripeSecretKeyGuard::set(Some("sk_test_dummy"));
let _webhook_env = StripeWebhookSecretGuard::set(None);
let pool = test_pool().await;
let query = Query::new(pool.clone());
let command = Command::new(pool);
let robot = Robot::test_stub();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
Billing::new(query, command, robot)
}));
let panic_payload = match result {
Ok(_) => panic!("constructor should panic when STRIPE_WEBHOOK_SECRET is missing"),
Err(payload) => payload,
};
let panic_msg = if let Some(msg) = panic_payload.downcast_ref::<&str>() {
(*msg).to_string()
} else if let Some(msg) = panic_payload.downcast_ref::<String>() {
msg.clone()
} else {
String::new()
};
assert!(
panic_msg.contains("missing STRIPE_WEBHOOK_SECRET environment variable"),
"unexpected panic: {panic_msg}"
);
}
#[tokio::test]
async fn billing_new_panics_with_blank_stripe_webhook_secret() {
let _lock = env_lock().lock().await;
let _secret_env = StripeSecretKeyGuard::set(Some("sk_test_dummy"));
let _webhook_env = StripeWebhookSecretGuard::set(Some(" "));
let pool = test_pool().await;
let query = Query::new(pool.clone());
let command = Command::new(pool);
let robot = Robot::test_stub();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
Billing::new(query, command, robot)
}));
let panic_payload = match result {
Ok(_) => panic!("constructor should panic when STRIPE_WEBHOOK_SECRET is blank"),
Err(payload) => payload,
};
let panic_msg = if let Some(msg) = panic_payload.downcast_ref::<&str>() {
(*msg).to_string()
} else if let Some(msg) = panic_payload.downcast_ref::<String>() {
msg.clone()
} else {
String::new()
};
assert!(
panic_msg.contains("missing STRIPE_WEBHOOK_SECRET environment variable"),
"unexpected panic: {panic_msg}"
);
}
#[tokio::test]
async fn billing_new_accepts_non_empty_stripe_secrets() {
let _lock = env_lock().lock().await;
let _secret_env = StripeSecretKeyGuard::set(Some("sk_test_dummy"));
let _webhook_env = StripeWebhookSecretGuard::set(Some("whsec_test_dummy"));
async fn billing_new_accepts_non_empty_stripe_secret_key() {
let _lock = env_lock().lock().expect("acquire env lock");
let _env = StripeSecretKeyGuard::set(Some("sk_test_dummy"));
let pool = test_pool().await;
let billing = Billing::new(
@@ -1144,6 +1046,5 @@ mod tests {
);
assert_eq!(billing.stripe_secret_key, "sk_test_dummy");
assert_eq!(billing.stripe_webhook_secret, "whsec_test_dummy");
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ impl Command {
.fetch_one(&mut **tx)
.await?
}
_ => anyhow::bail!("unknown resource_type: {resource_type}"),
_ => anyhow::bail!("unknown resource_type: {}", resource_type),
};
let id = uuid::Uuid::new_v4().to_string();
+1 -1
View File
@@ -169,7 +169,7 @@ impl Infra {
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
anyhow::bail!("zooid sync returned {status}: {body}")
anyhow::bail!("zooid sync returned {}: {}", status, body)
}
Ok(())
}
+2 -2
View File
@@ -3,8 +3,8 @@ mod billing;
mod command;
mod infra;
mod models;
mod pool;
mod query;
mod pool;
mod robot;
use anyhow::Result;
@@ -40,7 +40,7 @@ async fn main() -> Result<()> {
let port: u16 = std::env::var("PORT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(2892);
.unwrap_or(3000);
let origins: Vec<String> = std::env::var("ALLOW_ORIGINS")
.unwrap_or_default()
.split(',')
+2 -1
View File
@@ -21,7 +21,8 @@ pub async fn create_pool() -> Result<SqlitePool> {
std::fs::create_dir_all(parent)?;
}
let connect_options = SqliteConnectOptions::from_str(&database_url)?.create_if_missing(true);
let connect_options =
SqliteConnectOptions::from_str(&database_url)?.create_if_missing(true);
let pool = SqlitePoolOptions::new()
.max_connections(5)
+2 -2
View File
@@ -25,7 +25,7 @@ async fn quote_endpoint_can_be_stubbed_deterministically() {
assert_eq!(btc_price, 50_000.0);
let msats =
fiat_minor_to_msats_from_quote(100, "USD", btc_price).expect("convert quoted fiat amount");
let msats = fiat_minor_to_msats_from_quote(100, "USD", btc_price)
.expect("convert quoted fiat amount");
assert_eq!(msats, 2_000_000);
}
+1 -1
View File
@@ -1,5 +1,5 @@
# Backend API base URL
VITE_API_URL=http://127.0.0.1:2892
VITE_API_URL=http://127.0.0.1:3000
# Platform display name shown in UI
VITE_PLATFORM_NAME=Caravel
+1 -1
View File
@@ -32,7 +32,7 @@ Environment variables (see `.env.template`):
| Variable | Description | Default |
|---|---|---|
| `VITE_API_URL` | Backend API base URL | `http://127.0.0.1:2892` |
| `VITE_API_URL` | Backend API base URL | `http://127.0.0.1:3000` |
## Running
+7 -14
View File
@@ -1,20 +1,13 @@
import { defineConfig, loadEnv } from 'vite'
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
import tailwindcss from '@tailwindcss/vite'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig(({mode}) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [tailwindcss(), solid()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
export default defineConfig({
plugins: [tailwindcss(), solid()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
server: {
port: Number(env.VITE_PORT) || 5173,
},
}
},
})