1#![allow(dead_code)]
15
16mod admin;
17mod admins;
18mod auth;
19mod crypto;
20mod delegation;
21mod did;
22mod governance_api;
23mod http;
24mod invites;
25mod moderation;
26mod pod;
27mod schema;
28mod username;
29mod webauthn;
30mod welcome;
31mod wot;
32
33use worker::*;
34
35fn cors_headers(env: &Env) -> Headers {
37 let origin = env
38 .var("EXPECTED_ORIGIN")
39 .map(|v| v.to_string())
40 .unwrap_or_else(|_| "https://example.com".to_string());
41
42 let headers = Headers::new();
43 headers.set("Access-Control-Allow-Origin", &origin).ok();
44 headers
45 .set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
46 .ok();
47 headers
48 .set(
49 "Access-Control-Allow-Headers",
50 "Content-Type, Authorization",
51 )
52 .ok();
53 headers.set("Access-Control-Max-Age", "86400").ok();
54 headers
55}
56
57fn error_response(env: &Env, message: &str, status: u16) -> Response {
64 let body = format!(r#"{{"error":"{}"}}"#, message.replace('"', r#"\""#));
65 let cors = cors_headers(env);
66 match Response::ok(&body) {
67 Ok(resp) => {
68 let resp = resp.with_status(status).with_headers(cors);
69 resp.headers().set("Content-Type", "application/json").ok();
70 resp
71 }
72 Err(_) => {
73 Response::error("internal", 500).expect("static error response")
77 }
78 }
79}
80
81fn json_response(env: &Env, body: &serde_json::Value, status: u16) -> Result<Response> {
83 let json_str = serde_json::to_string(body).map_err(|e| Error::RustError(e.to_string()))?;
84 let cors = cors_headers(env);
85 let resp = Response::ok(json_str)?
86 .with_status(status)
87 .with_headers(cors);
88 resp.headers().set("Content-Type", "application/json").ok();
89 Ok(resp)
90}
91
92fn with_cors(resp: Response, env: &Env) -> Response {
94 let cors = cors_headers(env);
95 resp.with_headers(cors)
96}
97
98#[event(fetch)]
99async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {
100 match handle_request(req, &env).await {
104 Ok(resp) => Ok(resp),
105 Err(e) => {
106 console_error!("Unhandled worker error: {e:?}");
107 Ok(error_response(&env, "Internal server error", 500))
108 }
109 }
110}
111
112async fn handle_request(mut req: Request, env: &Env) -> Result<Response> {
114 schema::ensure_schema(env).await;
119 nostr_bbs_rate_limit::ensure_replay_schema(env, "DB").await;
120
121 if req.method() == Method::Options {
123 return Ok(Response::empty()?
124 .with_status(204)
125 .with_headers(cors_headers(env)));
126 }
127
128 let ip = nostr_bbs_rate_limit::client_ip(&req);
130 if !nostr_bbs_rate_limit::check_rate_limit(env, "SESSIONS", &ip, 20, 60).await {
131 return json_response(
132 env,
133 &serde_json::json!({ "error": "Too many requests" }),
134 429,
135 );
136 }
137
138 let url = req.url()?;
139 let path = url.path();
140 let method = req.method();
141
142 let body_bytes: Vec<u8> = match method {
145 Method::Post | Method::Put | Method::Patch => req.bytes().await.unwrap_or_default(),
146 _ => Vec::new(),
147 };
148
149 let result = route(&req, env, path, &method, &body_bytes).await;
150
151 match result {
152 Ok(resp) => Ok(with_cors(resp, env)),
153 Err(e) => {
154 console_error!("Route error: {e:?}");
155 let msg = format!("{e:?}");
156 let status = if msg.contains("Serialization")
157 || msg.contains("JSON")
158 || msg.contains("json")
159 || msg.contains("missing field")
160 || msg.contains("parsing")
161 || msg.contains("Invalid")
162 {
163 400u16
164 } else {
165 500u16
166 };
167 let user_msg = if status == 400 {
168 "Invalid request body"
169 } else {
170 "Internal server error"
171 };
172 Ok(error_response(env, user_msg, status))
173 }
174 }
175}
176
177async fn route(
183 req: &Request,
184 env: &Env,
185 path: &str,
186 method: &Method,
187 body_bytes: &[u8],
188) -> Result<Response> {
189 if let Some(rest) = path.strip_prefix("/.well-known/did/nostr/") {
191 if let Some(pubkey) = rest.strip_suffix(".json") {
192 return did::handle_did_document(pubkey, env).await;
193 }
194 }
195
196 if path == "/health" {
198 return json_response(
199 env,
200 &serde_json::json!({
201 "status": "ok",
202 "service": "auth-api",
203 "runtime": "workers-rs"
204 }),
205 200,
206 );
207 }
208
209 if path == "/auth/register/options" && *method == Method::Post {
211 return webauthn::register_options(body_bytes, env).await;
212 }
213
214 if path == "/auth/register/verify" && *method == Method::Post {
216 let cf_country = req.headers().get("CF-IPCountry").ok().flatten();
217 return webauthn::register_verify(body_bytes, cf_country.as_deref(), env).await;
218 }
219
220 if path == "/auth/login/options" && *method == Method::Post {
222 return webauthn::login_options(body_bytes, env).await;
223 }
224
225 if path == "/auth/login/verify" && *method == Method::Post {
227 return webauthn::login_verify(req, body_bytes, env).await;
228 }
229
230 if path == "/auth/lookup" && *method == Method::Post {
232 return webauthn::credential_lookup(body_bytes, env).await;
233 }
234
235 if path.starts_with("/api/") {
237 let auth_header_opt = req.headers().get("Authorization").ok().flatten();
238
239 if let Some(resp) = route_sprint_api(
245 req,
246 env,
247 path,
248 method,
249 body_bytes,
250 auth_header_opt.as_deref(),
251 )
252 .await?
253 {
254 return Ok(resp);
255 }
256
257 let auth_header = match auth_header_opt {
258 Some(h) => h,
259 None => {
260 return json_response(
261 env,
262 &serde_json::json!({ "error": "Authorization required" }),
263 401,
264 )
265 }
266 };
267
268 let expected_origin = env
269 .var("EXPECTED_ORIGIN")
270 .map(|v| v.to_string())
271 .unwrap_or_else(|_| "https://example.com".to_string());
272 let request_url = format!("{expected_origin}{path}");
273
274 let body_for_nip98: Option<&[u8]> = match method {
277 Method::Post | Method::Put | Method::Patch => Some(body_bytes),
278 _ => None,
279 };
280
281 let result = auth::verify_nip98_replay(
282 &auth_header,
283 &request_url,
284 method_str(method),
285 body_for_nip98,
286 env,
287 )
288 .await;
289
290 match result {
291 Ok(token) => {
292 if path == "/api/profile" && *method == Method::Get {
294 let cors = cors_headers(env);
295 return pod::handle_profile(&token.pubkey, env, cors).await;
296 }
297 }
298 Err(_) => {
299 return json_response(
300 env,
301 &serde_json::json!({ "error": "Invalid NIP-98 token" }),
302 401,
303 )
304 }
305 }
306 }
307
308 json_response(env, &serde_json::json!({ "error": "Not found" }), 404)
309}
310
311async fn route_sprint_api(
316 req: &Request,
317 env: &Env,
318 path: &str,
319 method: &Method,
320 body_bytes: &[u8],
321 auth_header: Option<&str>,
322) -> Result<Option<Response>> {
323 let query: Vec<(String, String)> = req
325 .url()
326 .map(|u| {
327 u.query_pairs()
328 .map(|(k, v)| (k.to_string(), v.to_string()))
329 .collect()
330 })
331 .unwrap_or_default();
332
333 if matches!(path, "/api/mod/ban" | "/api/mod/mute" | "/api/mod/warn") && *method == Method::Post
335 {
336 let resp = moderation::handle_action(path, body_bytes, auth_header, env).await?;
337 return Ok(Some(resp));
338 }
339 if path == "/api/mod/report" && *method == Method::Post {
340 let resp = moderation::handle_report(body_bytes, auth_header, env).await?;
341 return Ok(Some(resp));
342 }
343 if path == "/api/mod/actions" && *method == Method::Get {
344 let resp = moderation::handle_list_actions(&query, auth_header, env).await?;
345 return Ok(Some(resp));
346 }
347 if path == "/api/mod/reports" && *method == Method::Get {
348 let resp = moderation::handle_list_reports(&query, auth_header, env).await?;
349 return Ok(Some(resp));
350 }
351 if let Some(rest) = path.strip_prefix("/api/mod/reports/") {
353 if let Some(report_id) = rest.strip_suffix("/action") {
354 if *method == Method::Post && !report_id.is_empty() && !report_id.contains('/') {
355 let resp =
356 moderation::handle_report_action(report_id, body_bytes, auth_header, env)
357 .await?;
358 return Ok(Some(resp));
359 }
360 }
361 }
362
363 if path == "/api/wot/status" && *method == Method::Get {
365 let resp = wot::handle_status(auth_header, env).await?;
366 return Ok(Some(resp));
367 }
368 if path == "/api/wot/set-referente" && *method == Method::Post {
369 let resp = wot::handle_set_referente(body_bytes, auth_header, env).await?;
370 return Ok(Some(resp));
371 }
372 if path == "/api/wot/refresh" && *method == Method::Post {
373 let resp = wot::handle_refresh(body_bytes, auth_header, env).await?;
374 return Ok(Some(resp));
375 }
376 if matches!(path, "/api/wot/override/add" | "/api/wot/override/remove")
377 && *method == Method::Post
378 {
379 let resp = wot::handle_override(path, body_bytes, auth_header, env).await?;
380 return Ok(Some(resp));
381 }
382
383 if path == "/api/invites/create" && *method == Method::Post {
385 let resp = invites::handle_create(body_bytes, auth_header, env).await?;
386 return Ok(Some(resp));
387 }
388 if path == "/api/invites/mine" && *method == Method::Get {
389 let resp = invites::handle_list_mine(auth_header, env).await?;
390 return Ok(Some(resp));
391 }
392 if let Some(rest) = path.strip_prefix("/api/invites/") {
394 if let Some(invite_id) = rest.strip_suffix("/revoke") {
395 if *method == Method::Post && !invite_id.is_empty() && !invite_id.contains('/') {
396 let resp = invites::handle_revoke(invite_id, body_bytes, auth_header, env).await?;
397 return Ok(Some(resp));
398 }
399 }
400 if let Some(code) = rest.strip_suffix("/redeem") {
402 if *method == Method::Post && !code.is_empty() && !code.contains('/') {
403 let resp = invites::handle_redeem(code, body_bytes, auth_header, env).await?;
404 return Ok(Some(resp));
405 }
406 }
407 if *method == Method::Get
409 && !rest.is_empty()
410 && !rest.contains('/')
411 && rest != "create"
412 && rest != "mine"
413 {
414 let resp = invites::handle_preview(rest, env).await?;
415 return Ok(Some(resp));
416 }
417 }
418
419 if path == "/api/welcome/config" && *method == Method::Get {
421 let resp = welcome::handle_get_config(auth_header, env).await?;
422 return Ok(Some(resp));
423 }
424 if path == "/api/welcome/configure" && *method == Method::Post {
425 let resp = welcome::handle_configure(body_bytes, auth_header, env).await?;
426 return Ok(Some(resp));
427 }
428 if path == "/api/welcome/set-bot-key" && *method == Method::Post {
429 let resp = welcome::handle_set_bot_key(body_bytes, auth_header, env).await?;
430 return Ok(Some(resp));
431 }
432 if path == "/api/welcome/test" && *method == Method::Post {
433 let resp = welcome::handle_test(body_bytes, auth_header, env).await?;
434 return Ok(Some(resp));
435 }
436
437 if path == "/api/admins" && *method == Method::Get {
439 let resp = admins::handle_list(auth_header, env).await?;
440 return Ok(Some(resp));
441 }
442 if path == "/api/admins/add" && *method == Method::Post {
443 let resp = admins::handle_add(body_bytes, auth_header, env).await?;
444 return Ok(Some(resp));
445 }
446 if path == "/api/admins/remove" && *method == Method::Post {
447 let resp = admins::handle_remove(body_bytes, auth_header, env).await?;
448 return Ok(Some(resp));
449 }
450
451 if path == "/api/governance/agents" && *method == Method::Get {
453 let resp = governance_api::handle_list_agents(auth_header, env).await?;
454 return Ok(Some(resp));
455 }
456 if path == "/api/governance/agents/register" && *method == Method::Post {
457 let resp = governance_api::handle_register_agent(body_bytes, auth_header, env).await?;
458 return Ok(Some(resp));
459 }
460 if path == "/api/governance/agents/revoke" && *method == Method::Post {
461 let resp = governance_api::handle_revoke_agent(body_bytes, auth_header, env).await?;
462 return Ok(Some(resp));
463 }
464 if path == "/api/governance/cases" && *method == Method::Get {
465 let resp = governance_api::handle_list_cases(&query, auth_header, env).await?;
466 return Ok(Some(resp));
467 }
468 if let Some(case_id) = path.strip_prefix("/api/governance/cases/") {
469 if *method == Method::Get && !case_id.is_empty() && !case_id.contains('/') {
470 let resp = governance_api::handle_get_case(case_id, auth_header, env).await?;
471 return Ok(Some(resp));
472 }
473 }
474 if path == "/api/governance/roles/grant" && *method == Method::Post {
475 let resp = governance_api::handle_grant_role(body_bytes, auth_header, env).await?;
476 return Ok(Some(resp));
477 }
478 if path == "/api/governance/roles/revoke" && *method == Method::Post {
479 let resp = governance_api::handle_revoke_role(body_bytes, auth_header, env).await?;
480 return Ok(Some(resp));
481 }
482 if path == "/api/governance/roles" && *method == Method::Get {
483 let resp = governance_api::handle_list_roles(auth_header, env).await?;
484 return Ok(Some(resp));
485 }
486
487 if path == "/api/moderation/reports" && *method == Method::Get {
489 let resp = moderation::handle_nip1984_reports(auth_header, env).await?;
490 return Ok(Some(resp));
491 }
492
493 if path == "/api/delegation/verify" && *method == Method::Post {
495 let resp = delegation::handle_verify(body_bytes, auth_header, env).await?;
496 return Ok(Some(resp));
497 }
498
499 if path == "/api/username/check" && *method == Method::Get {
501 let resp = username::handle_check(&query, env).await?;
502 return Ok(Some(resp));
503 }
504 if path == "/api/username/claim" && *method == Method::Post {
505 let resp = username::handle_claim(body_bytes, auth_header, env).await?;
506 return Ok(Some(resp));
507 }
508 if path == "/api/username/release" && *method == Method::Post {
509 let resp = username::handle_release(body_bytes, auth_header, env).await?;
510 return Ok(Some(resp));
511 }
512
513 Ok(None)
514}
515
516fn method_str(m: &Method) -> &'static str {
518 match m {
519 Method::Get => "GET",
520 Method::Head => "HEAD",
521 Method::Post => "POST",
522 Method::Put => "PUT",
523 Method::Delete => "DELETE",
524 Method::Options => "OPTIONS",
525 Method::Patch => "PATCH",
526 Method::Connect => "CONNECT",
527 Method::Trace => "TRACE",
528 _ => "GET",
529 }
530}
531
532#[cfg(test)]
537mod tests {
538 use super::*;
539
540 #[test]
543 fn method_str_get() {
544 assert_eq!(method_str(&Method::Get), "GET");
545 }
546
547 #[test]
548 fn method_str_post() {
549 assert_eq!(method_str(&Method::Post), "POST");
550 }
551
552 #[test]
553 fn method_str_put() {
554 assert_eq!(method_str(&Method::Put), "PUT");
555 }
556
557 #[test]
558 fn method_str_delete() {
559 assert_eq!(method_str(&Method::Delete), "DELETE");
560 }
561
562 #[test]
563 fn method_str_head() {
564 assert_eq!(method_str(&Method::Head), "HEAD");
565 }
566
567 #[test]
568 fn method_str_options() {
569 assert_eq!(method_str(&Method::Options), "OPTIONS");
570 }
571
572 #[test]
573 fn method_str_patch() {
574 assert_eq!(method_str(&Method::Patch), "PATCH");
575 }
576
577 #[test]
578 fn method_str_connect() {
579 assert_eq!(method_str(&Method::Connect), "CONNECT");
580 }
581
582 #[test]
583 fn method_str_trace() {
584 assert_eq!(method_str(&Method::Trace), "TRACE");
585 }
586}
587
588#[event(scheduled)]
590async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) -> () {
591 let db = match env.d1("DB") {
592 Ok(db) => db,
593 Err(_) => return,
594 };
595 let _ = db
596 .prepare("SELECT 1")
597 .first::<serde_json::Value>(None)
598 .await;
599}