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_package, 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 allow_approval: ceiling.allow_approval.clone(),
154 budget,
155 }
156}
157
158fn validate_tenant_id(tenant_id: &str) -> anyhow::Result<()> {
159 if tenant_id.is_empty() {
160 anyhow::bail!("tenant_id must not be empty");
161 }
162 if tenant_id.len() > 64 {
163 anyhow::bail!("tenant_id must be at most 64 bytes");
164 }
165 if !tenant_id
166 .bytes()
167 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
168 {
169 anyhow::bail!(
170 "tenant_id {tenant_id:?} contains characters outside [A-Za-z0-9_-]"
171 );
172 }
173 Ok(())
174}
175
176#[derive(Debug, Serialize, Deserialize)]
177struct ErrorEnvelope {
178 error: String,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 detail: Option<serde_json::Value>,
181}
182
183fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
184 let bytes = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
185 Response::from_data(bytes)
186 .with_status_code(status)
187 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
188}
189
190fn error_response(status: u16, msg: impl Into<String>) -> Response<std::io::Cursor<Vec<u8>>> {
191 json_response(status, &serde_json::to_value(ErrorEnvelope {
192 error: msg.into(), detail: None,
193 }).unwrap())
194}
195
196fn error_with_detail(status: u16, msg: impl Into<String>, detail: serde_json::Value)
197 -> Response<std::io::Cursor<Vec<u8>>>
198{
199 json_response(status, &serde_json::to_value(ErrorEnvelope {
200 error: msg.into(), detail: Some(detail),
201 }).unwrap())
202}
203
204fn write_error_response(prefix: &str, err: lex_store::StoreError)
210 -> Response<std::io::Cursor<Vec<u8>>>
211{
212 if let lex_store::StoreError::Contention { branch, attempts } = &err {
213 let body = serde_json::to_vec(&ErrorEnvelope {
214 error: format!("{prefix}: branch '{branch}' is contended (attempts={attempts})"),
215 detail: Some(serde_json::json!({
216 "kind": "contention",
217 "branch": branch,
218 "attempts": attempts,
219 })),
220 }).unwrap_or_else(|_| b"{}".to_vec());
221 return Response::from_data(body)
222 .with_status_code(503)
223 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
224 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"1"[..]).unwrap());
225 }
226 if let lex_store::StoreError::BudgetExceeded { session_id, cap, spent_after } = &err {
234 let body = serde_json::to_vec(&ErrorEnvelope {
235 error: format!(
236 "{prefix}: session `{session_id}` budget exceeded \
237 (spent_after={spent_after}, cap={cap})"
238 ),
239 detail: Some(serde_json::json!({
240 "kind": "budget_exceeded",
241 "session_id": session_id,
242 "cap": cap,
243 "spent_after": spent_after,
244 })),
245 }).unwrap_or_else(|_| b"{}".to_vec());
246 return Response::from_data(body)
247 .with_status_code(503)
248 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
249 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"0"[..]).unwrap());
250 }
251 error_response(500, format!("{prefix}: {err}"))
252}
253
254pub fn handle(state: Arc<State>, mut req: Request) -> std::io::Result<()> {
255 let method = req.method().clone();
256 let url = req.url().to_string();
257 let path = url.split('?').next().unwrap_or("").to_string();
258 let query = url.split_once('?').map(|(_, q)| q.to_string()).unwrap_or_default();
259
260 let x_lex_user = req.headers().iter()
265 .find(|h| h.field.equiv("x-lex-user"))
266 .map(|h| h.value.as_str().to_string());
267
268 if matches!(method, Method::Post) && path == "/v1/pkg/publish" {
270 let mut body_bytes: Vec<u8> = Vec::new();
271 let _ = req.as_reader().read_to_end(&mut body_bytes);
272 let resp = pkg_publish_handler(&state, &body_bytes);
273 return req.respond(resp);
274 }
275
276 let mut body = String::new();
277 let _ = req.as_reader().read_to_string(&mut body);
278
279 let resp = route(&state, &method, &path, &query, &body, x_lex_user.as_deref());
280 req.respond(resp)
281}
282
283pub fn handle_with_auth<F>(state: Arc<State>, req: Request, auth: F) -> std::io::Result<()>
287where
288 F: FnOnce(&str, &[Header]) -> bool,
289{
290 let path = req.url().split('?').next().unwrap_or("").to_string();
291 if !auth(&path, req.headers()) {
292 return req.respond(
293 Response::from_data(br#"{"error":"unauthorized"}"#.to_vec())
294 .with_status_code(401)
295 .with_header(
296 Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
297 ),
298 );
299 }
300 handle(state, req)
301}
302
303fn route(
304 state: &State,
305 method: &Method,
306 path: &str,
307 query: &str,
308 body: &str,
309 x_lex_user: Option<&str>,
310) -> Response<std::io::Cursor<Vec<u8>>> {
311 match (method, path) {
312 (Method::Get, "/") => crate::web::activity_handler(state),
314 (Method::Get, "/web/branches") => crate::web::branches_handler(state),
315 (Method::Get, "/web/trust") => crate::web::trust_handler(state),
316 (Method::Get, "/web/attention") => crate::web::attention_handler(state),
317 (Method::Get, p) if p.starts_with("/web/branch/") => {
318 let name = &p["/web/branch/".len()..];
319 crate::web::branch_handler(state, name)
320 }
321 (Method::Get, p) if p.starts_with("/web/stage/") => {
322 let id = &p["/web/stage/".len()..];
323 crate::web::stage_html_handler(state, id)
324 }
325 (Method::Post, p) if p.starts_with("/web/stage/") && (
330 p.ends_with("/pin") || p.ends_with("/defer")
331 || p.ends_with("/block") || p.ends_with("/unblock")
332 ) => {
333 let prefix_len = "/web/stage/".len();
334 let last_slash = p.rfind('/').unwrap_or(p.len());
335 let id = &p[prefix_len..last_slash];
336 let verb = &p[last_slash + 1..];
337 let decision = match verb {
338 "pin" => crate::web::WebStageDecision::Pin,
339 "defer" => crate::web::WebStageDecision::Defer,
340 "block" => crate::web::WebStageDecision::Block,
341 "unblock" => crate::web::WebStageDecision::Unblock,
342 _ => unreachable!("matched in outer guard"),
343 };
344 crate::web::stage_decision_handler(state, id, body, decision, x_lex_user)
345 }
346 (Method::Get, "/v1/health") => json_response(200, &serde_json::json!({"ok": true})),
348 (Method::Post, "/v1/parse") => parse_handler(body),
349 (Method::Post, "/v1/check") => check_handler(body),
350 (Method::Post, "/v1/publish") => publish_handler(state, body),
351 (Method::Post, "/v1/patch") => patch_handler(state, body),
352 (Method::Get, p) if p.starts_with("/v1/stage/") => {
353 let suffix = &p["/v1/stage/".len()..];
354 if let Some(id) = suffix.strip_suffix("/attestations") {
357 stage_attestations_handler(state, id)
358 } else {
359 stage_handler(state, suffix)
360 }
361 }
362 (Method::Post, "/v1/run") => run_handler(state, body, false),
363 (Method::Post, "/v1/replay") => run_handler(state, body, true),
364 (Method::Get, p) if p.starts_with("/v1/trace/") => {
365 let id = &p["/v1/trace/".len()..];
366 trace_handler(state, id)
367 }
368 (Method::Get, "/v1/diff") => diff_handler(state, query),
369 (Method::Post, "/v1/merge/start") => merge_start_handler(state, body),
370 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/resolve") => {
371 let id = &p["/v1/merge/".len()..p.len() - "/resolve".len()];
372 merge_resolve_handler(state, id, body)
373 }
374 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/commit") => {
375 let id = &p["/v1/merge/".len()..p.len() - "/commit".len()];
376 merge_commit_handler(state, id)
377 }
378 (Method::Post, "/v1/ops/batch") => ops_batch_handler(state, body),
380 (Method::Post, "/v1/attestations/batch") => attestations_batch_handler(state, body),
381 (Method::Get, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
385 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
386 branch_head_handler(state, name)
387 }
388 (Method::Get, "/v1/ops/since") => ops_since_handler(state, query),
392 (Method::Get, "/v1/attestations/since") => attestations_since_handler(state, query),
393 (Method::Get, "/v1/pkg") => pkg_list_handler(state),
397 (Method::Put, p) if p.starts_with("/v1/pkg/") && p.ends_with("/visibility") => {
401 let name = &p["/v1/pkg/".len()..p.len() - "/visibility".len()];
402 pkg_set_visibility_handler(state, name, body)
403 }
404 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/head") => {
405 let name = &p["/v1/pkg/".len()..p.len() - "/head".len()];
406 pkg_head_handler(state, name)
407 }
408 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/versions") => {
409 let name = &p["/v1/pkg/".len()..p.len() - "/versions".len()];
410 pkg_versions_handler(state, name)
411 }
412 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/archive") => {
414 let inner = &p["/v1/pkg/".len()..p.len() - "/archive".len()];
415 if let Some((name, version)) = inner.split_once('/') {
417 pkg_archive_handler(state, name, version)
418 } else {
419 error_response(400, "expected /v1/pkg/{name}/{version}/archive")
420 }
421 }
422 (Method::Get, p) if p.starts_with("/v1/pkg/") && p["/v1/pkg/".len()..].contains('/') => {
424 let inner = &p["/v1/pkg/".len()..];
425 if let Some((name, version)) = inner.split_once('/') {
426 pkg_get_version_handler(state, name, version)
427 } else {
428 error_response(400, "expected /v1/pkg/{name}/{version}")
429 }
430 }
431 (Method::Get, p) if p.starts_with("/v1/pkg/") => {
432 let name = &p["/v1/pkg/".len()..];
433 pkg_get_handler(state, name)
434 }
435 (Method::Delete, p) if p.starts_with("/v1/pkg/") => {
436 let name = &p["/v1/pkg/".len()..];
437 pkg_delete_handler(state, name)
438 }
439 _ => error_response(404, format!("unknown route: {method:?} {path}")),
440 }
441}
442
443#[derive(Deserialize)]
444struct ParseReq { source: String }
445
446fn parse_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
447 let req: ParseReq = match serde_json::from_str(body) {
448 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
449 };
450 match load_program_from_str(&req.source) {
451 Ok(prog) => {
452 let stages = canonicalize_program(&prog);
453 json_response(200, &serde_json::to_value(&stages).unwrap())
454 }
455 Err(e) => error_response(400, format!("syntax error: {e}")),
456 }
457}
458
459pub(crate) fn check_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
460 let req: ParseReq = match serde_json::from_str(body) {
461 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
462 };
463 let prog = match load_program_from_str(&req.source) {
464 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
465 };
466 let stages = canonicalize_program(&prog);
467 match lex_types::check_program(&stages) {
468 Ok(_) => json_response(200, &serde_json::json!({"ok": true})),
469 Err(errs) => json_response(422, &serde_json::to_value(&errs).unwrap()),
470 }
471}
472
473#[derive(Deserialize)]
474struct PublishReq { source: String, #[serde(default)] activate: bool }
475
476pub(crate) fn publish_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
477 let req: PublishReq = match serde_json::from_str(body) {
478 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
479 };
480 let prog = match load_program_from_str(&req.source) {
481 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
482 };
483 let mut stages = canonicalize_program(&prog);
487 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
488 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
489 }
490
491 let store = state.store.lock().unwrap();
492 let branch = store.current_branch();
493
494 let old_head = match store.branch_head(&branch) {
496 Ok(h) => h,
497 Err(e) => return error_response(500, format!("branch_head: {e}")),
498 };
499 let old_fns: std::collections::BTreeMap<String, lex_ast::FnDecl> = old_head.values()
500 .filter_map(|stg| store.get_ast(stg).ok())
501 .filter_map(|s| match s {
502 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd)),
503 _ => None,
504 })
505 .collect();
506 let new_fns: std::collections::BTreeMap<String, lex_ast::FnDecl> = stages.iter()
507 .filter_map(|s| match s {
508 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
509 _ => None,
510 })
511 .collect();
512 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
513
514 let mut new_imports: lex_vcs::ImportMap = lex_vcs::ImportMap::new();
516 {
517 let entry = new_imports.entry("<source>".into()).or_default();
518 for s in &stages {
519 if let lex_ast::Stage::Import(im) = s {
520 entry.insert(im.reference.clone());
521 }
522 }
523 }
524
525 match store.publish_program(&branch, &stages, &report, &new_imports, req.activate) {
526 Ok(outcome) => json_response(200, &serde_json::json!({
527 "ops": outcome.ops,
528 "head_op": outcome.head_op,
529 })),
530 Err(lex_store::StoreError::TypeError(errs)) => {
538 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
539 }
540 Err(e) => write_error_response("publish_program", e),
541 }
542}
543
544#[derive(Deserialize)]
545struct PatchReq {
546 stage_id: String,
547 patch: lex_ast::Patch,
548 #[serde(default)] activate: bool,
549}
550
551fn patch_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
554 let req: PatchReq = match serde_json::from_str(body) {
555 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
556 };
557 let store = state.store.lock().unwrap();
558
559 let original = match store.get_ast(&req.stage_id) {
561 Ok(s) => s, Err(e) => return error_response(404, format!("stage: {e}")),
562 };
563
564 let patched = match lex_ast::apply_patch(&original, &req.patch) {
566 Ok(s) => s,
567 Err(e) => return error_with_detail(422, "patch failed",
568 serde_json::to_value(&e).unwrap_or_default()),
569 };
570
571 let branch = store.current_branch();
582
583 let sig = match lex_ast::sig_id(&patched) {
585 Some(s) => s,
586 None => return error_response(500, "patched stage has no sig_id"),
587 };
588
589 let new_id = match store.publish(&patched) {
592 Ok(id) => id, Err(e) => return error_response(500, format!("publish: {e}")),
593 };
594
595 let original_effects: std::collections::BTreeSet<String> = match &original {
597 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
598 _ => std::collections::BTreeSet::new(),
599 };
600 let patched_effects: std::collections::BTreeSet<String> = match &patched {
601 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
602 _ => std::collections::BTreeSet::new(),
603 };
604 let head_now = match store.get_branch(&branch) {
605 Ok(b) => b.and_then(|b| b.head_op),
606 Err(e) => return error_response(500, format!("get_branch: {e}")),
607 };
608 let kind = if original_effects != patched_effects {
609 let from_budget = lex_vcs::operation_budget_from_effects(&original_effects);
616 let to_budget = lex_vcs::operation_budget_from_effects(&patched_effects);
617 lex_vcs::OperationKind::ChangeEffectSig {
618 sig_id: sig.clone(),
619 from_stage_id: req.stage_id.clone(),
620 to_stage_id: new_id.clone(),
621 from_effects: original_effects,
622 to_effects: patched_effects,
623 from_budget,
624 to_budget,
625 }
626 } else {
627 let budget = lex_vcs::operation_budget_from_effects(&original_effects);
628 lex_vcs::OperationKind::ModifyBody {
629 sig_id: sig.clone(),
630 from_stage_id: req.stage_id.clone(),
631 to_stage_id: new_id.clone(),
632 from_budget: budget,
633 to_budget: budget,
634 }
635 };
636 let transition = lex_vcs::StageTransition::Replace {
637 sig_id: sig.clone(),
638 from: req.stage_id.clone(),
639 to: new_id.clone(),
640 };
641 let op = lex_vcs::Operation::new(
642 kind,
643 head_now.into_iter().collect::<Vec<_>>(),
644 );
645 let op_id = match store.apply_operation_gated(&branch, op, transition) {
646 Ok(id) => id,
647 Err(lex_store::StoreError::TypeError(errs)) => return error_with_detail(
648 422, "type errors after patch", serde_json::to_value(&errs).unwrap_or_default()),
649 Err(e) => return write_error_response("apply_operation_gated", e),
650 };
651 if req.activate {
652 if let Err(e) = store.activate(&new_id) {
653 return error_response(500, format!("activate: {e}"));
654 }
655 }
656
657 let status = format!("{:?}",
658 store.get_status(&new_id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
659 json_response(200, &serde_json::json!({
660 "old_stage_id": req.stage_id,
661 "new_stage_id": new_id,
662 "sig_id": sig,
663 "status": status,
664 "op_id": op_id,
665 }))
666}
667
668pub(crate) fn stage_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
669 let store = state.store.lock().unwrap();
670 let meta = match store.get_metadata(id) {
671 Ok(m) => m, Err(e) => return error_response(404, format!("{e}")),
672 };
673 let ast = match store.get_ast(id) {
674 Ok(a) => a, Err(e) => return error_response(404, format!("{e}")),
675 };
676 let status = format!("{:?}", store.get_status(id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
677 json_response(200, &serde_json::json!({
678 "metadata": meta,
679 "ast": ast,
680 "status": status,
681 }))
682}
683
684pub(crate) fn stage_attestations_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
693 let store = state.store.lock().unwrap();
694 if let Err(e) = store.get_metadata(id) {
695 return error_response(404, format!("{e}"));
696 }
697 let log = match store.attestation_log() {
698 Ok(l) => l,
699 Err(e) => return error_response(500, format!("attestation log: {e}")),
700 };
701 let mut listing = match log.list_for_stage(&id.to_string()) {
702 Ok(v) => v,
703 Err(e) => return error_response(500, format!("list_for_stage: {e}")),
704 };
705 listing.sort_by_key(|a| std::cmp::Reverse(a.timestamp));
706 json_response(200, &serde_json::json!({"attestations": listing}))
707}
708
709#[derive(Deserialize, Default)]
710struct PolicyJson {
711 #[serde(default)] allow_effects: Vec<String>,
712 #[serde(default)] allow_fs_read: Vec<String>,
713 #[serde(default)] allow_fs_write: Vec<String>,
714 #[serde(default)] budget: Option<u64>,
715}
716
717impl PolicyJson {
718 fn into_policy(self) -> Policy {
719 Policy {
720 allow_effects: self.allow_effects.into_iter().collect::<BTreeSet<_>>(),
721 allow_fs_read: self.allow_fs_read.into_iter().map(PathBuf::from).collect(),
722 allow_fs_write: self.allow_fs_write.into_iter().map(PathBuf::from).collect(),
723 allow_net_host: Vec::new(),
724 allow_proc: Vec::new(),
725 allow_approval: Vec::new(),
726 budget: self.budget,
727 }
728 }
729}
730
731#[derive(Deserialize)]
732struct RunReq {
733 source: String,
734 #[serde(rename = "fn")] func: String,
735 #[serde(default)] args: Vec<serde_json::Value>,
736 #[serde(default)] policy: PolicyJson,
737 #[serde(default)] overrides: IndexMap<String, serde_json::Value>,
738}
739
740pub(crate) fn run_handler(state: &State, body: &str, with_overrides: bool) -> Response<std::io::Cursor<Vec<u8>>> {
741 let req: RunReq = match serde_json::from_str(body) {
742 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
743 };
744 let prog = match load_program_from_str(&req.source) {
745 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
746 };
747 let stages = canonicalize_program(&prog);
748 if let Err(errs) = lex_types::check_program(&stages) {
749 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
750 }
751 let bc = compile_program(&stages);
752 let mut policy = req.policy.into_policy();
753 if let Some(ceiling) = &state.policy_ceiling {
759 policy = clamp_policy(policy, ceiling);
760 }
761 if let Err(violations) = check_policy(&bc, &policy) {
762 return error_with_detail(403, "policy violation", serde_json::to_value(&violations).unwrap());
763 }
764
765 let mut recorder = lex_trace::Recorder::new();
766 if with_overrides && !req.overrides.is_empty() {
767 recorder = recorder.with_overrides(req.overrides);
768 }
769 let handle = recorder.handle();
770 let handler = DefaultHandler::new(policy);
771 let mut vm = Vm::with_handler(&bc, Box::new(handler));
772 vm.set_tracer(Box::new(recorder));
773
774 let vargs: Vec<Value> = req.args.iter().map(json_to_value).collect();
775 let started = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
776 let result = vm.call(&req.func, vargs);
777 let ended = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
778
779 let store = state.store.lock().unwrap();
780 let (root_out, root_err, status) = match &result {
781 Ok(v) => (Some(value_to_json(v)), None, 200u16),
782 Err(e) => (None, Some(format!("{e}")), 200u16),
783 };
784 let tree = handle.finalize(req.func.clone(), serde_json::Value::Null,
785 root_out.clone(), root_err.clone(), started, ended);
786 let run_id = match store.save_trace(&tree) {
787 Ok(id) => id,
788 Err(e) => return error_response(500, format!("save_trace: {e}")),
789 };
790
791 let mut body = serde_json::json!({
792 "run_id": run_id,
793 "output": root_out,
794 });
795 if let Some(err) = root_err {
796 body["error"] = serde_json::Value::String(err);
797 }
798 json_response(status, &body)
799}
800
801fn trace_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
802 let store = state.store.lock().unwrap();
803 match store.load_trace(id) {
804 Ok(t) => json_response(200, &serde_json::to_value(&t).unwrap()),
805 Err(e) => error_response(404, format!("{e}")),
806 }
807}
808
809fn diff_handler(state: &State, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
810 let mut a = None;
811 let mut b = None;
812 for kv in query.split('&') {
813 if let Some((k, v)) = kv.split_once('=') {
814 match k { "a" => a = Some(v.to_string()), "b" => b = Some(v.to_string()), _ => {} }
815 }
816 }
817 let (Some(a), Some(b)) = (a, b) else {
818 return error_response(400, "missing a or b query params");
819 };
820 let store = state.store.lock().unwrap();
821 let ta = match store.load_trace(&a) { Ok(t) => t, Err(e) => return error_response(404, format!("a: {e}")) };
822 let tb = match store.load_trace(&b) { Ok(t) => t, Err(e) => return error_response(404, format!("b: {e}")) };
823 match lex_trace::diff_runs(&ta, &tb) {
824 Some(d) => json_response(200, &serde_json::to_value(&d).unwrap()),
825 None => json_response(200, &serde_json::json!({"divergence": null})),
826 }
827}
828
829fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
830
831fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
832
833#[derive(Deserialize)]
834struct MergeStartReq {
835 src_branch: String,
836 dst_branch: String,
837}
838
839fn merge_start_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
850 let req: MergeStartReq = match serde_json::from_str(body) {
851 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
852 };
853 let store = state.store.lock().unwrap();
854 let src_head = match store.get_branch(&req.src_branch) {
855 Ok(Some(b)) => b.head_op,
856 Ok(None) => return error_response(404, format!("unknown src branch `{}`", req.src_branch)),
857 Err(e) => return error_response(500, format!("src branch read: {e}")),
858 };
859 let dst_head = match store.get_branch(&req.dst_branch) {
860 Ok(Some(b)) => b.head_op,
861 Ok(None) => return error_response(404, format!("unknown dst branch `{}`", req.dst_branch)),
862 Err(e) => return error_response(500, format!("dst branch read: {e}")),
863 };
864 let log = match lex_vcs::OpLog::open(store.root()) {
865 Ok(l) => l,
866 Err(e) => return error_response(500, format!("op log: {e}")),
867 };
868 let merge_id = mint_merge_id();
872 let session = match MergeSession::start(
873 merge_id.clone(),
874 &log,
875 src_head.as_ref(),
876 dst_head.as_ref(),
877 ) {
878 Ok(s) => s,
879 Err(e) => return error_response(500, format!("merge start: {e}")),
880 };
881 let conflicts: Vec<&lex_vcs::ConflictRecord> = session.remaining_conflicts();
882 let auto_resolved_count = session.auto_resolved.len();
883 let body = serde_json::json!({
884 "merge_id": merge_id,
885 "src_head": session.src_head,
886 "dst_head": session.dst_head,
887 "lca": session.lca,
888 "conflicts": conflicts,
889 "auto_resolved_count": auto_resolved_count,
890 });
891 drop(conflicts);
892 drop(store);
893 let wrapped = ApiMergeSession {
894 inner: session,
895 src_branch: req.src_branch,
896 dst_branch: req.dst_branch,
897 };
898 state.sessions.lock().unwrap().insert(merge_id, wrapped);
899 json_response(200, &body)
900}
901
902#[derive(Deserialize)]
903struct MergeResolveReq {
904 resolutions: Vec<MergeResolveEntry>,
909}
910
911#[derive(Deserialize)]
912struct MergeResolveEntry {
913 conflict_id: String,
914 resolution: lex_vcs::Resolution,
915}
916
917fn merge_resolve_handler(
928 state: &State,
929 merge_id: &str,
930 body: &str,
931) -> Response<std::io::Cursor<Vec<u8>>> {
932 let req: MergeResolveReq = match serde_json::from_str(body) {
933 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
934 };
935 let mut sessions = state.sessions.lock().unwrap();
936 let Some(wrapped) = sessions.get_mut(merge_id) else {
937 return error_response(404, format!("unknown merge_id `{merge_id}`"));
938 };
939 let pairs: Vec<(String, lex_vcs::Resolution)> = req.resolutions.into_iter()
940 .map(|e| (e.conflict_id, e.resolution))
941 .collect();
942 let verdicts = wrapped.inner.resolve(pairs);
943 let remaining: Vec<&lex_vcs::ConflictRecord> = wrapped.inner.remaining_conflicts();
944 let body = serde_json::json!({
945 "verdicts": verdicts,
946 "remaining_conflicts": remaining,
947 });
948 json_response(200, &body)
949}
950
951fn merge_commit_handler(
970 state: &State,
971 merge_id: &str,
972) -> Response<std::io::Cursor<Vec<u8>>> {
973 use std::collections::BTreeMap;
974 let wrapped = match state.sessions.lock().unwrap().remove(merge_id) {
975 Some(w) => w,
976 None => return error_response(404, format!("unknown merge_id `{merge_id}`")),
977 };
978 let dst_branch = wrapped.dst_branch.clone();
979 let src_head = wrapped.inner.src_head.clone();
980 let dst_head = wrapped.inner.dst_head.clone();
981 let auto_resolved = wrapped.inner.auto_resolved.clone();
982
983 let mut entries: BTreeMap<lex_vcs::SigId, Option<lex_vcs::StageId>> = BTreeMap::new();
986
987 for outcome in &auto_resolved {
989 if let lex_vcs::MergeOutcome::Src { sig_id, stage_id } = outcome {
990 entries.insert(sig_id.clone(), stage_id.clone());
991 }
992 }
993
994 let resolved = match wrapped.inner.commit() {
996 Ok(r) => r,
997 Err(lex_vcs::CommitError::ConflictsRemaining(ids)) => {
998 return error_with_detail(
1002 422,
1003 "conflicts remaining",
1004 serde_json::json!({"unresolved": ids}),
1005 );
1006 }
1007 };
1008
1009 for (conflict_id, resolution) in resolved {
1010 match resolution {
1011 lex_vcs::Resolution::TakeOurs => {
1012 }
1014 lex_vcs::Resolution::TakeTheirs => {
1015 match resolve_take_theirs(state, &src_head, &conflict_id) {
1025 Ok(stage_id) => {
1026 entries.insert(conflict_id.clone(), stage_id);
1027 }
1028 Err(e) => return error_response(500, format!("resolve take_theirs: {e}")),
1029 }
1030 }
1031 lex_vcs::Resolution::Custom { op } => {
1032 match op.kind.merge_target() {
1041 Some((sig, stage)) => {
1042 if sig != conflict_id {
1043 return error_with_detail(
1044 422,
1045 "custom op targets a different sig than the conflict",
1046 serde_json::json!({
1047 "conflict_id": conflict_id,
1048 "op_targets": sig,
1049 }),
1050 );
1051 }
1052 entries.insert(conflict_id, stage);
1053 }
1054 None => {
1055 return error_with_detail(
1056 422,
1057 "custom op kind doesn't yield a single sig→stage delta",
1058 serde_json::json!({
1059 "conflict_id": conflict_id,
1060 "kind": serde_json::to_value(&op.kind).unwrap_or(serde_json::Value::Null),
1061 }),
1062 );
1063 }
1064 }
1065 }
1066 lex_vcs::Resolution::Defer => {
1067 return error_response(500, "internal: Defer slipped past commit gate");
1069 }
1070 }
1071 }
1072
1073 let resolved_count = entries.len();
1074 let mut parents: Vec<lex_vcs::OpId> = Vec::new();
1075 if let Some(d) = dst_head { parents.push(d); }
1076 if let Some(s) = src_head { parents.push(s); }
1077 let op = lex_vcs::Operation::new(
1078 lex_vcs::OperationKind::Merge { resolved: resolved_count },
1079 parents,
1080 );
1081 let transition = lex_vcs::StageTransition::Merge { entries };
1082 let store = state.store.lock().unwrap();
1083 match store.apply_merge_op_gated(&dst_branch, op, transition) {
1086 Ok(new_head_op) => json_response(200, &serde_json::json!({
1087 "new_head_op": new_head_op,
1088 "dst_branch": dst_branch,
1089 })),
1090 Err(lex_store::StoreError::TypeError(errs)) => error_with_detail(
1091 422, "merged program has type errors", serde_json::to_value(&errs).unwrap_or_default()),
1092 Err(e) => write_error_response("apply merge op", e),
1093 }
1094}
1095
1096fn resolve_take_theirs(
1101 state: &State,
1102 src_head: &Option<lex_vcs::OpId>,
1103 sig: &lex_vcs::SigId,
1104) -> std::io::Result<Option<lex_vcs::StageId>> {
1105 let store = state.store.lock().unwrap();
1106 let log = lex_vcs::OpLog::open(store.root())?;
1107 let Some(head) = src_head.as_ref() else { return Ok(None); };
1108 let mut current: Option<lex_vcs::StageId> = None;
1111 for record in log.walk_forward(head, None)? {
1112 match &record.produces {
1113 lex_vcs::StageTransition::Create { sig_id, stage_id }
1114 if sig_id == sig => { current = Some(stage_id.clone()); }
1115 lex_vcs::StageTransition::Replace { sig_id, to, .. }
1116 if sig_id == sig => { current = Some(to.clone()); }
1117 lex_vcs::StageTransition::Remove { sig_id, .. }
1118 if sig_id == sig => { current = None; }
1119 lex_vcs::StageTransition::Rename { from, to, body_stage_id }
1120 if from == sig || to == sig => {
1121 if from == sig { current = None; }
1122 if to == sig { current = Some(body_stage_id.clone()); }
1123 }
1124 lex_vcs::StageTransition::Merge { entries } => {
1125 if let Some(opt) = entries.get(sig) {
1126 current = opt.clone();
1127 }
1128 }
1129 _ => {}
1130 }
1131 }
1132 Ok(current)
1133}
1134
1135fn mint_merge_id() -> MergeSessionId {
1136 use std::sync::atomic::{AtomicU64, Ordering};
1137 static COUNTER: AtomicU64 = AtomicU64::new(0);
1138 let nanos = SystemTime::now()
1139 .duration_since(UNIX_EPOCH)
1140 .map(|d| d.as_nanos())
1141 .unwrap_or(0);
1142 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1143 format!("merge_{nanos:x}_{n:x}")
1144}
1145
1146pub(crate) fn ops_batch_handler(state: &State, body: &str)
1177 -> Response<std::io::Cursor<Vec<u8>>>
1178{
1179 let records: Vec<lex_vcs::OperationRecord> = match serde_json::from_str(body) {
1180 Ok(r) => r,
1181 Err(e) => return error_response(400,
1182 format!("body must be a JSON array of OperationRecord: {e}")),
1183 };
1184 let store = state.store.lock().unwrap();
1185 let log = match lex_vcs::OpLog::open(store.root()) {
1186 Ok(l) => l,
1187 Err(e) => return error_response(500, format!("opening op log: {e}")),
1188 };
1189
1190 let mut batch_ids: std::collections::BTreeSet<lex_vcs::OpId> =
1198 std::collections::BTreeSet::new();
1199 for rec in &records {
1200 let expected = rec.op.op_id();
1201 if expected != rec.op_id {
1202 return error_with_detail(409, "OpIdMismatch", serde_json::json!({
1203 "supplied": rec.op_id,
1204 "expected": expected,
1205 }));
1206 }
1207 for parent in &rec.op.parents {
1208 let known = match log.get(parent) {
1209 Ok(Some(_)) => true,
1210 Ok(None) => false,
1211 Err(e) => return error_response(500, format!("op log read: {e}")),
1212 };
1213 if !known && !batch_ids.contains(parent) {
1214 return error_with_detail(422, "MissingParent", serde_json::json!({
1215 "op_id": rec.op_id,
1216 "missing_parent": parent,
1217 }));
1218 }
1219 }
1220 batch_ids.insert(rec.op_id.clone());
1221 }
1222
1223 let mut added = 0usize;
1226 let mut added_ids: Vec<&lex_vcs::OpId> = Vec::new();
1227 for rec in &records {
1228 let already_present = matches!(log.get(&rec.op_id), Ok(Some(_)));
1229 match log.put(rec) {
1230 Ok(()) => {
1231 if !already_present {
1232 added += 1;
1233 added_ids.push(&rec.op_id);
1234 }
1235 }
1236 Err(e) => return error_response(500, format!("op log write: {e}")),
1237 }
1238 }
1239
1240 json_response(200, &serde_json::json!({
1241 "received": records.len(),
1242 "added": added,
1243 "skipped": records.len() - added,
1244 "added_ids": added_ids,
1245 }))
1246}
1247
1248pub(crate) fn attestations_batch_handler(state: &State, body: &str)
1270 -> Response<std::io::Cursor<Vec<u8>>>
1271{
1272 let attestations: Vec<lex_vcs::Attestation> = match serde_json::from_str(body) {
1273 Ok(a) => a,
1274 Err(e) => return error_response(400,
1275 format!("body must be a JSON array of Attestation: {e}")),
1276 };
1277 let store = state.store.lock().unwrap();
1278 let log = match store.attestation_log() {
1279 Ok(l) => l,
1280 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1281 };
1282 let op_log = match lex_vcs::OpLog::open(store.root()) {
1283 Ok(l) => l,
1284 Err(e) => return error_response(500, format!("opening op log: {e}")),
1285 };
1286
1287 for att in &attestations {
1289 let expected = lex_vcs::Attestation::with_timestamp(
1292 att.stage_id.clone(),
1293 att.op_id.clone(),
1294 att.intent_id.clone(),
1295 att.kind.clone(),
1296 att.result.clone(),
1297 att.produced_by.clone(),
1298 att.cost.clone(),
1299 att.timestamp,
1300 ).attestation_id;
1301 if expected != att.attestation_id {
1302 return error_with_detail(409, "AttestationIdMismatch", serde_json::json!({
1303 "supplied": att.attestation_id,
1304 "expected": expected,
1305 }));
1306 }
1307 if let Some(op_id) = &att.op_id {
1311 match op_log.get(op_id) {
1312 Ok(Some(_)) => {}
1313 Ok(None) => return error_with_detail(422, "UnknownOp", serde_json::json!({
1314 "attestation_id": att.attestation_id,
1315 "op_id": op_id,
1316 })),
1317 Err(e) => return error_response(500, format!("op log read: {e}")),
1318 }
1319 }
1320 }
1321
1322 let mut added = 0usize;
1326 let mut added_ids: Vec<&lex_vcs::AttestationId> = Vec::new();
1327 for att in &attestations {
1328 let already_present = matches!(log.get(&att.attestation_id), Ok(Some(_)));
1329 match log.put(att) {
1330 Ok(()) => {
1331 if !already_present {
1332 added += 1;
1333 added_ids.push(&att.attestation_id);
1334 }
1335 }
1336 Err(e) => return error_response(500, format!("attestation log write: {e}")),
1337 }
1338 }
1339
1340 json_response(200, &serde_json::json!({
1341 "received": attestations.len(),
1342 "added": added,
1343 "skipped": attestations.len() - added,
1344 "added_ids": added_ids,
1345 }))
1346}
1347
1348pub(crate) fn branch_head_handler(state: &State, name: &str)
1357 -> Response<std::io::Cursor<Vec<u8>>>
1358{
1359 let store = state.store.lock().unwrap();
1360 let head = match store.get_branch(name) {
1361 Ok(Some(b)) => b.head_op,
1362 Ok(None) => None,
1363 Err(e) => return error_response(500, format!("get_branch: {e}")),
1364 };
1365 json_response(200, &serde_json::json!({
1366 "branch": name,
1367 "head_op": head,
1368 }))
1369}
1370
1371pub(crate) fn ops_since_handler(state: &State, query: &str)
1395 -> Response<std::io::Cursor<Vec<u8>>>
1396{
1397 let mut after: Option<String> = None;
1398 let mut branch = String::from("main");
1399 let mut limit: Option<usize> = None;
1400 for kv in query.split('&') {
1401 let Some((k, v)) = kv.split_once('=') else { continue };
1402 match k {
1403 "after" => after = Some(v.to_string()),
1404 "branch" => branch = v.to_string(),
1405 "limit" => {
1406 limit = Some(match v.parse::<usize>() {
1407 Ok(n) => n,
1408 Err(_) => return error_response(400,
1409 format!("limit must be a positive integer, got `{v}`")),
1410 });
1411 }
1412 _ => {}
1413 }
1414 }
1415
1416 let store = state.store.lock().unwrap();
1417 let log = match lex_vcs::OpLog::open(store.root()) {
1418 Ok(l) => l,
1419 Err(e) => return error_response(500, format!("opening op log: {e}")),
1420 };
1421 let head = match store.get_branch(&branch) {
1422 Ok(Some(b)) => b.head_op,
1423 Ok(None) => None,
1424 Err(e) => return error_response(500, format!("get_branch: {e}")),
1425 };
1426 let Some(head) = head else {
1427 return json_response(200, &serde_json::json!([]));
1428 };
1429
1430 let ops_since = match log.ops_since(&head, after.as_ref()) {
1431 Ok(o) => o,
1432 Err(e) => return error_response(500, format!("ops_since: {e}")),
1433 };
1434 let mut ops = ops_since;
1438 ops.reverse();
1439 if let Some(n) = limit {
1440 ops.truncate(n);
1441 }
1442
1443 json_response(200, &serde_json::to_value(&ops).unwrap_or_default())
1444}
1445
1446pub(crate) fn attestations_since_handler(state: &State, query: &str)
1459 -> Response<std::io::Cursor<Vec<u8>>>
1460{
1461 let mut after_op: Option<String> = None;
1462 let mut limit: Option<usize> = None;
1463 for kv in query.split('&') {
1464 let Some((k, v)) = kv.split_once('=') else { continue };
1465 match k {
1466 "after-op" => after_op = Some(v.to_string()),
1467 "limit" => {
1468 limit = Some(match v.parse::<usize>() {
1469 Ok(n) => n,
1470 Err(_) => return error_response(400,
1471 format!("limit must be a positive integer, got `{v}`")),
1472 });
1473 }
1474 _ => {}
1475 }
1476 }
1477
1478 let store = state.store.lock().unwrap();
1479 let log = match store.attestation_log() {
1480 Ok(l) => l,
1481 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1482 };
1483
1484 let exclude: std::collections::BTreeSet<String> = match &after_op {
1488 None => std::collections::BTreeSet::new(),
1489 Some(cutoff) => {
1490 let op_log = match lex_vcs::OpLog::open(store.root()) {
1491 Ok(l) => l,
1492 Err(e) => return error_response(500, format!("opening op log: {e}")),
1493 };
1494 match op_log.walk_back(cutoff, None) {
1495 Ok(records) => records.into_iter().map(|r| r.op_id).collect(),
1496 Err(_) => {
1497 std::collections::BTreeSet::new()
1501 }
1502 }
1503 }
1504 };
1505
1506 let all = match log.list_all() {
1507 Ok(v) => v,
1508 Err(e) => return error_response(500, format!("listing attestations: {e}")),
1509 };
1510 let mut filtered: Vec<lex_vcs::Attestation> = all
1511 .into_iter()
1512 .filter(|a| match &a.op_id {
1513 Some(op_id) => !exclude.contains(op_id),
1514 None => true,
1518 })
1519 .collect();
1520 filtered.sort_by(|a, b| {
1524 a.timestamp.cmp(&b.timestamp)
1525 .then_with(|| a.attestation_id.cmp(&b.attestation_id))
1526 });
1527 if let Some(n) = limit {
1528 filtered.truncate(n);
1529 }
1530
1531 json_response(200, &serde_json::to_value(&filtered).unwrap_or_default())
1532}
1533
1534#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1538struct PkgRecord {
1539 name: String,
1540 version: String,
1541 head_op: Option<String>,
1542 published_at: u64,
1543 function_names: Vec<String>,
1545 ops: Vec<serde_json::Value>,
1547}
1548
1549#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
1556#[serde(rename_all = "lowercase")]
1557pub enum Visibility {
1558 #[default]
1559 Private,
1560 Public,
1561}
1562
1563#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
1568struct PkgIndex {
1569 latest: Option<String>,
1571 versions: Vec<PkgVersionSummary>,
1573 #[serde(default)]
1577 visibility: Visibility,
1578}
1579
1580#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1581struct PkgVersionSummary {
1582 version: String,
1583 head_op: Option<String>,
1584 published_at: u64,
1585}
1586
1587fn pkg_name_dir(root: &std::path::Path, name: &str) -> PathBuf {
1588 root.join("packages").join(name)
1589}
1590
1591fn pkg_index_path(root: &std::path::Path, name: &str) -> PathBuf {
1592 pkg_name_dir(root, name).join("index.json")
1593}
1594
1595fn pkg_version_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1596 pkg_name_dir(root, name).join(format!("{version}.json"))
1597}
1598
1599fn pkg_archive_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1600 pkg_name_dir(root, name).join(format!("{version}.tar.gz"))
1601}
1602
1603fn load_pkg_index(root: &std::path::Path, name: &str) -> Option<PkgIndex> {
1604 let bytes = std::fs::read(pkg_index_path(root, name)).ok()?;
1605 serde_json::from_slice(&bytes).ok()
1606}
1607
1608fn load_pkg_record(root: &std::path::Path, name: &str, version: &str) -> Option<PkgRecord> {
1609 let bytes = std::fs::read(pkg_version_path(root, name, version)).ok()?;
1610 serde_json::from_slice(&bytes).ok()
1611}
1612
1613fn load_latest_pkg_record(root: &std::path::Path, name: &str) -> Option<PkgRecord> {
1614 let index = load_pkg_index(root, name)?;
1615 let latest = index.latest.clone()?;
1616 load_pkg_record(root, name, &latest)
1617}
1618
1619fn pkg_is_public(root: &std::path::Path, name: &str) -> bool {
1623 load_pkg_index(root, name).map(|i| i.visibility) == Some(Visibility::Public)
1624}
1625
1626fn valid_pkg_segment(s: &str) -> bool {
1631 !s.is_empty()
1632 && s.len() <= 128
1633 && s != "."
1634 && s != ".."
1635 && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
1636}
1637
1638#[derive(Deserialize)]
1639struct VisibilityReq {
1640 visibility: Visibility,
1641}
1642
1643fn pkg_set_visibility_handler(
1650 state: &State,
1651 name: &str,
1652 body: &str,
1653) -> Response<std::io::Cursor<Vec<u8>>> {
1654 if !valid_pkg_segment(name) {
1655 return error_response(400, format!("invalid package name {name:?}"));
1656 }
1657 let req: VisibilityReq = match serde_json::from_str(body) {
1658 Ok(r) => r,
1659 Err(e) => return error_response(400, format!("bad request: {e}")),
1660 };
1661 let mut index = match load_pkg_index(&state.root, name) {
1662 Some(i) => i,
1663 None => return error_response(404, format!("package {name:?} not found")),
1664 };
1665 index.visibility = req.visibility;
1666 let bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1667 match std::fs::write(pkg_index_path(&state.root, name), bytes) {
1668 Ok(()) => json_response(
1669 200,
1670 &serde_json::json!({ "name": name, "visibility": index.visibility }),
1671 ),
1672 Err(e) => error_response(500, format!("write index: {e}")),
1673 }
1674}
1675
1676fn public_pkg_names(root: &std::path::Path) -> Vec<String> {
1679 list_pkg_names(root)
1680 .into_iter()
1681 .filter(|name| pkg_is_public(root, name))
1682 .collect()
1683}
1684
1685fn public_pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
1689 let packages: Vec<serde_json::Value> = public_pkg_names(&state.root)
1690 .iter()
1691 .filter_map(|name| {
1692 let r = load_latest_pkg_record(&state.root, name)?;
1693 Some(serde_json::json!({
1694 "name": r.name,
1695 "version": r.version,
1696 "head_op": r.head_op,
1697 "published_at": r.published_at,
1698 }))
1699 })
1700 .collect();
1701 json_response(200, &serde_json::json!({ "packages": packages }))
1702}
1703
1704#[derive(Debug, PartialEq, Eq)]
1709enum PublicTarget {
1710 List,
1711 Latest(String),
1712 Versions(String),
1713 Head(String),
1714 Version(String, String),
1715 Archive(String, String),
1716}
1717
1718impl PublicTarget {
1719 fn pkg_name(&self) -> Option<&str> {
1721 match self {
1722 PublicTarget::List => None,
1723 PublicTarget::Latest(n)
1724 | PublicTarget::Versions(n)
1725 | PublicTarget::Head(n)
1726 | PublicTarget::Version(n, _)
1727 | PublicTarget::Archive(n, _) => Some(n),
1728 }
1729 }
1730}
1731
1732fn resolve_public(method: &Method, path: &str) -> Result<PublicTarget, u16> {
1737 if !matches!(method, Method::Get) {
1738 return Err(405);
1739 }
1740 let rest = path.trim_matches('/');
1741 if rest.is_empty() {
1742 return Ok(PublicTarget::List);
1743 }
1744 let segs: Vec<&str> = rest.split('/').collect();
1745 if !segs.iter().all(|s| valid_pkg_segment(s)) {
1746 return Err(404);
1747 }
1748 match segs.as_slice() {
1749 [n] => Ok(PublicTarget::Latest(n.to_string())),
1750 [n, "versions"] => Ok(PublicTarget::Versions(n.to_string())),
1751 [n, "head"] => Ok(PublicTarget::Head(n.to_string())),
1752 [n, v, "archive"] => Ok(PublicTarget::Archive(n.to_string(), v.to_string())),
1753 [n, v] => Ok(PublicTarget::Version(n.to_string(), v.to_string())),
1754 _ => Err(404),
1755 }
1756}
1757
1758pub fn route_public(
1769 state: &State,
1770 method: &Method,
1771 path: &str,
1772 _query: &str,
1773) -> Response<std::io::Cursor<Vec<u8>>> {
1774 let target = match resolve_public(method, path) {
1775 Ok(t) => t,
1776 Err(405) => return error_response(405, "public read is GET-only"),
1777 Err(_) => return error_response(404, "not found"),
1778 };
1779 if let PublicTarget::List = target {
1781 return public_pkg_list_handler(state);
1782 }
1783 if let Some(name) = target.pkg_name() {
1785 if !pkg_is_public(&state.root, name) {
1786 return error_response(404, format!("package {name:?} not found"));
1787 }
1788 }
1789 match target {
1790 PublicTarget::List => unreachable!("handled above"),
1791 PublicTarget::Latest(n) => pkg_get_handler(state, &n),
1792 PublicTarget::Versions(n) => pkg_versions_handler(state, &n),
1793 PublicTarget::Head(n) => pkg_head_handler(state, &n),
1794 PublicTarget::Version(n, v) => pkg_get_version_handler(state, &n, &v),
1795 PublicTarget::Archive(n, v) => pkg_archive_handler(state, &n, &v),
1796 }
1797}
1798
1799fn save_pkg_record(
1800 root: &std::path::Path,
1801 record: &PkgRecord,
1802 archive: &[u8],
1803) -> std::io::Result<()> {
1804 let dir = pkg_name_dir(root, &record.name);
1805 std::fs::create_dir_all(&dir)?;
1806
1807 let rec_bytes = serde_json::to_vec_pretty(record).unwrap_or_default();
1809 std::fs::write(pkg_version_path(root, &record.name, &record.version), rec_bytes)?;
1810
1811 std::fs::write(pkg_archive_path(root, &record.name, &record.version), archive)?;
1813
1814 let mut index = load_pkg_index(root, &record.name).unwrap_or_default();
1816 index.latest = Some(record.version.clone());
1817 if !index.versions.iter().any(|v| v.version == record.version) {
1818 index.versions.push(PkgVersionSummary {
1819 version: record.version.clone(),
1820 head_op: record.head_op.clone(),
1821 published_at: record.published_at,
1822 });
1823 }
1824 let idx_bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1825 std::fs::write(pkg_index_path(root, &record.name), idx_bytes)
1826}
1827
1828fn list_pkg_names(root: &std::path::Path) -> Vec<String> {
1829 let dir = root.join("packages");
1830 let Ok(entries) = std::fs::read_dir(&dir) else {
1831 return Vec::new();
1832 };
1833 let mut names: Vec<String> = entries
1834 .filter_map(|e| e.ok())
1835 .filter(|e| e.path().is_dir())
1836 .filter_map(|e| e.file_name().into_string().ok())
1837 .collect();
1838 names.sort();
1839 names
1840}
1841
1842fn collect_lex_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
1843 let Ok(entries) = std::fs::read_dir(dir) else { return };
1844 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
1845 entries.sort_by_key(|e| e.path());
1846 for entry in entries {
1847 let path = entry.path();
1848 if path.is_dir() {
1849 collect_lex_files(&path, out);
1850 } else if path.extension().and_then(|x| x.to_str()) == Some("lex") {
1851 out.push(path);
1852 }
1853 }
1854}
1855
1856fn pkg_publish_handler(state: &State, body: &[u8]) -> Response<std::io::Cursor<Vec<u8>>> {
1859 let tmp = match tempfile::TempDir::new() {
1860 Ok(t) => t,
1861 Err(e) => return error_response(500, format!("create temp dir: {e}")),
1862 };
1863 {
1864 let gz = flate2::read::GzDecoder::new(std::io::Cursor::new(body));
1865 let mut ar = tar::Archive::new(gz);
1866 if let Err(e) = ar.unpack(tmp.path()) {
1867 return error_response(400, format!("unpack archive: {e}"));
1868 }
1869 }
1870
1871 let toml_path = tmp.path().join("lex.toml");
1872 if !toml_path.exists() {
1873 return error_response(400, "archive must contain lex.toml at root");
1874 }
1875 let manifest = match Manifest::load(&toml_path) {
1876 Ok(m) => m,
1877 Err(e) => return error_response(400, format!("lex.toml: {e}")),
1878 };
1879 let (pkg_name, pkg_version) = match &manifest.package {
1880 Some(m) => (m.name.clone(), m.version.clone()),
1881 None => return error_response(400, "lex.toml must have a [package] section"),
1882 };
1883
1884 if load_pkg_record(&state.root, &pkg_name, &pkg_version).is_some() {
1888 return error_response(
1889 409,
1890 format!(
1891 "package {pkg_name}@{pkg_version} already published; \
1892 bump the version in lex.toml to publish a new release"
1893 ),
1894 );
1895 }
1896
1897 let src_dir = tmp.path().join("src");
1898 if !src_dir.exists() {
1899 return error_response(400, "archive must contain a src/ directory");
1900 }
1901 let mut lex_files: Vec<PathBuf> = Vec::new();
1902 collect_lex_files(&src_dir, &mut lex_files);
1903 if lex_files.is_empty() {
1904 return error_response(400, "no .lex files found in src/");
1905 }
1906
1907 let store = state.store.lock().unwrap();
1908 let branch = store.current_branch();
1909
1910 let old_head = match store.branch_head(&branch) {
1930 Ok(h) => h,
1931 Err(e) => return error_response(500, format!("branch_head: {e}")),
1932 };
1933 let old_pairs: Vec<(String, String)> =
1940 old_head.iter().map(|(sig, stage)| (sig.clone(), stage.clone())).collect();
1941 let mut old_fns_by_name: BTreeMap<String, Vec<lex_ast::FnDecl>> = BTreeMap::new();
1942 for fd in store.get_asts_for_sigs_bulk(&old_pairs)
1943 .into_iter()
1944 .filter_map(|r| r.ok())
1945 .filter_map(|s| match s { lex_ast::Stage::FnDecl(fd) => Some(fd), _ => None })
1946 {
1947 old_fns_by_name.entry(fd.name.clone()).or_default().push(fd);
1948 }
1949 fn structural_key(fd: &lex_ast::FnDecl) -> Option<String> {
1957 let mut anon = fd.clone();
1958 anon.name = String::new();
1959 lex_ast::sig_id(&lex_ast::Stage::FnDecl(anon))
1960 }
1961
1962 fn take_matching(
1972 map: &mut BTreeMap<String, Vec<lex_ast::FnDecl>>,
1973 name: &str,
1974 new_fd: &lex_ast::FnDecl,
1975 ) -> Option<lex_ast::FnDecl> {
1976 let candidates = map.get_mut(name)?;
1977 let idx = match candidates.len() {
1978 0 => return None,
1979 1 => 0,
1980 _ => {
1981 let want = structural_key(new_fd);
1982 candidates.iter().position(|c| structural_key(c) == want)?
1983 }
1984 };
1985 let matched = candidates.remove(idx);
1986 if candidates.is_empty() {
1987 map.remove(name);
1988 }
1989 Some(matched)
1990 }
1991
1992 let loaded = match load_package(&lex_files, tmp.path(), &pkg_name) {
2012 Ok(p) => p,
2013 Err(e) => return error_response(400, format!("load package: {e}")),
2014 };
2015 let mut stages = canonicalize_program(&loaded.program);
2016 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
2022 return error_with_detail(
2023 422,
2024 format!("type errors in package {pkg_name}"),
2025 serde_json::to_value(&errs).unwrap(),
2026 );
2027 }
2028 let new_fns: BTreeMap<String, lex_ast::FnDecl> = stages
2029 .iter()
2030 .filter_map(|s| match s {
2031 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
2032 _ => None,
2033 })
2034 .collect();
2035 let all_function_names: Vec<String> = new_fns.keys().cloned().collect();
2036
2037 let mut old_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2042 for (name, new_fd) in &new_fns {
2043 if let Some(fd) = take_matching(&mut old_fns_by_name, name, new_fd) {
2044 old_fns.insert(name.clone(), fd);
2045 }
2046 }
2047 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
2048
2049 let mut new_imports = lex_vcs::ImportMap::new();
2054 for (file, modules) in &loaded.imports_by_file {
2055 let entry = new_imports.entry(file.clone()).or_default();
2056 for m in modules {
2057 entry.insert(m.clone());
2058 }
2059 }
2060
2061 let outcome = match store.publish_program(&branch, &stages, &report, &new_imports, false) {
2062 Ok(outcome) => outcome,
2063 Err(lex_store::StoreError::TypeError(errs)) => {
2064 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
2065 }
2066 Err(e) => return write_error_response("publish_program", e),
2067 };
2068 let all_ops: Vec<serde_json::Value> = match serde_json::to_value(&outcome.ops) {
2069 Ok(serde_json::Value::Array(arr)) => arr,
2070 _ => Vec::new(),
2071 };
2072 let final_head_op = outcome.head_op;
2073
2074 let now = SystemTime::now()
2099 .duration_since(UNIX_EPOCH)
2100 .map(|d| d.as_secs())
2101 .unwrap_or(0);
2102 let record = PkgRecord {
2103 name: pkg_name.clone(),
2104 version: pkg_version,
2105 head_op: final_head_op.clone(),
2106 published_at: now,
2107 function_names: all_function_names,
2108 ops: all_ops.clone(),
2109 };
2110 if let Err(e) = save_pkg_record(&state.root, &record, body) {
2111 return error_response(500, format!("save package index: {e}"));
2112 }
2113
2114 json_response(200, &serde_json::json!({
2115 "package": pkg_name,
2116 "ops": all_ops,
2117 "head_op": final_head_op,
2118 }))
2119}
2120
2121fn pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
2123 let names = list_pkg_names(&state.root);
2124 let packages: Vec<serde_json::Value> = names.iter()
2125 .filter_map(|name| {
2126 let idx = load_pkg_index(&state.root, name)?;
2127 let latest = idx.latest.as_deref()?;
2128 let r = load_pkg_record(&state.root, name, latest)?;
2129 Some(serde_json::json!({
2130 "name": r.name,
2131 "version": r.version,
2132 "head_op": r.head_op,
2133 "published_at": r.published_at,
2134 }))
2135 })
2136 .collect();
2137 json_response(200, &serde_json::json!({ "packages": packages }))
2138}
2139
2140fn pkg_get_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2142 match load_latest_pkg_record(&state.root, name) {
2143 Some(r) => json_response(200, &serde_json::json!({
2144 "name": r.name,
2145 "version": r.version,
2146 "head_op": r.head_op,
2147 "published_at": r.published_at,
2148 "function_names": r.function_names,
2149 "ops": r.ops,
2150 })),
2151 None => error_response(404, format!("package {name:?} not found")),
2152 }
2153}
2154
2155fn pkg_versions_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2157 match load_pkg_index(&state.root, name) {
2158 Some(idx) => json_response(200, &serde_json::json!({
2159 "name": name,
2160 "latest": idx.latest,
2161 "versions": idx.versions,
2162 })),
2163 None => error_response(404, format!("package {name:?} not found")),
2164 }
2165}
2166
2167fn pkg_get_version_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2169 match load_pkg_record(&state.root, name, version) {
2170 Some(r) => json_response(200, &serde_json::json!({
2171 "name": r.name,
2172 "version": r.version,
2173 "head_op": r.head_op,
2174 "published_at": r.published_at,
2175 "function_names": r.function_names,
2176 "ops": r.ops,
2177 })),
2178 None => error_response(404, format!("package {name:?}@{version:?} not found")),
2179 }
2180}
2181
2182fn pkg_archive_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2184 let path = pkg_archive_path(&state.root, name, version);
2185 match std::fs::read(&path) {
2186 Ok(bytes) => Response::from_data(bytes)
2187 .with_status_code(200)
2188 .with_header(
2189 tiny_http::Header::from_bytes(
2190 &b"Content-Type"[..],
2191 &b"application/gzip"[..],
2192 )
2193 .unwrap(),
2194 ),
2195 Err(_) => error_response(404, format!("archive for {name:?}@{version:?} not found")),
2196 }
2197}
2198
2199fn pkg_head_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2201 match load_latest_pkg_record(&state.root, name) {
2202 Some(r) => json_response(200, &serde_json::json!({
2203 "name": r.name,
2204 "version": r.version,
2205 "head_op": r.head_op,
2206 })),
2207 None => error_response(404, format!("package {name:?} not found")),
2208 }
2209}
2210
2211fn pkg_delete_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2213 let record = match load_latest_pkg_record(&state.root, name) {
2214 Some(r) => r,
2215 None => return error_response(404, format!("package {name:?} not found")),
2216 };
2217
2218 let store = state.store.lock().unwrap();
2219 let branch = store.current_branch();
2220
2221 let head = match store.branch_head(&branch) {
2222 Ok(h) => h,
2223 Err(e) => return error_response(500, format!("branch_head: {e}")),
2224 };
2225
2226 let head_pairs: Vec<(String, String)> = head
2233 .iter()
2234 .map(|(sig, stage)| (sig.clone(), stage.clone()))
2235 .collect();
2236 let old_fns: BTreeMap<String, lex_ast::FnDecl> = store
2237 .get_asts_for_sigs_bulk(&head_pairs)
2238 .into_iter()
2239 .filter_map(|r| r.ok())
2240 .filter_map(|s| match s {
2241 lex_ast::Stage::FnDecl(fd)
2242 if record.function_names.contains(&fd.name) => Some((fd.name.clone(), fd)),
2243 _ => None,
2244 })
2245 .collect();
2246
2247 let new_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2248 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
2249 let empty_imports = lex_vcs::ImportMap::new();
2250
2251 match store.publish_program(&branch, &[], &report, &empty_imports, false) {
2252 Ok(outcome) => {
2253 let ver = record.version.clone();
2255 let _ = std::fs::remove_file(pkg_version_path(&state.root, name, &ver));
2256 let _ = std::fs::remove_file(pkg_archive_path(&state.root, name, &ver));
2257 if let Some(mut idx) = load_pkg_index(&state.root, name) {
2259 idx.versions.retain(|v| v.version != ver);
2260 idx.latest = idx.versions.last().map(|v| v.version.clone());
2261 if idx.versions.is_empty() {
2262 let _ = std::fs::remove_dir_all(pkg_name_dir(&state.root, name));
2263 } else {
2264 let bytes = serde_json::to_vec_pretty(&idx).unwrap_or_default();
2265 let _ = std::fs::write(pkg_index_path(&state.root, name), bytes);
2266 }
2267 }
2268 json_response(200, &serde_json::json!({
2269 "deleted": name,
2270 "version": ver,
2271 "ops": outcome.ops,
2272 "head_op": outcome.head_op,
2273 }))
2274 }
2275 Err(lex_store::StoreError::TypeError(errs)) => {
2276 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
2277 }
2278 Err(e) => write_error_response("retract package", e),
2279 }
2280}
2281
2282#[cfg(test)]
2283mod policy_ceiling_tests {
2284 use super::*;
2285 use lex_runtime::Policy;
2286 use std::path::PathBuf;
2287
2288 fn permissive_request() -> Policy {
2292 Policy {
2293 allow_effects: ["io", "fs_read", "fs_write", "net", "proc"]
2294 .iter()
2295 .map(|s| s.to_string())
2296 .collect(),
2297 allow_fs_read: vec![PathBuf::from("/")],
2298 allow_fs_write: vec![PathBuf::from("/")],
2299 allow_net_host: Vec::new(),
2300 allow_proc: Vec::new(),
2301 allow_approval: Vec::new(),
2302 budget: None,
2303 }
2304 }
2305
2306 #[test]
2307 fn ceiling_drops_effects_the_caller_was_not_granted() {
2308 let ceiling = Policy {
2309 allow_effects: ["io", "time"].iter().map(|s| s.to_string()).collect(),
2310 ..Policy::default()
2311 };
2312 let got = clamp_policy(permissive_request(), &ceiling);
2313 assert!(got.allow_effects.contains("io"));
2314 assert!(!got.allow_effects.contains("proc"), "proc must not survive a ceiling without it");
2315 assert!(!got.allow_effects.contains("fs_write"));
2316 assert!(!got.allow_effects.contains("net"));
2317 assert!(!got.allow_effects.contains("time"));
2319 }
2320
2321 #[test]
2322 fn ceiling_scopes_override_caller_scopes() {
2323 let ceiling = Policy {
2324 allow_effects: ["fs_read"].iter().map(|s| s.to_string()).collect(),
2325 allow_fs_read: vec![PathBuf::from("/srv/tenant")],
2326 ..Policy::default()
2327 };
2328 let got = clamp_policy(permissive_request(), &ceiling);
2329 assert_eq!(got.allow_fs_read, vec![PathBuf::from("/srv/tenant")]);
2332 assert!(got.allow_fs_write.is_empty());
2333 assert!(got.allow_proc.is_empty());
2334 assert!(got.allow_net_host.is_empty());
2335 }
2336
2337 #[test]
2338 fn ceiling_caps_budget_and_prefers_the_smaller() {
2339 let mut req = permissive_request();
2341 req.budget = None;
2342 let ceiling = Policy { budget: Some(1_000), ..Policy::default() };
2343 assert_eq!(clamp_policy(req, &ceiling).budget, Some(1_000));
2344
2345 let mut req2 = permissive_request();
2347 req2.budget = Some(50);
2348 let ceiling2 = Policy { budget: Some(1_000), ..Policy::default() };
2349 assert_eq!(clamp_policy(req2, &ceiling2).budget, Some(50));
2350 }
2351
2352 #[test]
2353 fn empty_ceiling_is_pure_only() {
2354 let got = clamp_policy(permissive_request(), &Policy::default());
2355 assert!(got.allow_effects.is_empty(), "an empty ceiling grants nothing");
2356 assert!(got.allow_proc.is_empty());
2357 assert!(got.allow_fs_write.is_empty());
2358 }
2359}
2360
2361#[cfg(test)]
2362mod public_read_tests {
2363 use super::*;
2364
2365 fn seed_pkg(root: &std::path::Path, name: &str, version: &str) {
2368 let record = PkgRecord {
2369 name: name.to_string(),
2370 version: version.to_string(),
2371 head_op: Some(format!("op-{name}")),
2372 published_at: 1,
2373 function_names: vec![format!("{name}.f")],
2374 ops: vec![],
2375 };
2376 save_pkg_record(root, &record, format!("ARCHIVE:{name}@{version}").as_bytes())
2377 .expect("seed package");
2378 }
2379
2380 #[test]
2381 fn new_package_defaults_to_private() {
2382 let tmp = tempfile::TempDir::new().unwrap();
2383 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2384 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2385 assert!(!pkg_is_public(tmp.path(), "does-not-exist"));
2387 }
2388
2389 #[test]
2390 fn set_visibility_round_trips_and_index_persists() {
2391 let tmp = tempfile::TempDir::new().unwrap();
2392 let state = State::open(tmp.path().to_path_buf()).unwrap();
2393 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2394
2395 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"public"}"#);
2396 assert!(pkg_is_public(tmp.path(), "lex-schema"));
2397 let idx = load_pkg_index(tmp.path(), "lex-schema").unwrap();
2399 assert_eq!(idx.latest.as_deref(), Some("0.9.2"));
2400 assert_eq!(idx.versions.len(), 1);
2401
2402 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"private"}"#);
2403 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2404 }
2405
2406 #[test]
2407 fn set_visibility_on_unknown_package_is_a_noop() {
2408 let tmp = tempfile::TempDir::new().unwrap();
2409 let state = State::open(tmp.path().to_path_buf()).unwrap();
2410 let _ = pkg_set_visibility_handler(&state, "ghost", r#"{"visibility":"public"}"#);
2412 assert!(load_pkg_index(tmp.path(), "ghost").is_none());
2413 }
2414
2415 #[test]
2416 fn public_listing_omits_private_packages() {
2417 let tmp = tempfile::TempDir::new().unwrap();
2418 let state = State::open(tmp.path().to_path_buf()).unwrap();
2419 seed_pkg(tmp.path(), "pub-pkg", "1.0.0");
2420 seed_pkg(tmp.path(), "priv-pkg", "1.0.0");
2421 let _ = pkg_set_visibility_handler(&state, "pub-pkg", r#"{"visibility":"public"}"#);
2422
2423 let names = public_pkg_names(tmp.path());
2424 assert_eq!(names, vec!["pub-pkg".to_string()]);
2425 }
2426
2427 #[test]
2428 fn resolve_public_maps_routes() {
2429 let get = Method::Get;
2430 assert_eq!(resolve_public(&get, "").unwrap(), PublicTarget::List);
2431 assert_eq!(resolve_public(&get, "/").unwrap(), PublicTarget::List);
2432 assert_eq!(
2433 resolve_public(&get, "/lex-schema").unwrap(),
2434 PublicTarget::Latest("lex-schema".into())
2435 );
2436 assert_eq!(
2437 resolve_public(&get, "/lex-schema/versions").unwrap(),
2438 PublicTarget::Versions("lex-schema".into())
2439 );
2440 assert_eq!(
2441 resolve_public(&get, "/lex-schema/head").unwrap(),
2442 PublicTarget::Head("lex-schema".into())
2443 );
2444 assert_eq!(
2445 resolve_public(&get, "/lex-schema/0.9.2").unwrap(),
2446 PublicTarget::Version("lex-schema".into(), "0.9.2".into())
2447 );
2448 assert_eq!(
2449 resolve_public(&get, "/lex-schema/0.9.2/archive").unwrap(),
2450 PublicTarget::Archive("lex-schema".into(), "0.9.2".into())
2451 );
2452 }
2453
2454 #[test]
2455 fn resolve_public_rejects_bad_method_and_traversal() {
2456 assert_eq!(resolve_public(&Method::Put, "/lex-schema"), Err(405));
2458 assert_eq!(resolve_public(&Method::Post, "").err(), Some(405));
2459 assert_eq!(resolve_public(&Method::Get, "/.."), Err(404));
2461 assert_eq!(resolve_public(&Method::Get, "/lex-schema/../etc"), Err(404));
2462 assert_eq!(resolve_public(&Method::Get, "/a/b/c/d"), Err(404));
2463 assert!(resolve_public(&Method::Get, "/lex schema").is_err());
2465 }
2466}