1use indexmap::IndexMap;
9use lex_ast::canonicalize_program;
10use lex_bytecode::{compile_program, vm::Vm, Value};
11use lex_runtime::{check_program as check_policy, DefaultHandler, Policy};
12use lex_store::Store;
13use lex_syntax::{load_program, load_program_from_str, Manifest};
14use lex_vcs::{MergeSession, MergeSessionId};
15use serde::{Deserialize, Serialize};
16use std::collections::{BTreeMap, BTreeSet, HashMap};
17use std::path::PathBuf;
18use std::sync::{Arc, Mutex};
19use std::time::{SystemTime, UNIX_EPOCH};
20use tiny_http::{Header, Method, Request, Response};
21
22pub struct State {
23 pub store: Mutex<Store>,
24 pub root: PathBuf,
29 pub sessions: Mutex<HashMap<MergeSessionId, ApiMergeSession>>,
36 pub policy_ceiling: Option<Policy>,
57}
58
59pub struct ApiMergeSession {
66 pub inner: MergeSession,
67 pub src_branch: String,
68 pub dst_branch: String,
69}
70
71impl State {
72 pub fn open(root: PathBuf) -> anyhow::Result<Self> {
73 Self::open_with_ceiling(root, None)
74 }
75
76 pub fn open_with_ceiling(
81 root: PathBuf,
82 policy_ceiling: Option<Policy>,
83 ) -> anyhow::Result<Self> {
84 Ok(Self {
85 store: Mutex::new(Store::open(&root)?),
86 root,
87 sessions: Mutex::new(HashMap::new()),
88 policy_ceiling,
89 })
90 }
91
92 pub fn new_with_tenant(tenant_id: &str, store_root: PathBuf) -> anyhow::Result<Self> {
102 validate_tenant_id(tenant_id)?;
103 Self::open(store_root.join(tenant_id))
104 }
105
106 pub fn new_with_tenant_and_ceiling(
111 tenant_id: &str,
112 store_root: PathBuf,
113 policy_ceiling: Option<Policy>,
114 ) -> anyhow::Result<Self> {
115 validate_tenant_id(tenant_id)?;
116 Self::open_with_ceiling(store_root.join(tenant_id), policy_ceiling)
117 }
118}
119
120fn clamp_policy(requested: Policy, ceiling: &Policy) -> Policy {
136 let allow_effects: BTreeSet<String> = requested
137 .allow_effects
138 .intersection(&ceiling.allow_effects)
139 .cloned()
140 .collect();
141 let budget = match (requested.budget, ceiling.budget) {
142 (Some(r), Some(c)) => Some(r.min(c)),
143 (None, Some(c)) => Some(c),
144 (Some(r), None) => Some(r),
145 (None, None) => None,
146 };
147 Policy {
148 allow_effects,
149 allow_fs_read: ceiling.allow_fs_read.clone(),
150 allow_fs_write: ceiling.allow_fs_write.clone(),
151 allow_net_host: ceiling.allow_net_host.clone(),
152 allow_proc: ceiling.allow_proc.clone(),
153 budget,
154 }
155}
156
157fn validate_tenant_id(tenant_id: &str) -> anyhow::Result<()> {
158 if tenant_id.is_empty() {
159 anyhow::bail!("tenant_id must not be empty");
160 }
161 if tenant_id.len() > 64 {
162 anyhow::bail!("tenant_id must be at most 64 bytes");
163 }
164 if !tenant_id
165 .bytes()
166 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
167 {
168 anyhow::bail!(
169 "tenant_id {tenant_id:?} contains characters outside [A-Za-z0-9_-]"
170 );
171 }
172 Ok(())
173}
174
175#[derive(Debug, Serialize, Deserialize)]
176struct ErrorEnvelope {
177 error: String,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 detail: Option<serde_json::Value>,
180}
181
182fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
183 let bytes = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
184 Response::from_data(bytes)
185 .with_status_code(status)
186 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
187}
188
189fn error_response(status: u16, msg: impl Into<String>) -> Response<std::io::Cursor<Vec<u8>>> {
190 json_response(status, &serde_json::to_value(ErrorEnvelope {
191 error: msg.into(), detail: None,
192 }).unwrap())
193}
194
195fn error_with_detail(status: u16, msg: impl Into<String>, detail: serde_json::Value)
196 -> Response<std::io::Cursor<Vec<u8>>>
197{
198 json_response(status, &serde_json::to_value(ErrorEnvelope {
199 error: msg.into(), detail: Some(detail),
200 }).unwrap())
201}
202
203fn write_error_response(prefix: &str, err: lex_store::StoreError)
209 -> Response<std::io::Cursor<Vec<u8>>>
210{
211 if let lex_store::StoreError::Contention { branch, attempts } = &err {
212 let body = serde_json::to_vec(&ErrorEnvelope {
213 error: format!("{prefix}: branch '{branch}' is contended (attempts={attempts})"),
214 detail: Some(serde_json::json!({
215 "kind": "contention",
216 "branch": branch,
217 "attempts": attempts,
218 })),
219 }).unwrap_or_else(|_| b"{}".to_vec());
220 return Response::from_data(body)
221 .with_status_code(503)
222 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
223 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"1"[..]).unwrap());
224 }
225 if let lex_store::StoreError::BudgetExceeded { session_id, cap, spent_after } = &err {
233 let body = serde_json::to_vec(&ErrorEnvelope {
234 error: format!(
235 "{prefix}: session `{session_id}` budget exceeded \
236 (spent_after={spent_after}, cap={cap})"
237 ),
238 detail: Some(serde_json::json!({
239 "kind": "budget_exceeded",
240 "session_id": session_id,
241 "cap": cap,
242 "spent_after": spent_after,
243 })),
244 }).unwrap_or_else(|_| b"{}".to_vec());
245 return Response::from_data(body)
246 .with_status_code(503)
247 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
248 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"0"[..]).unwrap());
249 }
250 error_response(500, format!("{prefix}: {err}"))
251}
252
253pub fn handle(state: Arc<State>, mut req: Request) -> std::io::Result<()> {
254 let method = req.method().clone();
255 let url = req.url().to_string();
256 let path = url.split('?').next().unwrap_or("").to_string();
257 let query = url.split_once('?').map(|(_, q)| q.to_string()).unwrap_or_default();
258
259 let x_lex_user = req.headers().iter()
264 .find(|h| h.field.equiv("x-lex-user"))
265 .map(|h| h.value.as_str().to_string());
266
267 if matches!(method, Method::Post) && path == "/v1/pkg/publish" {
269 let mut body_bytes: Vec<u8> = Vec::new();
270 let _ = req.as_reader().read_to_end(&mut body_bytes);
271 let resp = pkg_publish_handler(&state, &body_bytes);
272 return req.respond(resp);
273 }
274
275 let mut body = String::new();
276 let _ = req.as_reader().read_to_string(&mut body);
277
278 let resp = route(&state, &method, &path, &query, &body, x_lex_user.as_deref());
279 req.respond(resp)
280}
281
282pub fn handle_with_auth<F>(state: Arc<State>, req: Request, auth: F) -> std::io::Result<()>
286where
287 F: FnOnce(&str, &[Header]) -> bool,
288{
289 let path = req.url().split('?').next().unwrap_or("").to_string();
290 if !auth(&path, req.headers()) {
291 return req.respond(
292 Response::from_data(br#"{"error":"unauthorized"}"#.to_vec())
293 .with_status_code(401)
294 .with_header(
295 Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
296 ),
297 );
298 }
299 handle(state, req)
300}
301
302fn route(
303 state: &State,
304 method: &Method,
305 path: &str,
306 query: &str,
307 body: &str,
308 x_lex_user: Option<&str>,
309) -> Response<std::io::Cursor<Vec<u8>>> {
310 match (method, path) {
311 (Method::Get, "/") => crate::web::activity_handler(state),
313 (Method::Get, "/web/branches") => crate::web::branches_handler(state),
314 (Method::Get, "/web/trust") => crate::web::trust_handler(state),
315 (Method::Get, "/web/attention") => crate::web::attention_handler(state),
316 (Method::Get, p) if p.starts_with("/web/branch/") => {
317 let name = &p["/web/branch/".len()..];
318 crate::web::branch_handler(state, name)
319 }
320 (Method::Get, p) if p.starts_with("/web/stage/") => {
321 let id = &p["/web/stage/".len()..];
322 crate::web::stage_html_handler(state, id)
323 }
324 (Method::Post, p) if p.starts_with("/web/stage/") && (
329 p.ends_with("/pin") || p.ends_with("/defer")
330 || p.ends_with("/block") || p.ends_with("/unblock")
331 ) => {
332 let prefix_len = "/web/stage/".len();
333 let last_slash = p.rfind('/').unwrap_or(p.len());
334 let id = &p[prefix_len..last_slash];
335 let verb = &p[last_slash + 1..];
336 let decision = match verb {
337 "pin" => crate::web::WebStageDecision::Pin,
338 "defer" => crate::web::WebStageDecision::Defer,
339 "block" => crate::web::WebStageDecision::Block,
340 "unblock" => crate::web::WebStageDecision::Unblock,
341 _ => unreachable!("matched in outer guard"),
342 };
343 crate::web::stage_decision_handler(state, id, body, decision, x_lex_user)
344 }
345 (Method::Get, "/v1/health") => json_response(200, &serde_json::json!({"ok": true})),
347 (Method::Post, "/v1/parse") => parse_handler(body),
348 (Method::Post, "/v1/check") => check_handler(body),
349 (Method::Post, "/v1/publish") => publish_handler(state, body),
350 (Method::Post, "/v1/patch") => patch_handler(state, body),
351 (Method::Get, p) if p.starts_with("/v1/stage/") => {
352 let suffix = &p["/v1/stage/".len()..];
353 if let Some(id) = suffix.strip_suffix("/attestations") {
356 stage_attestations_handler(state, id)
357 } else {
358 stage_handler(state, suffix)
359 }
360 }
361 (Method::Post, "/v1/run") => run_handler(state, body, false),
362 (Method::Post, "/v1/replay") => run_handler(state, body, true),
363 (Method::Get, p) if p.starts_with("/v1/trace/") => {
364 let id = &p["/v1/trace/".len()..];
365 trace_handler(state, id)
366 }
367 (Method::Get, "/v1/diff") => diff_handler(state, query),
368 (Method::Post, "/v1/merge/start") => merge_start_handler(state, body),
369 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/resolve") => {
370 let id = &p["/v1/merge/".len()..p.len() - "/resolve".len()];
371 merge_resolve_handler(state, id, body)
372 }
373 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/commit") => {
374 let id = &p["/v1/merge/".len()..p.len() - "/commit".len()];
375 merge_commit_handler(state, id)
376 }
377 (Method::Post, "/v1/ops/batch") => ops_batch_handler(state, body),
379 (Method::Post, "/v1/attestations/batch") => attestations_batch_handler(state, body),
380 (Method::Get, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
384 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
385 branch_head_handler(state, name)
386 }
387 (Method::Get, "/v1/ops/since") => ops_since_handler(state, query),
391 (Method::Get, "/v1/attestations/since") => attestations_since_handler(state, query),
392 (Method::Get, "/v1/pkg") => pkg_list_handler(state),
396 (Method::Put, p) if p.starts_with("/v1/pkg/") && p.ends_with("/visibility") => {
400 let name = &p["/v1/pkg/".len()..p.len() - "/visibility".len()];
401 pkg_set_visibility_handler(state, name, body)
402 }
403 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/head") => {
404 let name = &p["/v1/pkg/".len()..p.len() - "/head".len()];
405 pkg_head_handler(state, name)
406 }
407 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/versions") => {
408 let name = &p["/v1/pkg/".len()..p.len() - "/versions".len()];
409 pkg_versions_handler(state, name)
410 }
411 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/archive") => {
413 let inner = &p["/v1/pkg/".len()..p.len() - "/archive".len()];
414 if let Some((name, version)) = inner.split_once('/') {
416 pkg_archive_handler(state, name, version)
417 } else {
418 error_response(400, "expected /v1/pkg/{name}/{version}/archive")
419 }
420 }
421 (Method::Get, p) if p.starts_with("/v1/pkg/") && p["/v1/pkg/".len()..].contains('/') => {
423 let inner = &p["/v1/pkg/".len()..];
424 if let Some((name, version)) = inner.split_once('/') {
425 pkg_get_version_handler(state, name, version)
426 } else {
427 error_response(400, "expected /v1/pkg/{name}/{version}")
428 }
429 }
430 (Method::Get, p) if p.starts_with("/v1/pkg/") => {
431 let name = &p["/v1/pkg/".len()..];
432 pkg_get_handler(state, name)
433 }
434 (Method::Delete, p) if p.starts_with("/v1/pkg/") => {
435 let name = &p["/v1/pkg/".len()..];
436 pkg_delete_handler(state, name)
437 }
438 _ => error_response(404, format!("unknown route: {method:?} {path}")),
439 }
440}
441
442#[derive(Deserialize)]
443struct ParseReq { source: String }
444
445fn parse_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
446 let req: ParseReq = match serde_json::from_str(body) {
447 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
448 };
449 match load_program_from_str(&req.source) {
450 Ok(prog) => {
451 let stages = canonicalize_program(&prog);
452 json_response(200, &serde_json::to_value(&stages).unwrap())
453 }
454 Err(e) => error_response(400, format!("syntax error: {e}")),
455 }
456}
457
458pub(crate) fn check_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
459 let req: ParseReq = match serde_json::from_str(body) {
460 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
461 };
462 let prog = match load_program_from_str(&req.source) {
463 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
464 };
465 let stages = canonicalize_program(&prog);
466 match lex_types::check_program(&stages) {
467 Ok(_) => json_response(200, &serde_json::json!({"ok": true})),
468 Err(errs) => json_response(422, &serde_json::to_value(&errs).unwrap()),
469 }
470}
471
472#[derive(Deserialize)]
473struct PublishReq { source: String, #[serde(default)] activate: bool }
474
475pub(crate) fn publish_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
476 let req: PublishReq = match serde_json::from_str(body) {
477 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
478 };
479 let prog = match load_program_from_str(&req.source) {
480 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
481 };
482 let mut stages = canonicalize_program(&prog);
486 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
487 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
488 }
489
490 let store = state.store.lock().unwrap();
491 let branch = store.current_branch();
492
493 let old_head = match store.branch_head(&branch) {
495 Ok(h) => h,
496 Err(e) => return error_response(500, format!("branch_head: {e}")),
497 };
498 let old_fns: std::collections::BTreeMap<String, lex_ast::FnDecl> = old_head.values()
499 .filter_map(|stg| store.get_ast(stg).ok())
500 .filter_map(|s| match s {
501 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd)),
502 _ => None,
503 })
504 .collect();
505 let new_fns: std::collections::BTreeMap<String, lex_ast::FnDecl> = stages.iter()
506 .filter_map(|s| match s {
507 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
508 _ => None,
509 })
510 .collect();
511 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
512
513 let mut new_imports: lex_vcs::ImportMap = lex_vcs::ImportMap::new();
515 {
516 let entry = new_imports.entry("<source>".into()).or_default();
517 for s in &stages {
518 if let lex_ast::Stage::Import(im) = s {
519 entry.insert(im.reference.clone());
520 }
521 }
522 }
523
524 match store.publish_program(&branch, &stages, &report, &new_imports, req.activate) {
525 Ok(outcome) => json_response(200, &serde_json::json!({
526 "ops": outcome.ops,
527 "head_op": outcome.head_op,
528 })),
529 Err(lex_store::StoreError::TypeError(errs)) => {
537 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
538 }
539 Err(e) => write_error_response("publish_program", e),
540 }
541}
542
543#[derive(Deserialize)]
544struct PatchReq {
545 stage_id: String,
546 patch: lex_ast::Patch,
547 #[serde(default)] activate: bool,
548}
549
550fn patch_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
553 let req: PatchReq = match serde_json::from_str(body) {
554 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
555 };
556 let store = state.store.lock().unwrap();
557
558 let original = match store.get_ast(&req.stage_id) {
560 Ok(s) => s, Err(e) => return error_response(404, format!("stage: {e}")),
561 };
562
563 let patched = match lex_ast::apply_patch(&original, &req.patch) {
565 Ok(s) => s,
566 Err(e) => return error_with_detail(422, "patch failed",
567 serde_json::to_value(&e).unwrap_or_default()),
568 };
569
570 let stages = vec![patched.clone()];
572 if let Err(errs) = lex_types::check_program(&stages) {
573 return error_with_detail(422, "type errors after patch",
574 serde_json::to_value(&errs).unwrap_or_default());
575 }
576
577 let branch = store.current_branch();
581
582 let sig = match lex_ast::sig_id(&patched) {
584 Some(s) => s,
585 None => return error_response(500, "patched stage has no sig_id"),
586 };
587
588 let new_id = match store.publish(&patched) {
589 Ok(id) => id, Err(e) => return error_response(500, format!("publish: {e}")),
590 };
591 if req.activate {
592 if let Err(e) = store.activate(&new_id) {
593 return error_response(500, format!("activate: {e}"));
594 }
595 }
596
597 let original_effects: std::collections::BTreeSet<String> = match &original {
599 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
600 _ => std::collections::BTreeSet::new(),
601 };
602 let patched_effects: std::collections::BTreeSet<String> = match &patched {
603 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
604 _ => std::collections::BTreeSet::new(),
605 };
606 let head_now = match store.get_branch(&branch) {
607 Ok(b) => b.and_then(|b| b.head_op),
608 Err(e) => return error_response(500, format!("get_branch: {e}")),
609 };
610 let kind = if original_effects != patched_effects {
611 let from_budget = lex_vcs::operation_budget_from_effects(&original_effects);
618 let to_budget = lex_vcs::operation_budget_from_effects(&patched_effects);
619 lex_vcs::OperationKind::ChangeEffectSig {
620 sig_id: sig.clone(),
621 from_stage_id: req.stage_id.clone(),
622 to_stage_id: new_id.clone(),
623 from_effects: original_effects,
624 to_effects: patched_effects,
625 from_budget,
626 to_budget,
627 }
628 } else {
629 let budget = lex_vcs::operation_budget_from_effects(&original_effects);
630 lex_vcs::OperationKind::ModifyBody {
631 sig_id: sig.clone(),
632 from_stage_id: req.stage_id.clone(),
633 to_stage_id: new_id.clone(),
634 from_budget: budget,
635 to_budget: budget,
636 }
637 };
638 let transition = lex_vcs::StageTransition::Replace {
639 sig_id: sig.clone(),
640 from: req.stage_id.clone(),
641 to: new_id.clone(),
642 };
643 let op = lex_vcs::Operation::new(
644 kind,
645 head_now.into_iter().collect::<Vec<_>>(),
646 );
647 let op_id = match store.apply_operation(&branch, op, transition) {
648 Ok(id) => id,
649 Err(e) => return write_error_response("apply_operation", e),
650 };
651
652 let status = format!("{:?}",
653 store.get_status(&new_id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
654 json_response(200, &serde_json::json!({
655 "old_stage_id": req.stage_id,
656 "new_stage_id": new_id,
657 "sig_id": sig,
658 "status": status,
659 "op_id": op_id,
660 }))
661}
662
663pub(crate) fn stage_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
664 let store = state.store.lock().unwrap();
665 let meta = match store.get_metadata(id) {
666 Ok(m) => m, Err(e) => return error_response(404, format!("{e}")),
667 };
668 let ast = match store.get_ast(id) {
669 Ok(a) => a, Err(e) => return error_response(404, format!("{e}")),
670 };
671 let status = format!("{:?}", store.get_status(id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
672 json_response(200, &serde_json::json!({
673 "metadata": meta,
674 "ast": ast,
675 "status": status,
676 }))
677}
678
679pub(crate) fn stage_attestations_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
688 let store = state.store.lock().unwrap();
689 if let Err(e) = store.get_metadata(id) {
690 return error_response(404, format!("{e}"));
691 }
692 let log = match store.attestation_log() {
693 Ok(l) => l,
694 Err(e) => return error_response(500, format!("attestation log: {e}")),
695 };
696 let mut listing = match log.list_for_stage(&id.to_string()) {
697 Ok(v) => v,
698 Err(e) => return error_response(500, format!("list_for_stage: {e}")),
699 };
700 listing.sort_by_key(|a| std::cmp::Reverse(a.timestamp));
701 json_response(200, &serde_json::json!({"attestations": listing}))
702}
703
704#[derive(Deserialize, Default)]
705struct PolicyJson {
706 #[serde(default)] allow_effects: Vec<String>,
707 #[serde(default)] allow_fs_read: Vec<String>,
708 #[serde(default)] allow_fs_write: Vec<String>,
709 #[serde(default)] budget: Option<u64>,
710}
711
712impl PolicyJson {
713 fn into_policy(self) -> Policy {
714 Policy {
715 allow_effects: self.allow_effects.into_iter().collect::<BTreeSet<_>>(),
716 allow_fs_read: self.allow_fs_read.into_iter().map(PathBuf::from).collect(),
717 allow_fs_write: self.allow_fs_write.into_iter().map(PathBuf::from).collect(),
718 allow_net_host: Vec::new(),
719 allow_proc: Vec::new(),
720 budget: self.budget,
721 }
722 }
723}
724
725#[derive(Deserialize)]
726struct RunReq {
727 source: String,
728 #[serde(rename = "fn")] func: String,
729 #[serde(default)] args: Vec<serde_json::Value>,
730 #[serde(default)] policy: PolicyJson,
731 #[serde(default)] overrides: IndexMap<String, serde_json::Value>,
732}
733
734pub(crate) fn run_handler(state: &State, body: &str, with_overrides: bool) -> Response<std::io::Cursor<Vec<u8>>> {
735 let req: RunReq = match serde_json::from_str(body) {
736 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
737 };
738 let prog = match load_program_from_str(&req.source) {
739 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
740 };
741 let stages = canonicalize_program(&prog);
742 if let Err(errs) = lex_types::check_program(&stages) {
743 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
744 }
745 let bc = compile_program(&stages);
746 let mut policy = req.policy.into_policy();
747 if let Some(ceiling) = &state.policy_ceiling {
753 policy = clamp_policy(policy, ceiling);
754 }
755 if let Err(violations) = check_policy(&bc, &policy) {
756 return error_with_detail(403, "policy violation", serde_json::to_value(&violations).unwrap());
757 }
758
759 let mut recorder = lex_trace::Recorder::new();
760 if with_overrides && !req.overrides.is_empty() {
761 recorder = recorder.with_overrides(req.overrides);
762 }
763 let handle = recorder.handle();
764 let handler = DefaultHandler::new(policy);
765 let mut vm = Vm::with_handler(&bc, Box::new(handler));
766 vm.set_tracer(Box::new(recorder));
767
768 let vargs: Vec<Value> = req.args.iter().map(json_to_value).collect();
769 let started = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
770 let result = vm.call(&req.func, vargs);
771 let ended = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
772
773 let store = state.store.lock().unwrap();
774 let (root_out, root_err, status) = match &result {
775 Ok(v) => (Some(value_to_json(v)), None, 200u16),
776 Err(e) => (None, Some(format!("{e}")), 200u16),
777 };
778 let tree = handle.finalize(req.func.clone(), serde_json::Value::Null,
779 root_out.clone(), root_err.clone(), started, ended);
780 let run_id = match store.save_trace(&tree) {
781 Ok(id) => id,
782 Err(e) => return error_response(500, format!("save_trace: {e}")),
783 };
784
785 let mut body = serde_json::json!({
786 "run_id": run_id,
787 "output": root_out,
788 });
789 if let Some(err) = root_err {
790 body["error"] = serde_json::Value::String(err);
791 }
792 json_response(status, &body)
793}
794
795fn trace_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
796 let store = state.store.lock().unwrap();
797 match store.load_trace(id) {
798 Ok(t) => json_response(200, &serde_json::to_value(&t).unwrap()),
799 Err(e) => error_response(404, format!("{e}")),
800 }
801}
802
803fn diff_handler(state: &State, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
804 let mut a = None;
805 let mut b = None;
806 for kv in query.split('&') {
807 if let Some((k, v)) = kv.split_once('=') {
808 match k { "a" => a = Some(v.to_string()), "b" => b = Some(v.to_string()), _ => {} }
809 }
810 }
811 let (Some(a), Some(b)) = (a, b) else {
812 return error_response(400, "missing a or b query params");
813 };
814 let store = state.store.lock().unwrap();
815 let ta = match store.load_trace(&a) { Ok(t) => t, Err(e) => return error_response(404, format!("a: {e}")) };
816 let tb = match store.load_trace(&b) { Ok(t) => t, Err(e) => return error_response(404, format!("b: {e}")) };
817 match lex_trace::diff_runs(&ta, &tb) {
818 Some(d) => json_response(200, &serde_json::to_value(&d).unwrap()),
819 None => json_response(200, &serde_json::json!({"divergence": null})),
820 }
821}
822
823fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
824
825fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
826
827#[derive(Deserialize)]
828struct MergeStartReq {
829 src_branch: String,
830 dst_branch: String,
831}
832
833fn merge_start_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
844 let req: MergeStartReq = match serde_json::from_str(body) {
845 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
846 };
847 let store = state.store.lock().unwrap();
848 let src_head = match store.get_branch(&req.src_branch) {
849 Ok(Some(b)) => b.head_op,
850 Ok(None) => return error_response(404, format!("unknown src branch `{}`", req.src_branch)),
851 Err(e) => return error_response(500, format!("src branch read: {e}")),
852 };
853 let dst_head = match store.get_branch(&req.dst_branch) {
854 Ok(Some(b)) => b.head_op,
855 Ok(None) => return error_response(404, format!("unknown dst branch `{}`", req.dst_branch)),
856 Err(e) => return error_response(500, format!("dst branch read: {e}")),
857 };
858 let log = match lex_vcs::OpLog::open(store.root()) {
859 Ok(l) => l,
860 Err(e) => return error_response(500, format!("op log: {e}")),
861 };
862 let merge_id = mint_merge_id();
866 let session = match MergeSession::start(
867 merge_id.clone(),
868 &log,
869 src_head.as_ref(),
870 dst_head.as_ref(),
871 ) {
872 Ok(s) => s,
873 Err(e) => return error_response(500, format!("merge start: {e}")),
874 };
875 let conflicts: Vec<&lex_vcs::ConflictRecord> = session.remaining_conflicts();
876 let auto_resolved_count = session.auto_resolved.len();
877 let body = serde_json::json!({
878 "merge_id": merge_id,
879 "src_head": session.src_head,
880 "dst_head": session.dst_head,
881 "lca": session.lca,
882 "conflicts": conflicts,
883 "auto_resolved_count": auto_resolved_count,
884 });
885 drop(conflicts);
886 drop(store);
887 let wrapped = ApiMergeSession {
888 inner: session,
889 src_branch: req.src_branch,
890 dst_branch: req.dst_branch,
891 };
892 state.sessions.lock().unwrap().insert(merge_id, wrapped);
893 json_response(200, &body)
894}
895
896#[derive(Deserialize)]
897struct MergeResolveReq {
898 resolutions: Vec<MergeResolveEntry>,
903}
904
905#[derive(Deserialize)]
906struct MergeResolveEntry {
907 conflict_id: String,
908 resolution: lex_vcs::Resolution,
909}
910
911fn merge_resolve_handler(
922 state: &State,
923 merge_id: &str,
924 body: &str,
925) -> Response<std::io::Cursor<Vec<u8>>> {
926 let req: MergeResolveReq = match serde_json::from_str(body) {
927 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
928 };
929 let mut sessions = state.sessions.lock().unwrap();
930 let Some(wrapped) = sessions.get_mut(merge_id) else {
931 return error_response(404, format!("unknown merge_id `{merge_id}`"));
932 };
933 let pairs: Vec<(String, lex_vcs::Resolution)> = req.resolutions.into_iter()
934 .map(|e| (e.conflict_id, e.resolution))
935 .collect();
936 let verdicts = wrapped.inner.resolve(pairs);
937 let remaining: Vec<&lex_vcs::ConflictRecord> = wrapped.inner.remaining_conflicts();
938 let body = serde_json::json!({
939 "verdicts": verdicts,
940 "remaining_conflicts": remaining,
941 });
942 json_response(200, &body)
943}
944
945fn merge_commit_handler(
964 state: &State,
965 merge_id: &str,
966) -> Response<std::io::Cursor<Vec<u8>>> {
967 use std::collections::BTreeMap;
968 let wrapped = match state.sessions.lock().unwrap().remove(merge_id) {
969 Some(w) => w,
970 None => return error_response(404, format!("unknown merge_id `{merge_id}`")),
971 };
972 let dst_branch = wrapped.dst_branch.clone();
973 let src_head = wrapped.inner.src_head.clone();
974 let dst_head = wrapped.inner.dst_head.clone();
975 let auto_resolved = wrapped.inner.auto_resolved.clone();
976
977 let mut entries: BTreeMap<lex_vcs::SigId, Option<lex_vcs::StageId>> = BTreeMap::new();
980
981 for outcome in &auto_resolved {
983 if let lex_vcs::MergeOutcome::Src { sig_id, stage_id } = outcome {
984 entries.insert(sig_id.clone(), stage_id.clone());
985 }
986 }
987
988 let resolved = match wrapped.inner.commit() {
990 Ok(r) => r,
991 Err(lex_vcs::CommitError::ConflictsRemaining(ids)) => {
992 return error_with_detail(
996 422,
997 "conflicts remaining",
998 serde_json::json!({"unresolved": ids}),
999 );
1000 }
1001 };
1002
1003 for (conflict_id, resolution) in resolved {
1004 match resolution {
1005 lex_vcs::Resolution::TakeOurs => {
1006 }
1008 lex_vcs::Resolution::TakeTheirs => {
1009 match resolve_take_theirs(state, &src_head, &conflict_id) {
1019 Ok(stage_id) => {
1020 entries.insert(conflict_id.clone(), stage_id);
1021 }
1022 Err(e) => return error_response(500, format!("resolve take_theirs: {e}")),
1023 }
1024 }
1025 lex_vcs::Resolution::Custom { op } => {
1026 match op.kind.merge_target() {
1035 Some((sig, stage)) => {
1036 if sig != conflict_id {
1037 return error_with_detail(
1038 422,
1039 "custom op targets a different sig than the conflict",
1040 serde_json::json!({
1041 "conflict_id": conflict_id,
1042 "op_targets": sig,
1043 }),
1044 );
1045 }
1046 entries.insert(conflict_id, stage);
1047 }
1048 None => {
1049 return error_with_detail(
1050 422,
1051 "custom op kind doesn't yield a single sig→stage delta",
1052 serde_json::json!({
1053 "conflict_id": conflict_id,
1054 "kind": serde_json::to_value(&op.kind).unwrap_or(serde_json::Value::Null),
1055 }),
1056 );
1057 }
1058 }
1059 }
1060 lex_vcs::Resolution::Defer => {
1061 return error_response(500, "internal: Defer slipped past commit gate");
1063 }
1064 }
1065 }
1066
1067 let resolved_count = entries.len();
1068 let mut parents: Vec<lex_vcs::OpId> = Vec::new();
1069 if let Some(d) = dst_head { parents.push(d); }
1070 if let Some(s) = src_head { parents.push(s); }
1071 let op = lex_vcs::Operation::new(
1072 lex_vcs::OperationKind::Merge { resolved: resolved_count },
1073 parents,
1074 );
1075 let transition = lex_vcs::StageTransition::Merge { entries };
1076 let store = state.store.lock().unwrap();
1077 match store.apply_operation(&dst_branch, op, transition) {
1078 Ok(new_head_op) => json_response(200, &serde_json::json!({
1079 "new_head_op": new_head_op,
1080 "dst_branch": dst_branch,
1081 })),
1082 Err(e) => write_error_response("apply merge op", e),
1083 }
1084}
1085
1086fn resolve_take_theirs(
1091 state: &State,
1092 src_head: &Option<lex_vcs::OpId>,
1093 sig: &lex_vcs::SigId,
1094) -> std::io::Result<Option<lex_vcs::StageId>> {
1095 let store = state.store.lock().unwrap();
1096 let log = lex_vcs::OpLog::open(store.root())?;
1097 let Some(head) = src_head.as_ref() else { return Ok(None); };
1098 let mut current: Option<lex_vcs::StageId> = None;
1101 for record in log.walk_forward(head, None)? {
1102 match &record.produces {
1103 lex_vcs::StageTransition::Create { sig_id, stage_id }
1104 if sig_id == sig => { current = Some(stage_id.clone()); }
1105 lex_vcs::StageTransition::Replace { sig_id, to, .. }
1106 if sig_id == sig => { current = Some(to.clone()); }
1107 lex_vcs::StageTransition::Remove { sig_id, .. }
1108 if sig_id == sig => { current = None; }
1109 lex_vcs::StageTransition::Rename { from, to, body_stage_id }
1110 if from == sig || to == sig => {
1111 if from == sig { current = None; }
1112 if to == sig { current = Some(body_stage_id.clone()); }
1113 }
1114 lex_vcs::StageTransition::Merge { entries } => {
1115 if let Some(opt) = entries.get(sig) {
1116 current = opt.clone();
1117 }
1118 }
1119 _ => {}
1120 }
1121 }
1122 Ok(current)
1123}
1124
1125fn mint_merge_id() -> MergeSessionId {
1126 use std::sync::atomic::{AtomicU64, Ordering};
1127 static COUNTER: AtomicU64 = AtomicU64::new(0);
1128 let nanos = SystemTime::now()
1129 .duration_since(UNIX_EPOCH)
1130 .map(|d| d.as_nanos())
1131 .unwrap_or(0);
1132 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1133 format!("merge_{nanos:x}_{n:x}")
1134}
1135
1136pub(crate) fn ops_batch_handler(state: &State, body: &str)
1167 -> Response<std::io::Cursor<Vec<u8>>>
1168{
1169 let records: Vec<lex_vcs::OperationRecord> = match serde_json::from_str(body) {
1170 Ok(r) => r,
1171 Err(e) => return error_response(400,
1172 format!("body must be a JSON array of OperationRecord: {e}")),
1173 };
1174 let store = state.store.lock().unwrap();
1175 let log = match lex_vcs::OpLog::open(store.root()) {
1176 Ok(l) => l,
1177 Err(e) => return error_response(500, format!("opening op log: {e}")),
1178 };
1179
1180 let mut batch_ids: std::collections::BTreeSet<lex_vcs::OpId> =
1188 std::collections::BTreeSet::new();
1189 for rec in &records {
1190 let expected = rec.op.op_id();
1191 if expected != rec.op_id {
1192 return error_with_detail(409, "OpIdMismatch", serde_json::json!({
1193 "supplied": rec.op_id,
1194 "expected": expected,
1195 }));
1196 }
1197 for parent in &rec.op.parents {
1198 let known = match log.get(parent) {
1199 Ok(Some(_)) => true,
1200 Ok(None) => false,
1201 Err(e) => return error_response(500, format!("op log read: {e}")),
1202 };
1203 if !known && !batch_ids.contains(parent) {
1204 return error_with_detail(422, "MissingParent", serde_json::json!({
1205 "op_id": rec.op_id,
1206 "missing_parent": parent,
1207 }));
1208 }
1209 }
1210 batch_ids.insert(rec.op_id.clone());
1211 }
1212
1213 let mut added = 0usize;
1216 let mut added_ids: Vec<&lex_vcs::OpId> = Vec::new();
1217 for rec in &records {
1218 let already_present = matches!(log.get(&rec.op_id), Ok(Some(_)));
1219 match log.put(rec) {
1220 Ok(()) => {
1221 if !already_present {
1222 added += 1;
1223 added_ids.push(&rec.op_id);
1224 }
1225 }
1226 Err(e) => return error_response(500, format!("op log write: {e}")),
1227 }
1228 }
1229
1230 json_response(200, &serde_json::json!({
1231 "received": records.len(),
1232 "added": added,
1233 "skipped": records.len() - added,
1234 "added_ids": added_ids,
1235 }))
1236}
1237
1238pub(crate) fn attestations_batch_handler(state: &State, body: &str)
1260 -> Response<std::io::Cursor<Vec<u8>>>
1261{
1262 let attestations: Vec<lex_vcs::Attestation> = match serde_json::from_str(body) {
1263 Ok(a) => a,
1264 Err(e) => return error_response(400,
1265 format!("body must be a JSON array of Attestation: {e}")),
1266 };
1267 let store = state.store.lock().unwrap();
1268 let log = match store.attestation_log() {
1269 Ok(l) => l,
1270 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1271 };
1272 let op_log = match lex_vcs::OpLog::open(store.root()) {
1273 Ok(l) => l,
1274 Err(e) => return error_response(500, format!("opening op log: {e}")),
1275 };
1276
1277 for att in &attestations {
1279 let expected = lex_vcs::Attestation::with_timestamp(
1282 att.stage_id.clone(),
1283 att.op_id.clone(),
1284 att.intent_id.clone(),
1285 att.kind.clone(),
1286 att.result.clone(),
1287 att.produced_by.clone(),
1288 att.cost.clone(),
1289 att.timestamp,
1290 ).attestation_id;
1291 if expected != att.attestation_id {
1292 return error_with_detail(409, "AttestationIdMismatch", serde_json::json!({
1293 "supplied": att.attestation_id,
1294 "expected": expected,
1295 }));
1296 }
1297 if let Some(op_id) = &att.op_id {
1301 match op_log.get(op_id) {
1302 Ok(Some(_)) => {}
1303 Ok(None) => return error_with_detail(422, "UnknownOp", serde_json::json!({
1304 "attestation_id": att.attestation_id,
1305 "op_id": op_id,
1306 })),
1307 Err(e) => return error_response(500, format!("op log read: {e}")),
1308 }
1309 }
1310 }
1311
1312 let mut added = 0usize;
1316 let mut added_ids: Vec<&lex_vcs::AttestationId> = Vec::new();
1317 for att in &attestations {
1318 let already_present = matches!(log.get(&att.attestation_id), Ok(Some(_)));
1319 match log.put(att) {
1320 Ok(()) => {
1321 if !already_present {
1322 added += 1;
1323 added_ids.push(&att.attestation_id);
1324 }
1325 }
1326 Err(e) => return error_response(500, format!("attestation log write: {e}")),
1327 }
1328 }
1329
1330 json_response(200, &serde_json::json!({
1331 "received": attestations.len(),
1332 "added": added,
1333 "skipped": attestations.len() - added,
1334 "added_ids": added_ids,
1335 }))
1336}
1337
1338pub(crate) fn branch_head_handler(state: &State, name: &str)
1347 -> Response<std::io::Cursor<Vec<u8>>>
1348{
1349 let store = state.store.lock().unwrap();
1350 let head = match store.get_branch(name) {
1351 Ok(Some(b)) => b.head_op,
1352 Ok(None) => None,
1353 Err(e) => return error_response(500, format!("get_branch: {e}")),
1354 };
1355 json_response(200, &serde_json::json!({
1356 "branch": name,
1357 "head_op": head,
1358 }))
1359}
1360
1361pub(crate) fn ops_since_handler(state: &State, query: &str)
1385 -> Response<std::io::Cursor<Vec<u8>>>
1386{
1387 let mut after: Option<String> = None;
1388 let mut branch = String::from("main");
1389 let mut limit: Option<usize> = None;
1390 for kv in query.split('&') {
1391 let Some((k, v)) = kv.split_once('=') else { continue };
1392 match k {
1393 "after" => after = Some(v.to_string()),
1394 "branch" => branch = v.to_string(),
1395 "limit" => {
1396 limit = Some(match v.parse::<usize>() {
1397 Ok(n) => n,
1398 Err(_) => return error_response(400,
1399 format!("limit must be a positive integer, got `{v}`")),
1400 });
1401 }
1402 _ => {}
1403 }
1404 }
1405
1406 let store = state.store.lock().unwrap();
1407 let log = match lex_vcs::OpLog::open(store.root()) {
1408 Ok(l) => l,
1409 Err(e) => return error_response(500, format!("opening op log: {e}")),
1410 };
1411 let head = match store.get_branch(&branch) {
1412 Ok(Some(b)) => b.head_op,
1413 Ok(None) => None,
1414 Err(e) => return error_response(500, format!("get_branch: {e}")),
1415 };
1416 let Some(head) = head else {
1417 return json_response(200, &serde_json::json!([]));
1418 };
1419
1420 let ops_since = match log.ops_since(&head, after.as_ref()) {
1421 Ok(o) => o,
1422 Err(e) => return error_response(500, format!("ops_since: {e}")),
1423 };
1424 let mut ops = ops_since;
1428 ops.reverse();
1429 if let Some(n) = limit {
1430 ops.truncate(n);
1431 }
1432
1433 json_response(200, &serde_json::to_value(&ops).unwrap_or_default())
1434}
1435
1436pub(crate) fn attestations_since_handler(state: &State, query: &str)
1449 -> Response<std::io::Cursor<Vec<u8>>>
1450{
1451 let mut after_op: Option<String> = None;
1452 let mut limit: Option<usize> = None;
1453 for kv in query.split('&') {
1454 let Some((k, v)) = kv.split_once('=') else { continue };
1455 match k {
1456 "after-op" => after_op = Some(v.to_string()),
1457 "limit" => {
1458 limit = Some(match v.parse::<usize>() {
1459 Ok(n) => n,
1460 Err(_) => return error_response(400,
1461 format!("limit must be a positive integer, got `{v}`")),
1462 });
1463 }
1464 _ => {}
1465 }
1466 }
1467
1468 let store = state.store.lock().unwrap();
1469 let log = match store.attestation_log() {
1470 Ok(l) => l,
1471 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1472 };
1473
1474 let exclude: std::collections::BTreeSet<String> = match &after_op {
1478 None => std::collections::BTreeSet::new(),
1479 Some(cutoff) => {
1480 let op_log = match lex_vcs::OpLog::open(store.root()) {
1481 Ok(l) => l,
1482 Err(e) => return error_response(500, format!("opening op log: {e}")),
1483 };
1484 match op_log.walk_back(cutoff, None) {
1485 Ok(records) => records.into_iter().map(|r| r.op_id).collect(),
1486 Err(_) => {
1487 std::collections::BTreeSet::new()
1491 }
1492 }
1493 }
1494 };
1495
1496 let all = match log.list_all() {
1497 Ok(v) => v,
1498 Err(e) => return error_response(500, format!("listing attestations: {e}")),
1499 };
1500 let mut filtered: Vec<lex_vcs::Attestation> = all
1501 .into_iter()
1502 .filter(|a| match &a.op_id {
1503 Some(op_id) => !exclude.contains(op_id),
1504 None => true,
1508 })
1509 .collect();
1510 filtered.sort_by(|a, b| {
1514 a.timestamp.cmp(&b.timestamp)
1515 .then_with(|| a.attestation_id.cmp(&b.attestation_id))
1516 });
1517 if let Some(n) = limit {
1518 filtered.truncate(n);
1519 }
1520
1521 json_response(200, &serde_json::to_value(&filtered).unwrap_or_default())
1522}
1523
1524#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1528struct PkgRecord {
1529 name: String,
1530 version: String,
1531 head_op: Option<String>,
1532 published_at: u64,
1533 function_names: Vec<String>,
1535 ops: Vec<serde_json::Value>,
1537}
1538
1539#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
1546#[serde(rename_all = "lowercase")]
1547pub enum Visibility {
1548 #[default]
1549 Private,
1550 Public,
1551}
1552
1553#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
1558struct PkgIndex {
1559 latest: Option<String>,
1561 versions: Vec<PkgVersionSummary>,
1563 #[serde(default)]
1567 visibility: Visibility,
1568}
1569
1570#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1571struct PkgVersionSummary {
1572 version: String,
1573 head_op: Option<String>,
1574 published_at: u64,
1575}
1576
1577fn pkg_name_dir(root: &std::path::Path, name: &str) -> PathBuf {
1578 root.join("packages").join(name)
1579}
1580
1581fn pkg_index_path(root: &std::path::Path, name: &str) -> PathBuf {
1582 pkg_name_dir(root, name).join("index.json")
1583}
1584
1585fn pkg_version_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1586 pkg_name_dir(root, name).join(format!("{version}.json"))
1587}
1588
1589fn pkg_archive_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1590 pkg_name_dir(root, name).join(format!("{version}.tar.gz"))
1591}
1592
1593fn load_pkg_index(root: &std::path::Path, name: &str) -> Option<PkgIndex> {
1594 let bytes = std::fs::read(pkg_index_path(root, name)).ok()?;
1595 serde_json::from_slice(&bytes).ok()
1596}
1597
1598fn load_pkg_record(root: &std::path::Path, name: &str, version: &str) -> Option<PkgRecord> {
1599 let bytes = std::fs::read(pkg_version_path(root, name, version)).ok()?;
1600 serde_json::from_slice(&bytes).ok()
1601}
1602
1603fn load_latest_pkg_record(root: &std::path::Path, name: &str) -> Option<PkgRecord> {
1604 let index = load_pkg_index(root, name)?;
1605 let latest = index.latest.clone()?;
1606 load_pkg_record(root, name, &latest)
1607}
1608
1609fn pkg_is_public(root: &std::path::Path, name: &str) -> bool {
1613 load_pkg_index(root, name).map(|i| i.visibility) == Some(Visibility::Public)
1614}
1615
1616fn valid_pkg_segment(s: &str) -> bool {
1621 !s.is_empty()
1622 && s.len() <= 128
1623 && s != "."
1624 && s != ".."
1625 && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
1626}
1627
1628#[derive(Deserialize)]
1629struct VisibilityReq {
1630 visibility: Visibility,
1631}
1632
1633fn pkg_set_visibility_handler(
1640 state: &State,
1641 name: &str,
1642 body: &str,
1643) -> Response<std::io::Cursor<Vec<u8>>> {
1644 if !valid_pkg_segment(name) {
1645 return error_response(400, format!("invalid package name {name:?}"));
1646 }
1647 let req: VisibilityReq = match serde_json::from_str(body) {
1648 Ok(r) => r,
1649 Err(e) => return error_response(400, format!("bad request: {e}")),
1650 };
1651 let mut index = match load_pkg_index(&state.root, name) {
1652 Some(i) => i,
1653 None => return error_response(404, format!("package {name:?} not found")),
1654 };
1655 index.visibility = req.visibility;
1656 let bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1657 match std::fs::write(pkg_index_path(&state.root, name), bytes) {
1658 Ok(()) => json_response(
1659 200,
1660 &serde_json::json!({ "name": name, "visibility": index.visibility }),
1661 ),
1662 Err(e) => error_response(500, format!("write index: {e}")),
1663 }
1664}
1665
1666fn public_pkg_names(root: &std::path::Path) -> Vec<String> {
1669 list_pkg_names(root)
1670 .into_iter()
1671 .filter(|name| pkg_is_public(root, name))
1672 .collect()
1673}
1674
1675fn public_pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
1679 let packages: Vec<serde_json::Value> = public_pkg_names(&state.root)
1680 .iter()
1681 .filter_map(|name| {
1682 let r = load_latest_pkg_record(&state.root, name)?;
1683 Some(serde_json::json!({
1684 "name": r.name,
1685 "version": r.version,
1686 "head_op": r.head_op,
1687 "published_at": r.published_at,
1688 }))
1689 })
1690 .collect();
1691 json_response(200, &serde_json::json!({ "packages": packages }))
1692}
1693
1694#[derive(Debug, PartialEq, Eq)]
1699enum PublicTarget {
1700 List,
1701 Latest(String),
1702 Versions(String),
1703 Head(String),
1704 Version(String, String),
1705 Archive(String, String),
1706}
1707
1708impl PublicTarget {
1709 fn pkg_name(&self) -> Option<&str> {
1711 match self {
1712 PublicTarget::List => None,
1713 PublicTarget::Latest(n)
1714 | PublicTarget::Versions(n)
1715 | PublicTarget::Head(n)
1716 | PublicTarget::Version(n, _)
1717 | PublicTarget::Archive(n, _) => Some(n),
1718 }
1719 }
1720}
1721
1722fn resolve_public(method: &Method, path: &str) -> Result<PublicTarget, u16> {
1727 if !matches!(method, Method::Get) {
1728 return Err(405);
1729 }
1730 let rest = path.trim_matches('/');
1731 if rest.is_empty() {
1732 return Ok(PublicTarget::List);
1733 }
1734 let segs: Vec<&str> = rest.split('/').collect();
1735 if !segs.iter().all(|s| valid_pkg_segment(s)) {
1736 return Err(404);
1737 }
1738 match segs.as_slice() {
1739 [n] => Ok(PublicTarget::Latest(n.to_string())),
1740 [n, "versions"] => Ok(PublicTarget::Versions(n.to_string())),
1741 [n, "head"] => Ok(PublicTarget::Head(n.to_string())),
1742 [n, v, "archive"] => Ok(PublicTarget::Archive(n.to_string(), v.to_string())),
1743 [n, v] => Ok(PublicTarget::Version(n.to_string(), v.to_string())),
1744 _ => Err(404),
1745 }
1746}
1747
1748pub fn route_public(
1759 state: &State,
1760 method: &Method,
1761 path: &str,
1762 _query: &str,
1763) -> Response<std::io::Cursor<Vec<u8>>> {
1764 let target = match resolve_public(method, path) {
1765 Ok(t) => t,
1766 Err(405) => return error_response(405, "public read is GET-only"),
1767 Err(_) => return error_response(404, "not found"),
1768 };
1769 if let PublicTarget::List = target {
1771 return public_pkg_list_handler(state);
1772 }
1773 if let Some(name) = target.pkg_name() {
1775 if !pkg_is_public(&state.root, name) {
1776 return error_response(404, format!("package {name:?} not found"));
1777 }
1778 }
1779 match target {
1780 PublicTarget::List => unreachable!("handled above"),
1781 PublicTarget::Latest(n) => pkg_get_handler(state, &n),
1782 PublicTarget::Versions(n) => pkg_versions_handler(state, &n),
1783 PublicTarget::Head(n) => pkg_head_handler(state, &n),
1784 PublicTarget::Version(n, v) => pkg_get_version_handler(state, &n, &v),
1785 PublicTarget::Archive(n, v) => pkg_archive_handler(state, &n, &v),
1786 }
1787}
1788
1789fn save_pkg_record(
1790 root: &std::path::Path,
1791 record: &PkgRecord,
1792 archive: &[u8],
1793) -> std::io::Result<()> {
1794 let dir = pkg_name_dir(root, &record.name);
1795 std::fs::create_dir_all(&dir)?;
1796
1797 let rec_bytes = serde_json::to_vec_pretty(record).unwrap_or_default();
1799 std::fs::write(pkg_version_path(root, &record.name, &record.version), rec_bytes)?;
1800
1801 std::fs::write(pkg_archive_path(root, &record.name, &record.version), archive)?;
1803
1804 let mut index = load_pkg_index(root, &record.name).unwrap_or_default();
1806 index.latest = Some(record.version.clone());
1807 if !index.versions.iter().any(|v| v.version == record.version) {
1808 index.versions.push(PkgVersionSummary {
1809 version: record.version.clone(),
1810 head_op: record.head_op.clone(),
1811 published_at: record.published_at,
1812 });
1813 }
1814 let idx_bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1815 std::fs::write(pkg_index_path(root, &record.name), idx_bytes)
1816}
1817
1818fn list_pkg_names(root: &std::path::Path) -> Vec<String> {
1819 let dir = root.join("packages");
1820 let Ok(entries) = std::fs::read_dir(&dir) else {
1821 return Vec::new();
1822 };
1823 let mut names: Vec<String> = entries
1824 .filter_map(|e| e.ok())
1825 .filter(|e| e.path().is_dir())
1826 .filter_map(|e| e.file_name().into_string().ok())
1827 .collect();
1828 names.sort();
1829 names
1830}
1831
1832fn collect_lex_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
1833 let Ok(entries) = std::fs::read_dir(dir) else { return };
1834 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
1835 entries.sort_by_key(|e| e.path());
1836 for entry in entries {
1837 let path = entry.path();
1838 if path.is_dir() {
1839 collect_lex_files(&path, out);
1840 } else if path.extension().and_then(|x| x.to_str()) == Some("lex") {
1841 out.push(path);
1842 }
1843 }
1844}
1845
1846fn pkg_publish_handler(state: &State, body: &[u8]) -> Response<std::io::Cursor<Vec<u8>>> {
1849 let tmp = match tempfile::TempDir::new() {
1850 Ok(t) => t,
1851 Err(e) => return error_response(500, format!("create temp dir: {e}")),
1852 };
1853 {
1854 let gz = flate2::read::GzDecoder::new(std::io::Cursor::new(body));
1855 let mut ar = tar::Archive::new(gz);
1856 if let Err(e) = ar.unpack(tmp.path()) {
1857 return error_response(400, format!("unpack archive: {e}"));
1858 }
1859 }
1860
1861 let toml_path = tmp.path().join("lex.toml");
1862 if !toml_path.exists() {
1863 return error_response(400, "archive must contain lex.toml at root");
1864 }
1865 let manifest = match Manifest::load(&toml_path) {
1866 Ok(m) => m,
1867 Err(e) => return error_response(400, format!("lex.toml: {e}")),
1868 };
1869 let (pkg_name, pkg_version) = match &manifest.package {
1870 Some(m) => (m.name.clone(), m.version.clone()),
1871 None => return error_response(400, "lex.toml must have a [package] section"),
1872 };
1873
1874 let src_dir = tmp.path().join("src");
1875 if !src_dir.exists() {
1876 return error_response(400, "archive must contain a src/ directory");
1877 }
1878 let mut lex_files: Vec<PathBuf> = Vec::new();
1879 collect_lex_files(&src_dir, &mut lex_files);
1880 if lex_files.is_empty() {
1881 return error_response(400, "no .lex files found in src/");
1882 }
1883
1884 let store = state.store.lock().unwrap();
1885 let branch = store.current_branch();
1886
1887 let mut all_ops: Vec<serde_json::Value> = Vec::new();
1888 let mut final_head_op: Option<String> = None;
1889 let mut all_function_names: Vec<String> = Vec::new();
1890
1891 for lex_path in &lex_files {
1892 let prog = match load_program(lex_path) {
1893 Ok(p) => p,
1894 Err(e) => return error_response(400, format!("load {}: {e}", lex_path.display())),
1895 };
1896 let mut stages = canonicalize_program(&prog);
1897 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
1898 return error_with_detail(
1899 422,
1900 format!("type errors in {}", lex_path.display()),
1901 serde_json::to_value(&errs).unwrap(),
1902 );
1903 }
1904
1905 let old_head = match store.branch_head(&branch) {
1906 Ok(h) => h,
1907 Err(e) => return error_response(500, format!("branch_head: {e}")),
1908 };
1909 let old_fns: BTreeMap<String, lex_ast::FnDecl> = old_head.values()
1910 .filter_map(|stg| store.get_ast(stg).ok())
1911 .filter_map(|s| match s {
1912 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd)),
1913 _ => None,
1914 })
1915 .collect();
1916 let new_fns: BTreeMap<String, lex_ast::FnDecl> = stages.iter()
1917 .filter_map(|s| match s {
1918 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
1919 _ => None,
1920 })
1921 .collect();
1922
1923 for name in new_fns.keys() {
1924 if !all_function_names.contains(name) {
1925 all_function_names.push(name.clone());
1926 }
1927 }
1928
1929 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
1930
1931 let file_key = lex_path
1932 .strip_prefix(tmp.path())
1933 .unwrap_or(lex_path)
1934 .display()
1935 .to_string();
1936 let mut new_imports = lex_vcs::ImportMap::new();
1937 {
1938 let entry = new_imports.entry(file_key).or_default();
1939 for s in &stages {
1940 if let lex_ast::Stage::Import(im) = s {
1941 entry.insert(im.reference.clone());
1942 }
1943 }
1944 }
1945
1946 match store.publish_program(&branch, &stages, &report, &new_imports, false) {
1947 Ok(outcome) => {
1948 let ops_json = serde_json::to_value(&outcome.ops).unwrap_or_default();
1949 if let serde_json::Value::Array(arr) = ops_json {
1950 all_ops.extend(arr);
1951 }
1952 if let Some(h) = outcome.head_op {
1953 final_head_op = Some(h);
1954 }
1955 }
1956 Err(lex_store::StoreError::TypeError(errs)) => {
1957 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
1958 }
1959 Err(e) => return write_error_response("publish_program", e),
1960 }
1961 }
1962
1963 if load_pkg_record(&state.root, &pkg_name, &pkg_version).is_some() {
1965 return error_response(
1966 409,
1967 format!(
1968 "package {pkg_name}@{pkg_version} already published; \
1969 bump the version in lex.toml to publish a new release"
1970 ),
1971 );
1972 }
1973
1974 let now = SystemTime::now()
1975 .duration_since(UNIX_EPOCH)
1976 .map(|d| d.as_secs())
1977 .unwrap_or(0);
1978 let record = PkgRecord {
1979 name: pkg_name.clone(),
1980 version: pkg_version,
1981 head_op: final_head_op.clone(),
1982 published_at: now,
1983 function_names: all_function_names,
1984 ops: all_ops.clone(),
1985 };
1986 if let Err(e) = save_pkg_record(&state.root, &record, body) {
1987 return error_response(500, format!("save package index: {e}"));
1988 }
1989
1990 json_response(200, &serde_json::json!({
1991 "package": pkg_name,
1992 "ops": all_ops,
1993 "head_op": final_head_op,
1994 }))
1995}
1996
1997fn pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
1999 let names = list_pkg_names(&state.root);
2000 let packages: Vec<serde_json::Value> = names.iter()
2001 .filter_map(|name| {
2002 let idx = load_pkg_index(&state.root, name)?;
2003 let latest = idx.latest.as_deref()?;
2004 let r = load_pkg_record(&state.root, name, latest)?;
2005 Some(serde_json::json!({
2006 "name": r.name,
2007 "version": r.version,
2008 "head_op": r.head_op,
2009 "published_at": r.published_at,
2010 }))
2011 })
2012 .collect();
2013 json_response(200, &serde_json::json!({ "packages": packages }))
2014}
2015
2016fn pkg_get_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2018 match load_latest_pkg_record(&state.root, name) {
2019 Some(r) => json_response(200, &serde_json::json!({
2020 "name": r.name,
2021 "version": r.version,
2022 "head_op": r.head_op,
2023 "published_at": r.published_at,
2024 "function_names": r.function_names,
2025 "ops": r.ops,
2026 })),
2027 None => error_response(404, format!("package {name:?} not found")),
2028 }
2029}
2030
2031fn pkg_versions_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2033 match load_pkg_index(&state.root, name) {
2034 Some(idx) => json_response(200, &serde_json::json!({
2035 "name": name,
2036 "latest": idx.latest,
2037 "versions": idx.versions,
2038 })),
2039 None => error_response(404, format!("package {name:?} not found")),
2040 }
2041}
2042
2043fn pkg_get_version_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2045 match load_pkg_record(&state.root, name, version) {
2046 Some(r) => json_response(200, &serde_json::json!({
2047 "name": r.name,
2048 "version": r.version,
2049 "head_op": r.head_op,
2050 "published_at": r.published_at,
2051 "function_names": r.function_names,
2052 "ops": r.ops,
2053 })),
2054 None => error_response(404, format!("package {name:?}@{version:?} not found")),
2055 }
2056}
2057
2058fn pkg_archive_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2060 let path = pkg_archive_path(&state.root, name, version);
2061 match std::fs::read(&path) {
2062 Ok(bytes) => Response::from_data(bytes)
2063 .with_status_code(200)
2064 .with_header(
2065 tiny_http::Header::from_bytes(
2066 &b"Content-Type"[..],
2067 &b"application/gzip"[..],
2068 )
2069 .unwrap(),
2070 ),
2071 Err(_) => error_response(404, format!("archive for {name:?}@{version:?} not found")),
2072 }
2073}
2074
2075fn pkg_head_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2077 match load_latest_pkg_record(&state.root, name) {
2078 Some(r) => json_response(200, &serde_json::json!({
2079 "name": r.name,
2080 "version": r.version,
2081 "head_op": r.head_op,
2082 })),
2083 None => error_response(404, format!("package {name:?} not found")),
2084 }
2085}
2086
2087fn pkg_delete_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2089 let record = match load_latest_pkg_record(&state.root, name) {
2090 Some(r) => r,
2091 None => return error_response(404, format!("package {name:?} not found")),
2092 };
2093
2094 let store = state.store.lock().unwrap();
2095 let branch = store.current_branch();
2096
2097 let head = match store.branch_head(&branch) {
2098 Ok(h) => h,
2099 Err(e) => return error_response(500, format!("branch_head: {e}")),
2100 };
2101
2102 let old_fns: BTreeMap<String, lex_ast::FnDecl> = head.values()
2104 .filter_map(|stage_id| store.get_ast(stage_id).ok())
2105 .filter_map(|s| match s {
2106 lex_ast::Stage::FnDecl(fd)
2107 if record.function_names.contains(&fd.name) => Some((fd.name.clone(), fd)),
2108 _ => None,
2109 })
2110 .collect();
2111
2112 let new_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2113 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
2114 let empty_imports = lex_vcs::ImportMap::new();
2115
2116 match store.publish_program(&branch, &[], &report, &empty_imports, false) {
2117 Ok(outcome) => {
2118 let ver = record.version.clone();
2120 let _ = std::fs::remove_file(pkg_version_path(&state.root, name, &ver));
2121 let _ = std::fs::remove_file(pkg_archive_path(&state.root, name, &ver));
2122 if let Some(mut idx) = load_pkg_index(&state.root, name) {
2124 idx.versions.retain(|v| v.version != ver);
2125 idx.latest = idx.versions.last().map(|v| v.version.clone());
2126 if idx.versions.is_empty() {
2127 let _ = std::fs::remove_dir_all(pkg_name_dir(&state.root, name));
2128 } else {
2129 let bytes = serde_json::to_vec_pretty(&idx).unwrap_or_default();
2130 let _ = std::fs::write(pkg_index_path(&state.root, name), bytes);
2131 }
2132 }
2133 json_response(200, &serde_json::json!({
2134 "deleted": name,
2135 "version": ver,
2136 "ops": outcome.ops,
2137 "head_op": outcome.head_op,
2138 }))
2139 }
2140 Err(lex_store::StoreError::TypeError(errs)) => {
2141 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
2142 }
2143 Err(e) => write_error_response("retract package", e),
2144 }
2145}
2146
2147#[cfg(test)]
2148mod policy_ceiling_tests {
2149 use super::*;
2150 use lex_runtime::Policy;
2151 use std::path::PathBuf;
2152
2153 fn permissive_request() -> Policy {
2157 Policy {
2158 allow_effects: ["io", "fs_read", "fs_write", "net", "proc"]
2159 .iter()
2160 .map(|s| s.to_string())
2161 .collect(),
2162 allow_fs_read: vec![PathBuf::from("/")],
2163 allow_fs_write: vec![PathBuf::from("/")],
2164 allow_net_host: Vec::new(),
2165 allow_proc: Vec::new(),
2166 budget: None,
2167 }
2168 }
2169
2170 #[test]
2171 fn ceiling_drops_effects_the_caller_was_not_granted() {
2172 let ceiling = Policy {
2173 allow_effects: ["io", "time"].iter().map(|s| s.to_string()).collect(),
2174 ..Policy::default()
2175 };
2176 let got = clamp_policy(permissive_request(), &ceiling);
2177 assert!(got.allow_effects.contains("io"));
2178 assert!(!got.allow_effects.contains("proc"), "proc must not survive a ceiling without it");
2179 assert!(!got.allow_effects.contains("fs_write"));
2180 assert!(!got.allow_effects.contains("net"));
2181 assert!(!got.allow_effects.contains("time"));
2183 }
2184
2185 #[test]
2186 fn ceiling_scopes_override_caller_scopes() {
2187 let ceiling = Policy {
2188 allow_effects: ["fs_read"].iter().map(|s| s.to_string()).collect(),
2189 allow_fs_read: vec![PathBuf::from("/srv/tenant")],
2190 ..Policy::default()
2191 };
2192 let got = clamp_policy(permissive_request(), &ceiling);
2193 assert_eq!(got.allow_fs_read, vec![PathBuf::from("/srv/tenant")]);
2196 assert!(got.allow_fs_write.is_empty());
2197 assert!(got.allow_proc.is_empty());
2198 assert!(got.allow_net_host.is_empty());
2199 }
2200
2201 #[test]
2202 fn ceiling_caps_budget_and_prefers_the_smaller() {
2203 let mut req = permissive_request();
2205 req.budget = None;
2206 let ceiling = Policy { budget: Some(1_000), ..Policy::default() };
2207 assert_eq!(clamp_policy(req, &ceiling).budget, Some(1_000));
2208
2209 let mut req2 = permissive_request();
2211 req2.budget = Some(50);
2212 let ceiling2 = Policy { budget: Some(1_000), ..Policy::default() };
2213 assert_eq!(clamp_policy(req2, &ceiling2).budget, Some(50));
2214 }
2215
2216 #[test]
2217 fn empty_ceiling_is_pure_only() {
2218 let got = clamp_policy(permissive_request(), &Policy::default());
2219 assert!(got.allow_effects.is_empty(), "an empty ceiling grants nothing");
2220 assert!(got.allow_proc.is_empty());
2221 assert!(got.allow_fs_write.is_empty());
2222 }
2223}
2224
2225#[cfg(test)]
2226mod public_read_tests {
2227 use super::*;
2228
2229 fn seed_pkg(root: &std::path::Path, name: &str, version: &str) {
2232 let record = PkgRecord {
2233 name: name.to_string(),
2234 version: version.to_string(),
2235 head_op: Some(format!("op-{name}")),
2236 published_at: 1,
2237 function_names: vec![format!("{name}.f")],
2238 ops: vec![],
2239 };
2240 save_pkg_record(root, &record, format!("ARCHIVE:{name}@{version}").as_bytes())
2241 .expect("seed package");
2242 }
2243
2244 #[test]
2245 fn new_package_defaults_to_private() {
2246 let tmp = tempfile::TempDir::new().unwrap();
2247 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2248 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2249 assert!(!pkg_is_public(tmp.path(), "does-not-exist"));
2251 }
2252
2253 #[test]
2254 fn set_visibility_round_trips_and_index_persists() {
2255 let tmp = tempfile::TempDir::new().unwrap();
2256 let state = State::open(tmp.path().to_path_buf()).unwrap();
2257 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2258
2259 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"public"}"#);
2260 assert!(pkg_is_public(tmp.path(), "lex-schema"));
2261 let idx = load_pkg_index(tmp.path(), "lex-schema").unwrap();
2263 assert_eq!(idx.latest.as_deref(), Some("0.9.2"));
2264 assert_eq!(idx.versions.len(), 1);
2265
2266 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"private"}"#);
2267 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2268 }
2269
2270 #[test]
2271 fn set_visibility_on_unknown_package_is_a_noop() {
2272 let tmp = tempfile::TempDir::new().unwrap();
2273 let state = State::open(tmp.path().to_path_buf()).unwrap();
2274 let _ = pkg_set_visibility_handler(&state, "ghost", r#"{"visibility":"public"}"#);
2276 assert!(load_pkg_index(tmp.path(), "ghost").is_none());
2277 }
2278
2279 #[test]
2280 fn public_listing_omits_private_packages() {
2281 let tmp = tempfile::TempDir::new().unwrap();
2282 let state = State::open(tmp.path().to_path_buf()).unwrap();
2283 seed_pkg(tmp.path(), "pub-pkg", "1.0.0");
2284 seed_pkg(tmp.path(), "priv-pkg", "1.0.0");
2285 let _ = pkg_set_visibility_handler(&state, "pub-pkg", r#"{"visibility":"public"}"#);
2286
2287 let names = public_pkg_names(tmp.path());
2288 assert_eq!(names, vec!["pub-pkg".to_string()]);
2289 }
2290
2291 #[test]
2292 fn resolve_public_maps_routes() {
2293 let get = Method::Get;
2294 assert_eq!(resolve_public(&get, "").unwrap(), PublicTarget::List);
2295 assert_eq!(resolve_public(&get, "/").unwrap(), PublicTarget::List);
2296 assert_eq!(
2297 resolve_public(&get, "/lex-schema").unwrap(),
2298 PublicTarget::Latest("lex-schema".into())
2299 );
2300 assert_eq!(
2301 resolve_public(&get, "/lex-schema/versions").unwrap(),
2302 PublicTarget::Versions("lex-schema".into())
2303 );
2304 assert_eq!(
2305 resolve_public(&get, "/lex-schema/head").unwrap(),
2306 PublicTarget::Head("lex-schema".into())
2307 );
2308 assert_eq!(
2309 resolve_public(&get, "/lex-schema/0.9.2").unwrap(),
2310 PublicTarget::Version("lex-schema".into(), "0.9.2".into())
2311 );
2312 assert_eq!(
2313 resolve_public(&get, "/lex-schema/0.9.2/archive").unwrap(),
2314 PublicTarget::Archive("lex-schema".into(), "0.9.2".into())
2315 );
2316 }
2317
2318 #[test]
2319 fn resolve_public_rejects_bad_method_and_traversal() {
2320 assert_eq!(resolve_public(&Method::Put, "/lex-schema"), Err(405));
2322 assert_eq!(resolve_public(&Method::Post, "").err(), Some(405));
2323 assert_eq!(resolve_public(&Method::Get, "/.."), Err(404));
2325 assert_eq!(resolve_public(&Method::Get, "/lex-schema/../etc"), Err(404));
2326 assert_eq!(resolve_public(&Method::Get, "/a/b/c/d"), Err(404));
2327 assert!(resolve_public(&Method::Get, "/lex schema").is_err());
2329 }
2330}