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 crate::publish_examples::record_examples_for_publish;
14use lex_syntax::{load_package, load_program_from_str, Manifest};
15use lex_vcs::{MergeSession, MergeSessionId};
16use serde::{Deserialize, Serialize};
17use std::collections::{BTreeMap, BTreeSet, HashMap};
18use std::path::PathBuf;
19use std::sync::{Arc, Mutex};
20use std::time::{SystemTime, UNIX_EPOCH};
21use tiny_http::{Header, Method, Request, Response};
22
23pub struct State {
24 pub store: Mutex<Store>,
25 pub root: PathBuf,
30 pub sessions: Mutex<HashMap<MergeSessionId, ApiMergeSession>>,
37 pub policy_ceiling: Option<Policy>,
58}
59
60pub struct ApiMergeSession {
67 pub inner: MergeSession,
68 pub src_branch: String,
69 pub dst_branch: String,
70}
71
72impl State {
73 pub fn open(root: PathBuf) -> anyhow::Result<Self> {
74 Self::open_with_ceiling(root, None)
75 }
76
77 pub fn open_with_ceiling(
82 root: PathBuf,
83 policy_ceiling: Option<Policy>,
84 ) -> anyhow::Result<Self> {
85 Ok(Self {
86 store: Mutex::new(Store::open(&root)?),
87 root,
88 sessions: Mutex::new(HashMap::new()),
89 policy_ceiling,
90 })
91 }
92
93 pub fn new_with_tenant(tenant_id: &str, store_root: PathBuf) -> anyhow::Result<Self> {
103 validate_tenant_id(tenant_id)?;
104 Self::open(store_root.join(tenant_id))
105 }
106
107 pub fn new_with_tenant_and_ceiling(
112 tenant_id: &str,
113 store_root: PathBuf,
114 policy_ceiling: Option<Policy>,
115 ) -> anyhow::Result<Self> {
116 validate_tenant_id(tenant_id)?;
117 Self::open_with_ceiling(store_root.join(tenant_id), policy_ceiling)
118 }
119}
120
121fn clamp_policy(requested: Policy, ceiling: &Policy) -> Policy {
137 let allow_effects: BTreeSet<String> = requested
138 .allow_effects
139 .intersection(&ceiling.allow_effects)
140 .cloned()
141 .collect();
142 let budget = match (requested.budget, ceiling.budget) {
143 (Some(r), Some(c)) => Some(r.min(c)),
144 (None, Some(c)) => Some(c),
145 (Some(r), None) => Some(r),
146 (None, None) => None,
147 };
148 Policy {
149 allow_effects,
150 allow_fs_read: ceiling.allow_fs_read.clone(),
151 allow_fs_write: ceiling.allow_fs_write.clone(),
152 allow_net_host: ceiling.allow_net_host.clone(),
153 allow_proc: ceiling.allow_proc.clone(),
154 allow_approval: ceiling.allow_approval.clone(),
155 budget,
156 }
157}
158
159fn validate_tenant_id(tenant_id: &str) -> anyhow::Result<()> {
160 if tenant_id.is_empty() {
161 anyhow::bail!("tenant_id must not be empty");
162 }
163 if tenant_id.len() > 64 {
164 anyhow::bail!("tenant_id must be at most 64 bytes");
165 }
166 if !tenant_id
167 .bytes()
168 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
169 {
170 anyhow::bail!(
171 "tenant_id {tenant_id:?} contains characters outside [A-Za-z0-9_-]"
172 );
173 }
174 Ok(())
175}
176
177#[derive(Debug, Serialize, Deserialize)]
178struct ErrorEnvelope {
179 error: String,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 detail: Option<serde_json::Value>,
182}
183
184fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
185 let bytes = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
186 Response::from_data(bytes)
187 .with_status_code(status)
188 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
189}
190
191fn error_response(status: u16, msg: impl Into<String>) -> Response<std::io::Cursor<Vec<u8>>> {
192 json_response(status, &serde_json::to_value(ErrorEnvelope {
193 error: msg.into(), detail: None,
194 }).unwrap())
195}
196
197fn error_with_detail(status: u16, msg: impl Into<String>, detail: serde_json::Value)
198 -> Response<std::io::Cursor<Vec<u8>>>
199{
200 json_response(status, &serde_json::to_value(ErrorEnvelope {
201 error: msg.into(), detail: Some(detail),
202 }).unwrap())
203}
204
205fn write_error_response(prefix: &str, err: lex_store::StoreError)
211 -> Response<std::io::Cursor<Vec<u8>>>
212{
213 if let lex_store::StoreError::Contention { branch, attempts } = &err {
214 let body = serde_json::to_vec(&ErrorEnvelope {
215 error: format!("{prefix}: branch '{branch}' is contended (attempts={attempts})"),
216 detail: Some(serde_json::json!({
217 "kind": "contention",
218 "branch": branch,
219 "attempts": attempts,
220 })),
221 }).unwrap_or_else(|_| b"{}".to_vec());
222 return Response::from_data(body)
223 .with_status_code(503)
224 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
225 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"1"[..]).unwrap());
226 }
227 if let lex_store::StoreError::BudgetExceeded { session_id, cap, spent_after } = &err {
235 let body = serde_json::to_vec(&ErrorEnvelope {
236 error: format!(
237 "{prefix}: session `{session_id}` budget exceeded \
238 (spent_after={spent_after}, cap={cap})"
239 ),
240 detail: Some(serde_json::json!({
241 "kind": "budget_exceeded",
242 "session_id": session_id,
243 "cap": cap,
244 "spent_after": spent_after,
245 })),
246 }).unwrap_or_else(|_| b"{}".to_vec());
247 return Response::from_data(body)
248 .with_status_code(503)
249 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
250 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"0"[..]).unwrap());
251 }
252 error_response(500, format!("{prefix}: {err}"))
253}
254
255pub fn handle(state: Arc<State>, mut req: Request) -> std::io::Result<()> {
256 let method = req.method().clone();
257 let url = req.url().to_string();
258 let path = url.split('?').next().unwrap_or("").to_string();
259 let query = url.split_once('?').map(|(_, q)| q.to_string()).unwrap_or_default();
260
261 let x_lex_user = req.headers().iter()
266 .find(|h| h.field.equiv("x-lex-user"))
267 .map(|h| h.value.as_str().to_string());
268
269 if matches!(method, Method::Post) && path == "/v1/pkg/publish" {
271 let mut body_bytes: Vec<u8> = Vec::new();
272 let _ = req.as_reader().read_to_end(&mut body_bytes);
273 let resp = pkg_publish_handler(&state, &body_bytes);
274 return req.respond(resp);
275 }
276
277 let mut body = String::new();
278 let _ = req.as_reader().read_to_string(&mut body);
279
280 let resp = route(&state, &method, &path, &query, &body, x_lex_user.as_deref());
281 req.respond(resp)
282}
283
284pub fn handle_with_auth<F>(state: Arc<State>, req: Request, auth: F) -> std::io::Result<()>
288where
289 F: FnOnce(&str, &[Header]) -> bool,
290{
291 let path = req.url().split('?').next().unwrap_or("").to_string();
292 if !auth(&path, req.headers()) {
293 return req.respond(
294 Response::from_data(br#"{"error":"unauthorized"}"#.to_vec())
295 .with_status_code(401)
296 .with_header(
297 Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
298 ),
299 );
300 }
301 handle(state, req)
302}
303
304fn route(
305 state: &State,
306 method: &Method,
307 path: &str,
308 query: &str,
309 body: &str,
310 x_lex_user: Option<&str>,
311) -> Response<std::io::Cursor<Vec<u8>>> {
312 match (method, path) {
313 (Method::Get, "/") => crate::web::activity_handler(state),
315 (Method::Get, "/web/branches") => crate::web::branches_handler(state),
316 (Method::Get, "/web/trust") => crate::web::trust_handler(state),
317 (Method::Get, "/web/attention") => crate::web::attention_handler(state),
318 (Method::Get, p) if p.starts_with("/web/branch/") => {
319 let name = &p["/web/branch/".len()..];
320 crate::web::branch_handler(state, name)
321 }
322 (Method::Get, p) if p.starts_with("/web/stage/") => {
323 let id = &p["/web/stage/".len()..];
324 crate::web::stage_html_handler(state, id)
325 }
326 (Method::Post, p) if p.starts_with("/web/stage/") && (
331 p.ends_with("/pin") || p.ends_with("/defer")
332 || p.ends_with("/block") || p.ends_with("/unblock")
333 ) => {
334 let prefix_len = "/web/stage/".len();
335 let last_slash = p.rfind('/').unwrap_or(p.len());
336 let id = &p[prefix_len..last_slash];
337 let verb = &p[last_slash + 1..];
338 let decision = match verb {
339 "pin" => crate::web::WebStageDecision::Pin,
340 "defer" => crate::web::WebStageDecision::Defer,
341 "block" => crate::web::WebStageDecision::Block,
342 "unblock" => crate::web::WebStageDecision::Unblock,
343 _ => unreachable!("matched in outer guard"),
344 };
345 crate::web::stage_decision_handler(state, id, body, decision, x_lex_user)
346 }
347 (Method::Get, "/v1/health") => json_response(200, &serde_json::json!({"ok": true})),
349 (Method::Post, "/v1/parse") => parse_handler(body),
350 (Method::Post, "/v1/check") => check_handler(body),
351 (Method::Post, "/v1/publish") => publish_handler(state, body),
352 (Method::Post, "/v1/patch") => patch_handler(state, body),
353 (Method::Get, p) if p.starts_with("/v1/stage/") => {
354 let suffix = &p["/v1/stage/".len()..];
355 if let Some(id) = suffix.strip_suffix("/attestations") {
358 stage_attestations_handler(state, id)
359 } else {
360 stage_handler(state, suffix)
361 }
362 }
363 (Method::Post, "/v1/run") => run_handler(state, body, false),
364 (Method::Post, "/v1/replay") => run_handler(state, body, true),
365 (Method::Get, p) if p.starts_with("/v1/trace/") => {
366 let id = &p["/v1/trace/".len()..];
367 trace_handler(state, id)
368 }
369 (Method::Get, "/v1/diff") => diff_handler(state, query),
370 (Method::Post, "/v1/merge/start") => merge_start_handler(state, body),
371 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/resolve") => {
372 let id = &p["/v1/merge/".len()..p.len() - "/resolve".len()];
373 merge_resolve_handler(state, id, body)
374 }
375 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/commit") => {
376 let id = &p["/v1/merge/".len()..p.len() - "/commit".len()];
377 merge_commit_handler(state, id)
378 }
379 (Method::Post, "/v1/ops/batch") => ops_batch_handler(state, body),
381 (Method::Post, "/v1/attestations/batch") => attestations_batch_handler(state, body),
382 (Method::Get, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
386 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
387 branch_head_handler(state, name)
388 }
389 (Method::Get, "/v1/ops/since") => ops_since_handler(state, query),
393 (Method::Get, "/v1/attestations/since") => attestations_since_handler(state, query),
394 (Method::Get, "/v1/pkg") => pkg_list_handler(state),
398 (Method::Put, p) if p.starts_with("/v1/pkg/") && p.ends_with("/visibility") => {
402 let name = &p["/v1/pkg/".len()..p.len() - "/visibility".len()];
403 pkg_set_visibility_handler(state, name, body)
404 }
405 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/head") => {
406 let name = &p["/v1/pkg/".len()..p.len() - "/head".len()];
407 pkg_head_handler(state, name)
408 }
409 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/versions") => {
410 let name = &p["/v1/pkg/".len()..p.len() - "/versions".len()];
411 pkg_versions_handler(state, name)
412 }
413 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/archive") => {
415 let inner = &p["/v1/pkg/".len()..p.len() - "/archive".len()];
416 if let Some((name, version)) = inner.split_once('/') {
418 pkg_archive_handler(state, name, version)
419 } else {
420 error_response(400, "expected /v1/pkg/{name}/{version}/archive")
421 }
422 }
423 (Method::Get, p) if p.starts_with("/v1/pkg/") && p["/v1/pkg/".len()..].contains('/') => {
425 let inner = &p["/v1/pkg/".len()..];
426 if let Some((name, version)) = inner.split_once('/') {
427 pkg_get_version_handler(state, name, version)
428 } else {
429 error_response(400, "expected /v1/pkg/{name}/{version}")
430 }
431 }
432 (Method::Get, p) if p.starts_with("/v1/pkg/") => {
433 let name = &p["/v1/pkg/".len()..];
434 pkg_get_handler(state, name)
435 }
436 (Method::Delete, p) if p.starts_with("/v1/pkg/") => {
437 let name = &p["/v1/pkg/".len()..];
438 pkg_delete_handler(state, name)
439 }
440 _ => error_response(404, format!("unknown route: {method:?} {path}")),
441 }
442}
443
444#[derive(Deserialize)]
445struct ParseReq { source: String }
446
447fn parse_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
448 let req: ParseReq = match serde_json::from_str(body) {
449 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
450 };
451 match load_program_from_str(&req.source) {
452 Ok(prog) => {
453 let stages = canonicalize_program(&prog);
454 json_response(200, &serde_json::to_value(&stages).unwrap())
455 }
456 Err(e) => error_response(400, format!("syntax error: {e}")),
457 }
458}
459
460pub(crate) fn check_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
461 let req: ParseReq = match serde_json::from_str(body) {
462 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
463 };
464 let prog = match load_program_from_str(&req.source) {
465 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
466 };
467 let stages = canonicalize_program(&prog);
468 match lex_types::check_program(&stages) {
469 Ok(_) => json_response(200, &serde_json::json!({"ok": true})),
470 Err(errs) => json_response(422, &serde_json::to_value(&errs).unwrap()),
471 }
472}
473
474#[derive(Deserialize)]
475struct PublishReq { source: String, #[serde(default)] activate: bool }
476
477pub(crate) fn publish_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
478 let req: PublishReq = match serde_json::from_str(body) {
479 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
480 };
481 let prog = match load_program_from_str(&req.source) {
482 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
483 };
484 let mut stages = canonicalize_program(&prog);
488 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
489 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
490 }
491 let example_errors = lex_runtime::evaluate_examples(&stages);
497 if !example_errors.is_empty() {
498 return error_with_detail(422, "example mismatch",
499 serde_json::to_value(&example_errors).unwrap_or_default());
500 }
501
502 let store = state.store.lock().unwrap();
503 let branch = store.current_branch();
504
505 let old_head = match store.branch_head(&branch) {
507 Ok(h) => h,
508 Err(e) => return error_response(500, format!("branch_head: {e}")),
509 };
510 let old_fns: std::collections::BTreeMap<String, lex_ast::FnDecl> = old_head.values()
511 .filter_map(|stg| store.get_ast(stg).ok())
512 .filter_map(|s| match s {
513 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd)),
514 _ => None,
515 })
516 .collect();
517 let new_fns: std::collections::BTreeMap<String, lex_ast::FnDecl> = stages.iter()
518 .filter_map(|s| match s {
519 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
520 _ => None,
521 })
522 .collect();
523 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
524
525 let mut new_imports: lex_vcs::ImportMap = lex_vcs::ImportMap::new();
527 {
528 let entry = new_imports.entry("<source>".into()).or_default();
529 for s in &stages {
530 if let lex_ast::Stage::Import(im) = s {
531 entry.insert(im.reference.clone());
532 }
533 }
534 }
535
536 match store.publish_program(&branch, &stages, &report, &new_imports, req.activate) {
537 Ok(outcome) => {
538 record_examples_for_publish(&store, &stages, &outcome);
542 json_response(200, &serde_json::json!({
543 "ops": outcome.ops,
544 "head_op": outcome.head_op,
545 }))
546 }
547 Err(lex_store::StoreError::TypeError(errs)) => {
555 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
556 }
557 Err(e) => write_error_response("publish_program", e),
558 }
559}
560
561#[derive(Deserialize)]
562struct PatchReq {
563 stage_id: String,
564 patch: lex_ast::Patch,
565 #[serde(default)] activate: bool,
566}
567
568fn patch_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
571 let req: PatchReq = match serde_json::from_str(body) {
572 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
573 };
574 let store = state.store.lock().unwrap();
575
576 let original = match store.get_ast(&req.stage_id) {
578 Ok(s) => s, Err(e) => return error_response(404, format!("stage: {e}")),
579 };
580
581 let patched = match lex_ast::apply_patch(&original, &req.patch) {
583 Ok(s) => s,
584 Err(e) => return error_with_detail(422, "patch failed",
585 serde_json::to_value(&e).unwrap_or_default()),
586 };
587
588 let branch = store.current_branch();
599
600 let sig = match lex_ast::sig_id(&patched) {
602 Some(s) => s,
603 None => return error_response(500, "patched stage has no sig_id"),
604 };
605
606 let new_id = match store.publish(&patched) {
609 Ok(id) => id, Err(e) => return error_response(500, format!("publish: {e}")),
610 };
611
612 let original_effects: std::collections::BTreeSet<String> = match &original {
614 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
615 _ => std::collections::BTreeSet::new(),
616 };
617 let patched_effects: std::collections::BTreeSet<String> = match &patched {
618 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
619 _ => std::collections::BTreeSet::new(),
620 };
621 let head_now = match store.get_branch(&branch) {
622 Ok(b) => b.and_then(|b| b.head_op),
623 Err(e) => return error_response(500, format!("get_branch: {e}")),
624 };
625 let kind = if original_effects != patched_effects {
626 let from_budget = lex_vcs::operation_budget_from_effects(&original_effects);
633 let to_budget = lex_vcs::operation_budget_from_effects(&patched_effects);
634 lex_vcs::OperationKind::ChangeEffectSig {
635 sig_id: sig.clone(),
636 from_stage_id: req.stage_id.clone(),
637 to_stage_id: new_id.clone(),
638 from_effects: original_effects,
639 to_effects: patched_effects,
640 from_budget,
641 to_budget,
642 }
643 } else {
644 let budget = lex_vcs::operation_budget_from_effects(&original_effects);
645 lex_vcs::OperationKind::ModifyBody {
646 sig_id: sig.clone(),
647 from_stage_id: req.stage_id.clone(),
648 to_stage_id: new_id.clone(),
649 from_budget: budget,
650 to_budget: budget,
651 }
652 };
653 let transition = lex_vcs::StageTransition::Replace {
654 sig_id: sig.clone(),
655 from: req.stage_id.clone(),
656 to: new_id.clone(),
657 };
658 let op = lex_vcs::Operation::new(
659 kind,
660 head_now.into_iter().collect::<Vec<_>>(),
661 );
662 let op_id = match store.apply_operation_gated(&branch, op, transition) {
663 Ok(id) => id,
664 Err(lex_store::StoreError::TypeError(errs)) => return error_with_detail(
665 422, "type errors after patch", serde_json::to_value(&errs).unwrap_or_default()),
666 Err(e) => return write_error_response("apply_operation_gated", e),
667 };
668 if req.activate {
669 if let Err(e) = store.activate(&new_id) {
670 return error_response(500, format!("activate: {e}"));
671 }
672 }
673
674 let status = format!("{:?}",
675 store.get_status(&new_id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
676 json_response(200, &serde_json::json!({
677 "old_stage_id": req.stage_id,
678 "new_stage_id": new_id,
679 "sig_id": sig,
680 "status": status,
681 "op_id": op_id,
682 }))
683}
684
685pub(crate) fn stage_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
686 let store = state.store.lock().unwrap();
687 let meta = match store.get_metadata(id) {
688 Ok(m) => m, Err(e) => return error_response(404, format!("{e}")),
689 };
690 let ast = match store.get_ast(id) {
691 Ok(a) => a, Err(e) => return error_response(404, format!("{e}")),
692 };
693 let status = format!("{:?}", store.get_status(id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
694 json_response(200, &serde_json::json!({
695 "metadata": meta,
696 "ast": ast,
697 "status": status,
698 }))
699}
700
701pub(crate) fn stage_attestations_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
710 let store = state.store.lock().unwrap();
711 if let Err(e) = store.get_metadata(id) {
712 return error_response(404, format!("{e}"));
713 }
714 let log = match store.attestation_log() {
715 Ok(l) => l,
716 Err(e) => return error_response(500, format!("attestation log: {e}")),
717 };
718 let mut listing = match log.list_for_stage(&id.to_string()) {
719 Ok(v) => v,
720 Err(e) => return error_response(500, format!("list_for_stage: {e}")),
721 };
722 listing.sort_by_key(|a| std::cmp::Reverse(a.timestamp));
723 json_response(200, &serde_json::json!({"attestations": listing}))
724}
725
726#[derive(Deserialize, Default)]
727struct PolicyJson {
728 #[serde(default)] allow_effects: Vec<String>,
729 #[serde(default)] allow_fs_read: Vec<String>,
730 #[serde(default)] allow_fs_write: Vec<String>,
731 #[serde(default)] budget: Option<u64>,
732}
733
734impl PolicyJson {
735 fn into_policy(self) -> Policy {
736 Policy {
737 allow_effects: self.allow_effects.into_iter().collect::<BTreeSet<_>>(),
738 allow_fs_read: self.allow_fs_read.into_iter().map(PathBuf::from).collect(),
739 allow_fs_write: self.allow_fs_write.into_iter().map(PathBuf::from).collect(),
740 allow_net_host: Vec::new(),
741 allow_proc: Vec::new(),
742 allow_approval: Vec::new(),
743 budget: self.budget,
744 }
745 }
746}
747
748#[derive(Deserialize)]
749struct RunReq {
750 source: String,
751 #[serde(rename = "fn")] func: String,
752 #[serde(default)] args: Vec<serde_json::Value>,
753 #[serde(default)] policy: PolicyJson,
754 #[serde(default)] overrides: IndexMap<String, serde_json::Value>,
755}
756
757pub(crate) fn run_handler(state: &State, body: &str, with_overrides: bool) -> Response<std::io::Cursor<Vec<u8>>> {
758 let req: RunReq = match serde_json::from_str(body) {
759 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
760 };
761 let prog = match load_program_from_str(&req.source) {
762 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
763 };
764 let stages = canonicalize_program(&prog);
765 if let Err(errs) = lex_types::check_program(&stages) {
766 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
767 }
768 let bc = compile_program(&stages);
769 let mut policy = req.policy.into_policy();
770 if let Some(ceiling) = &state.policy_ceiling {
776 policy = clamp_policy(policy, ceiling);
777 }
778 if let Err(violations) = check_policy(&bc, &policy) {
779 return error_with_detail(403, "policy violation", serde_json::to_value(&violations).unwrap());
780 }
781
782 let mut recorder = lex_trace::Recorder::new();
783 if with_overrides && !req.overrides.is_empty() {
784 recorder = recorder.with_overrides(req.overrides);
785 }
786 let handle = recorder.handle();
787 let handler = DefaultHandler::new(policy);
788 let mut vm = Vm::with_handler(&bc, Box::new(handler));
789 vm.set_tracer(Box::new(recorder));
790
791 let vargs: Vec<Value> = req.args.iter().map(json_to_value).collect();
792 let started = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
793 let result = vm.call(&req.func, vargs);
794 let ended = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
795
796 let store = state.store.lock().unwrap();
797 let (root_out, root_err, status) = match &result {
798 Ok(v) => (Some(value_to_json(v)), None, 200u16),
799 Err(e) => (None, Some(format!("{e}")), 200u16),
800 };
801 let tree = handle.finalize(req.func.clone(), serde_json::Value::Null,
802 root_out.clone(), root_err.clone(), started, ended);
803 let run_id = match store.save_trace(&tree) {
804 Ok(id) => id,
805 Err(e) => return error_response(500, format!("save_trace: {e}")),
806 };
807
808 let mut body = serde_json::json!({
809 "run_id": run_id,
810 "output": root_out,
811 });
812 if let Some(err) = root_err {
813 body["error"] = serde_json::Value::String(err);
814 }
815 json_response(status, &body)
816}
817
818fn trace_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
819 let store = state.store.lock().unwrap();
820 match store.load_trace(id) {
821 Ok(t) => json_response(200, &serde_json::to_value(&t).unwrap()),
822 Err(e) => error_response(404, format!("{e}")),
823 }
824}
825
826fn diff_handler(state: &State, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
827 let mut a = None;
828 let mut b = None;
829 for kv in query.split('&') {
830 if let Some((k, v)) = kv.split_once('=') {
831 match k { "a" => a = Some(v.to_string()), "b" => b = Some(v.to_string()), _ => {} }
832 }
833 }
834 let (Some(a), Some(b)) = (a, b) else {
835 return error_response(400, "missing a or b query params");
836 };
837 let store = state.store.lock().unwrap();
838 let ta = match store.load_trace(&a) { Ok(t) => t, Err(e) => return error_response(404, format!("a: {e}")) };
839 let tb = match store.load_trace(&b) { Ok(t) => t, Err(e) => return error_response(404, format!("b: {e}")) };
840 match lex_trace::diff_runs(&ta, &tb) {
841 Some(d) => json_response(200, &serde_json::to_value(&d).unwrap()),
842 None => json_response(200, &serde_json::json!({"divergence": null})),
843 }
844}
845
846fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
847
848fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
849
850#[derive(Deserialize)]
851struct MergeStartReq {
852 src_branch: String,
853 dst_branch: String,
854}
855
856fn merge_start_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
867 let req: MergeStartReq = match serde_json::from_str(body) {
868 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
869 };
870 let store = state.store.lock().unwrap();
871 let src_head = match store.get_branch(&req.src_branch) {
872 Ok(Some(b)) => b.head_op,
873 Ok(None) => return error_response(404, format!("unknown src branch `{}`", req.src_branch)),
874 Err(e) => return error_response(500, format!("src branch read: {e}")),
875 };
876 let dst_head = match store.get_branch(&req.dst_branch) {
877 Ok(Some(b)) => b.head_op,
878 Ok(None) => return error_response(404, format!("unknown dst branch `{}`", req.dst_branch)),
879 Err(e) => return error_response(500, format!("dst branch read: {e}")),
880 };
881 let log = match lex_vcs::OpLog::open(store.root()) {
882 Ok(l) => l,
883 Err(e) => return error_response(500, format!("op log: {e}")),
884 };
885 let merge_id = mint_merge_id();
889 let session = match MergeSession::start(
890 merge_id.clone(),
891 &log,
892 src_head.as_ref(),
893 dst_head.as_ref(),
894 ) {
895 Ok(s) => s,
896 Err(e) => return error_response(500, format!("merge start: {e}")),
897 };
898 let conflicts: Vec<&lex_vcs::ConflictRecord> = session.remaining_conflicts();
899 let auto_resolved_count = session.auto_resolved.len();
900 let body = serde_json::json!({
901 "merge_id": merge_id,
902 "src_head": session.src_head,
903 "dst_head": session.dst_head,
904 "lca": session.lca,
905 "conflicts": conflicts,
906 "auto_resolved_count": auto_resolved_count,
907 });
908 drop(conflicts);
909 drop(store);
910 let wrapped = ApiMergeSession {
911 inner: session,
912 src_branch: req.src_branch,
913 dst_branch: req.dst_branch,
914 };
915 state.sessions.lock().unwrap().insert(merge_id, wrapped);
916 json_response(200, &body)
917}
918
919#[derive(Deserialize)]
920struct MergeResolveReq {
921 resolutions: Vec<MergeResolveEntry>,
926}
927
928#[derive(Deserialize)]
929struct MergeResolveEntry {
930 conflict_id: String,
931 resolution: lex_vcs::Resolution,
932}
933
934fn merge_resolve_handler(
945 state: &State,
946 merge_id: &str,
947 body: &str,
948) -> Response<std::io::Cursor<Vec<u8>>> {
949 let req: MergeResolveReq = match serde_json::from_str(body) {
950 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
951 };
952 let mut sessions = state.sessions.lock().unwrap();
953 let Some(wrapped) = sessions.get_mut(merge_id) else {
954 return error_response(404, format!("unknown merge_id `{merge_id}`"));
955 };
956 let pairs: Vec<(String, lex_vcs::Resolution)> = req.resolutions.into_iter()
957 .map(|e| (e.conflict_id, e.resolution))
958 .collect();
959 let verdicts = wrapped.inner.resolve(pairs);
960 let remaining: Vec<&lex_vcs::ConflictRecord> = wrapped.inner.remaining_conflicts();
961 let body = serde_json::json!({
962 "verdicts": verdicts,
963 "remaining_conflicts": remaining,
964 });
965 json_response(200, &body)
966}
967
968fn merge_commit_handler(
987 state: &State,
988 merge_id: &str,
989) -> Response<std::io::Cursor<Vec<u8>>> {
990 use std::collections::BTreeMap;
991 let wrapped = match state.sessions.lock().unwrap().remove(merge_id) {
992 Some(w) => w,
993 None => return error_response(404, format!("unknown merge_id `{merge_id}`")),
994 };
995 let dst_branch = wrapped.dst_branch.clone();
996 let src_head = wrapped.inner.src_head.clone();
997 let dst_head = wrapped.inner.dst_head.clone();
998 let auto_resolved = wrapped.inner.auto_resolved.clone();
999
1000 let mut entries: BTreeMap<lex_vcs::SigId, Option<lex_vcs::StageId>> = BTreeMap::new();
1003
1004 for outcome in &auto_resolved {
1006 if let lex_vcs::MergeOutcome::Src { sig_id, stage_id } = outcome {
1007 entries.insert(sig_id.clone(), stage_id.clone());
1008 }
1009 }
1010
1011 let resolved = match wrapped.inner.commit() {
1013 Ok(r) => r,
1014 Err(lex_vcs::CommitError::ConflictsRemaining(ids)) => {
1015 return error_with_detail(
1019 422,
1020 "conflicts remaining",
1021 serde_json::json!({"unresolved": ids}),
1022 );
1023 }
1024 };
1025
1026 for (conflict_id, resolution) in resolved {
1027 match resolution {
1028 lex_vcs::Resolution::TakeOurs => {
1029 }
1031 lex_vcs::Resolution::TakeTheirs => {
1032 match resolve_take_theirs(state, &src_head, &conflict_id) {
1042 Ok(stage_id) => {
1043 entries.insert(conflict_id.clone(), stage_id);
1044 }
1045 Err(e) => return error_response(500, format!("resolve take_theirs: {e}")),
1046 }
1047 }
1048 lex_vcs::Resolution::Custom { op } => {
1049 match op.kind.merge_target() {
1058 Some((sig, stage)) => {
1059 if sig != conflict_id {
1060 return error_with_detail(
1061 422,
1062 "custom op targets a different sig than the conflict",
1063 serde_json::json!({
1064 "conflict_id": conflict_id,
1065 "op_targets": sig,
1066 }),
1067 );
1068 }
1069 entries.insert(conflict_id, stage);
1070 }
1071 None => {
1072 return error_with_detail(
1073 422,
1074 "custom op kind doesn't yield a single sig→stage delta",
1075 serde_json::json!({
1076 "conflict_id": conflict_id,
1077 "kind": serde_json::to_value(&op.kind).unwrap_or(serde_json::Value::Null),
1078 }),
1079 );
1080 }
1081 }
1082 }
1083 lex_vcs::Resolution::Defer => {
1084 return error_response(500, "internal: Defer slipped past commit gate");
1086 }
1087 }
1088 }
1089
1090 let resolved_count = entries.len();
1091 let mut parents: Vec<lex_vcs::OpId> = Vec::new();
1092 if let Some(d) = dst_head { parents.push(d); }
1093 if let Some(s) = src_head { parents.push(s); }
1094 let op = lex_vcs::Operation::new(
1095 lex_vcs::OperationKind::Merge { resolved: resolved_count },
1096 parents,
1097 );
1098 let transition = lex_vcs::StageTransition::Merge { entries };
1099 let store = state.store.lock().unwrap();
1100 match store.apply_merge_op_gated(&dst_branch, op, transition) {
1103 Ok(new_head_op) => json_response(200, &serde_json::json!({
1104 "new_head_op": new_head_op,
1105 "dst_branch": dst_branch,
1106 })),
1107 Err(lex_store::StoreError::TypeError(errs)) => error_with_detail(
1108 422, "merged program has type errors", serde_json::to_value(&errs).unwrap_or_default()),
1109 Err(e) => write_error_response("apply merge op", e),
1110 }
1111}
1112
1113fn resolve_take_theirs(
1118 state: &State,
1119 src_head: &Option<lex_vcs::OpId>,
1120 sig: &lex_vcs::SigId,
1121) -> std::io::Result<Option<lex_vcs::StageId>> {
1122 let store = state.store.lock().unwrap();
1123 let log = lex_vcs::OpLog::open(store.root())?;
1124 let Some(head) = src_head.as_ref() else { return Ok(None); };
1125 let mut current: Option<lex_vcs::StageId> = None;
1128 for record in log.walk_forward(head, None)? {
1129 match &record.produces {
1130 lex_vcs::StageTransition::Create { sig_id, stage_id }
1131 if sig_id == sig => { current = Some(stage_id.clone()); }
1132 lex_vcs::StageTransition::Replace { sig_id, to, .. }
1133 if sig_id == sig => { current = Some(to.clone()); }
1134 lex_vcs::StageTransition::Remove { sig_id, .. }
1135 if sig_id == sig => { current = None; }
1136 lex_vcs::StageTransition::Rename { from, to, body_stage_id }
1137 if from == sig || to == sig => {
1138 if from == sig { current = None; }
1139 if to == sig { current = Some(body_stage_id.clone()); }
1140 }
1141 lex_vcs::StageTransition::Merge { entries } => {
1142 if let Some(opt) = entries.get(sig) {
1143 current = opt.clone();
1144 }
1145 }
1146 _ => {}
1147 }
1148 }
1149 Ok(current)
1150}
1151
1152fn mint_merge_id() -> MergeSessionId {
1153 use std::sync::atomic::{AtomicU64, Ordering};
1154 static COUNTER: AtomicU64 = AtomicU64::new(0);
1155 let nanos = SystemTime::now()
1156 .duration_since(UNIX_EPOCH)
1157 .map(|d| d.as_nanos())
1158 .unwrap_or(0);
1159 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1160 format!("merge_{nanos:x}_{n:x}")
1161}
1162
1163pub(crate) fn ops_batch_handler(state: &State, body: &str)
1194 -> Response<std::io::Cursor<Vec<u8>>>
1195{
1196 let records: Vec<lex_vcs::OperationRecord> = match serde_json::from_str(body) {
1197 Ok(r) => r,
1198 Err(e) => return error_response(400,
1199 format!("body must be a JSON array of OperationRecord: {e}")),
1200 };
1201 let store = state.store.lock().unwrap();
1202 let log = match lex_vcs::OpLog::open(store.root()) {
1203 Ok(l) => l,
1204 Err(e) => return error_response(500, format!("opening op log: {e}")),
1205 };
1206
1207 let mut batch_ids: std::collections::BTreeSet<lex_vcs::OpId> =
1215 std::collections::BTreeSet::new();
1216 for rec in &records {
1217 let expected = rec.op.op_id();
1218 if expected != rec.op_id {
1219 return error_with_detail(409, "OpIdMismatch", serde_json::json!({
1220 "supplied": rec.op_id,
1221 "expected": expected,
1222 }));
1223 }
1224 for parent in &rec.op.parents {
1225 let known = match log.get(parent) {
1226 Ok(Some(_)) => true,
1227 Ok(None) => false,
1228 Err(e) => return error_response(500, format!("op log read: {e}")),
1229 };
1230 if !known && !batch_ids.contains(parent) {
1231 return error_with_detail(422, "MissingParent", serde_json::json!({
1232 "op_id": rec.op_id,
1233 "missing_parent": parent,
1234 }));
1235 }
1236 }
1237 batch_ids.insert(rec.op_id.clone());
1238 }
1239
1240 let mut added = 0usize;
1243 let mut added_ids: Vec<&lex_vcs::OpId> = Vec::new();
1244 for rec in &records {
1245 let already_present = matches!(log.get(&rec.op_id), Ok(Some(_)));
1246 match log.put(rec) {
1247 Ok(()) => {
1248 if !already_present {
1249 added += 1;
1250 added_ids.push(&rec.op_id);
1251 }
1252 }
1253 Err(e) => return error_response(500, format!("op log write: {e}")),
1254 }
1255 }
1256
1257 json_response(200, &serde_json::json!({
1258 "received": records.len(),
1259 "added": added,
1260 "skipped": records.len() - added,
1261 "added_ids": added_ids,
1262 }))
1263}
1264
1265pub(crate) fn attestations_batch_handler(state: &State, body: &str)
1287 -> Response<std::io::Cursor<Vec<u8>>>
1288{
1289 let attestations: Vec<lex_vcs::Attestation> = match serde_json::from_str(body) {
1290 Ok(a) => a,
1291 Err(e) => return error_response(400,
1292 format!("body must be a JSON array of Attestation: {e}")),
1293 };
1294 let store = state.store.lock().unwrap();
1295 let log = match store.attestation_log() {
1296 Ok(l) => l,
1297 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1298 };
1299 let op_log = match lex_vcs::OpLog::open(store.root()) {
1300 Ok(l) => l,
1301 Err(e) => return error_response(500, format!("opening op log: {e}")),
1302 };
1303
1304 for att in &attestations {
1306 let expected = lex_vcs::Attestation::with_timestamp(
1309 att.stage_id.clone(),
1310 att.op_id.clone(),
1311 att.intent_id.clone(),
1312 att.kind.clone(),
1313 att.result.clone(),
1314 att.produced_by.clone(),
1315 att.cost.clone(),
1316 att.timestamp,
1317 ).attestation_id;
1318 if expected != att.attestation_id {
1319 return error_with_detail(409, "AttestationIdMismatch", serde_json::json!({
1320 "supplied": att.attestation_id,
1321 "expected": expected,
1322 }));
1323 }
1324 if let Some(op_id) = &att.op_id {
1328 match op_log.get(op_id) {
1329 Ok(Some(_)) => {}
1330 Ok(None) => return error_with_detail(422, "UnknownOp", serde_json::json!({
1331 "attestation_id": att.attestation_id,
1332 "op_id": op_id,
1333 })),
1334 Err(e) => return error_response(500, format!("op log read: {e}")),
1335 }
1336 }
1337 }
1338
1339 let mut added = 0usize;
1343 let mut added_ids: Vec<&lex_vcs::AttestationId> = Vec::new();
1344 for att in &attestations {
1345 let already_present = matches!(log.get(&att.attestation_id), Ok(Some(_)));
1346 match log.put(att) {
1347 Ok(()) => {
1348 if !already_present {
1349 added += 1;
1350 added_ids.push(&att.attestation_id);
1351 }
1352 }
1353 Err(e) => return error_response(500, format!("attestation log write: {e}")),
1354 }
1355 }
1356
1357 json_response(200, &serde_json::json!({
1358 "received": attestations.len(),
1359 "added": added,
1360 "skipped": attestations.len() - added,
1361 "added_ids": added_ids,
1362 }))
1363}
1364
1365pub(crate) fn branch_head_handler(state: &State, name: &str)
1374 -> Response<std::io::Cursor<Vec<u8>>>
1375{
1376 let store = state.store.lock().unwrap();
1377 let head = match store.get_branch(name) {
1378 Ok(Some(b)) => b.head_op,
1379 Ok(None) => None,
1380 Err(e) => return error_response(500, format!("get_branch: {e}")),
1381 };
1382 json_response(200, &serde_json::json!({
1383 "branch": name,
1384 "head_op": head,
1385 }))
1386}
1387
1388pub(crate) fn ops_since_handler(state: &State, query: &str)
1412 -> Response<std::io::Cursor<Vec<u8>>>
1413{
1414 let mut after: Option<String> = None;
1415 let mut branch = String::from("main");
1416 let mut limit: Option<usize> = None;
1417 for kv in query.split('&') {
1418 let Some((k, v)) = kv.split_once('=') else { continue };
1419 match k {
1420 "after" => after = Some(v.to_string()),
1421 "branch" => branch = v.to_string(),
1422 "limit" => {
1423 limit = Some(match v.parse::<usize>() {
1424 Ok(n) => n,
1425 Err(_) => return error_response(400,
1426 format!("limit must be a positive integer, got `{v}`")),
1427 });
1428 }
1429 _ => {}
1430 }
1431 }
1432
1433 let store = state.store.lock().unwrap();
1434 let log = match lex_vcs::OpLog::open(store.root()) {
1435 Ok(l) => l,
1436 Err(e) => return error_response(500, format!("opening op log: {e}")),
1437 };
1438 let head = match store.get_branch(&branch) {
1439 Ok(Some(b)) => b.head_op,
1440 Ok(None) => None,
1441 Err(e) => return error_response(500, format!("get_branch: {e}")),
1442 };
1443 let Some(head) = head else {
1444 return json_response(200, &serde_json::json!([]));
1445 };
1446
1447 let ops_since = match log.ops_since(&head, after.as_ref()) {
1448 Ok(o) => o,
1449 Err(e) => return error_response(500, format!("ops_since: {e}")),
1450 };
1451 let mut ops = ops_since;
1455 ops.reverse();
1456 if let Some(n) = limit {
1457 ops.truncate(n);
1458 }
1459
1460 json_response(200, &serde_json::to_value(&ops).unwrap_or_default())
1461}
1462
1463pub(crate) fn attestations_since_handler(state: &State, query: &str)
1476 -> Response<std::io::Cursor<Vec<u8>>>
1477{
1478 let mut after_op: Option<String> = None;
1479 let mut limit: Option<usize> = None;
1480 for kv in query.split('&') {
1481 let Some((k, v)) = kv.split_once('=') else { continue };
1482 match k {
1483 "after-op" => after_op = Some(v.to_string()),
1484 "limit" => {
1485 limit = Some(match v.parse::<usize>() {
1486 Ok(n) => n,
1487 Err(_) => return error_response(400,
1488 format!("limit must be a positive integer, got `{v}`")),
1489 });
1490 }
1491 _ => {}
1492 }
1493 }
1494
1495 let store = state.store.lock().unwrap();
1496 let log = match store.attestation_log() {
1497 Ok(l) => l,
1498 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1499 };
1500
1501 let exclude: std::collections::BTreeSet<String> = match &after_op {
1505 None => std::collections::BTreeSet::new(),
1506 Some(cutoff) => {
1507 let op_log = match lex_vcs::OpLog::open(store.root()) {
1508 Ok(l) => l,
1509 Err(e) => return error_response(500, format!("opening op log: {e}")),
1510 };
1511 match op_log.walk_back(cutoff, None) {
1512 Ok(records) => records.into_iter().map(|r| r.op_id).collect(),
1513 Err(_) => {
1514 std::collections::BTreeSet::new()
1518 }
1519 }
1520 }
1521 };
1522
1523 let all = match log.list_all() {
1524 Ok(v) => v,
1525 Err(e) => return error_response(500, format!("listing attestations: {e}")),
1526 };
1527 let mut filtered: Vec<lex_vcs::Attestation> = all
1528 .into_iter()
1529 .filter(|a| match &a.op_id {
1530 Some(op_id) => !exclude.contains(op_id),
1531 None => true,
1535 })
1536 .collect();
1537 filtered.sort_by(|a, b| {
1541 a.timestamp.cmp(&b.timestamp)
1542 .then_with(|| a.attestation_id.cmp(&b.attestation_id))
1543 });
1544 if let Some(n) = limit {
1545 filtered.truncate(n);
1546 }
1547
1548 json_response(200, &serde_json::to_value(&filtered).unwrap_or_default())
1549}
1550
1551#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1555struct PkgRecord {
1556 name: String,
1557 version: String,
1558 head_op: Option<String>,
1559 published_at: u64,
1560 function_names: Vec<String>,
1562 ops: Vec<serde_json::Value>,
1564}
1565
1566#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
1573#[serde(rename_all = "lowercase")]
1574pub enum Visibility {
1575 #[default]
1576 Private,
1577 Public,
1578}
1579
1580#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
1585struct PkgIndex {
1586 latest: Option<String>,
1588 versions: Vec<PkgVersionSummary>,
1590 #[serde(default)]
1594 visibility: Visibility,
1595}
1596
1597#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1598struct PkgVersionSummary {
1599 version: String,
1600 head_op: Option<String>,
1601 published_at: u64,
1602}
1603
1604fn pkg_name_dir(root: &std::path::Path, name: &str) -> PathBuf {
1605 root.join("packages").join(name)
1606}
1607
1608fn pkg_index_path(root: &std::path::Path, name: &str) -> PathBuf {
1609 pkg_name_dir(root, name).join("index.json")
1610}
1611
1612fn pkg_version_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1613 pkg_name_dir(root, name).join(format!("{version}.json"))
1614}
1615
1616fn pkg_archive_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1617 pkg_name_dir(root, name).join(format!("{version}.tar.gz"))
1618}
1619
1620fn load_pkg_index(root: &std::path::Path, name: &str) -> Option<PkgIndex> {
1621 let bytes = std::fs::read(pkg_index_path(root, name)).ok()?;
1622 serde_json::from_slice(&bytes).ok()
1623}
1624
1625fn load_pkg_record(root: &std::path::Path, name: &str, version: &str) -> Option<PkgRecord> {
1626 let bytes = std::fs::read(pkg_version_path(root, name, version)).ok()?;
1627 serde_json::from_slice(&bytes).ok()
1628}
1629
1630fn load_latest_pkg_record(root: &std::path::Path, name: &str) -> Option<PkgRecord> {
1631 let index = load_pkg_index(root, name)?;
1632 let latest = index.latest.clone()?;
1633 load_pkg_record(root, name, &latest)
1634}
1635
1636fn pkg_is_public(root: &std::path::Path, name: &str) -> bool {
1640 load_pkg_index(root, name).map(|i| i.visibility) == Some(Visibility::Public)
1641}
1642
1643fn valid_pkg_segment(s: &str) -> bool {
1648 !s.is_empty()
1649 && s.len() <= 128
1650 && s != "."
1651 && s != ".."
1652 && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
1653}
1654
1655#[derive(Deserialize)]
1656struct VisibilityReq {
1657 visibility: Visibility,
1658}
1659
1660fn pkg_set_visibility_handler(
1667 state: &State,
1668 name: &str,
1669 body: &str,
1670) -> Response<std::io::Cursor<Vec<u8>>> {
1671 if !valid_pkg_segment(name) {
1672 return error_response(400, format!("invalid package name {name:?}"));
1673 }
1674 let req: VisibilityReq = match serde_json::from_str(body) {
1675 Ok(r) => r,
1676 Err(e) => return error_response(400, format!("bad request: {e}")),
1677 };
1678 let mut index = match load_pkg_index(&state.root, name) {
1679 Some(i) => i,
1680 None => return error_response(404, format!("package {name:?} not found")),
1681 };
1682 index.visibility = req.visibility;
1683 let bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1684 match std::fs::write(pkg_index_path(&state.root, name), bytes) {
1685 Ok(()) => json_response(
1686 200,
1687 &serde_json::json!({ "name": name, "visibility": index.visibility }),
1688 ),
1689 Err(e) => error_response(500, format!("write index: {e}")),
1690 }
1691}
1692
1693fn public_pkg_names(root: &std::path::Path) -> Vec<String> {
1696 list_pkg_names(root)
1697 .into_iter()
1698 .filter(|name| pkg_is_public(root, name))
1699 .collect()
1700}
1701
1702fn public_pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
1706 let packages: Vec<serde_json::Value> = public_pkg_names(&state.root)
1707 .iter()
1708 .filter_map(|name| {
1709 let r = load_latest_pkg_record(&state.root, name)?;
1710 Some(serde_json::json!({
1711 "name": r.name,
1712 "version": r.version,
1713 "head_op": r.head_op,
1714 "published_at": r.published_at,
1715 }))
1716 })
1717 .collect();
1718 json_response(200, &serde_json::json!({ "packages": packages }))
1719}
1720
1721#[derive(Debug, PartialEq, Eq)]
1726enum PublicTarget {
1727 List,
1728 Latest(String),
1729 Versions(String),
1730 Head(String),
1731 Version(String, String),
1732 Archive(String, String),
1733}
1734
1735impl PublicTarget {
1736 fn pkg_name(&self) -> Option<&str> {
1738 match self {
1739 PublicTarget::List => None,
1740 PublicTarget::Latest(n)
1741 | PublicTarget::Versions(n)
1742 | PublicTarget::Head(n)
1743 | PublicTarget::Version(n, _)
1744 | PublicTarget::Archive(n, _) => Some(n),
1745 }
1746 }
1747}
1748
1749fn resolve_public(method: &Method, path: &str) -> Result<PublicTarget, u16> {
1754 if !matches!(method, Method::Get) {
1755 return Err(405);
1756 }
1757 let rest = path.trim_matches('/');
1758 if rest.is_empty() {
1759 return Ok(PublicTarget::List);
1760 }
1761 let segs: Vec<&str> = rest.split('/').collect();
1762 if !segs.iter().all(|s| valid_pkg_segment(s)) {
1763 return Err(404);
1764 }
1765 match segs.as_slice() {
1766 [n] => Ok(PublicTarget::Latest(n.to_string())),
1767 [n, "versions"] => Ok(PublicTarget::Versions(n.to_string())),
1768 [n, "head"] => Ok(PublicTarget::Head(n.to_string())),
1769 [n, v, "archive"] => Ok(PublicTarget::Archive(n.to_string(), v.to_string())),
1770 [n, v] => Ok(PublicTarget::Version(n.to_string(), v.to_string())),
1771 _ => Err(404),
1772 }
1773}
1774
1775pub fn route_public(
1786 state: &State,
1787 method: &Method,
1788 path: &str,
1789 _query: &str,
1790) -> Response<std::io::Cursor<Vec<u8>>> {
1791 let target = match resolve_public(method, path) {
1792 Ok(t) => t,
1793 Err(405) => return error_response(405, "public read is GET-only"),
1794 Err(_) => return error_response(404, "not found"),
1795 };
1796 if let PublicTarget::List = target {
1798 return public_pkg_list_handler(state);
1799 }
1800 if let Some(name) = target.pkg_name() {
1802 if !pkg_is_public(&state.root, name) {
1803 return error_response(404, format!("package {name:?} not found"));
1804 }
1805 }
1806 match target {
1807 PublicTarget::List => unreachable!("handled above"),
1808 PublicTarget::Latest(n) => pkg_get_handler(state, &n),
1809 PublicTarget::Versions(n) => pkg_versions_handler(state, &n),
1810 PublicTarget::Head(n) => pkg_head_handler(state, &n),
1811 PublicTarget::Version(n, v) => pkg_get_version_handler(state, &n, &v),
1812 PublicTarget::Archive(n, v) => pkg_archive_handler(state, &n, &v),
1813 }
1814}
1815
1816fn save_pkg_record(
1817 root: &std::path::Path,
1818 record: &PkgRecord,
1819 archive: &[u8],
1820) -> std::io::Result<()> {
1821 let dir = pkg_name_dir(root, &record.name);
1822 std::fs::create_dir_all(&dir)?;
1823
1824 let rec_bytes = serde_json::to_vec_pretty(record).unwrap_or_default();
1826 std::fs::write(pkg_version_path(root, &record.name, &record.version), rec_bytes)?;
1827
1828 std::fs::write(pkg_archive_path(root, &record.name, &record.version), archive)?;
1830
1831 let mut index = load_pkg_index(root, &record.name).unwrap_or_default();
1833 index.latest = Some(record.version.clone());
1834 if !index.versions.iter().any(|v| v.version == record.version) {
1835 index.versions.push(PkgVersionSummary {
1836 version: record.version.clone(),
1837 head_op: record.head_op.clone(),
1838 published_at: record.published_at,
1839 });
1840 }
1841 let idx_bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1842 std::fs::write(pkg_index_path(root, &record.name), idx_bytes)
1843}
1844
1845fn list_pkg_names(root: &std::path::Path) -> Vec<String> {
1846 let dir = root.join("packages");
1847 let Ok(entries) = std::fs::read_dir(&dir) else {
1848 return Vec::new();
1849 };
1850 let mut names: Vec<String> = entries
1851 .filter_map(|e| e.ok())
1852 .filter(|e| e.path().is_dir())
1853 .filter_map(|e| e.file_name().into_string().ok())
1854 .collect();
1855 names.sort();
1856 names
1857}
1858
1859fn collect_lex_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
1860 let Ok(entries) = std::fs::read_dir(dir) else { return };
1861 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
1862 entries.sort_by_key(|e| e.path());
1863 for entry in entries {
1864 let path = entry.path();
1865 if path.is_dir() {
1866 collect_lex_files(&path, out);
1867 } else if path.extension().and_then(|x| x.to_str()) == Some("lex") {
1868 out.push(path);
1869 }
1870 }
1871}
1872
1873fn pkg_publish_handler(state: &State, body: &[u8]) -> Response<std::io::Cursor<Vec<u8>>> {
1876 let tmp = match tempfile::TempDir::new() {
1877 Ok(t) => t,
1878 Err(e) => return error_response(500, format!("create temp dir: {e}")),
1879 };
1880 {
1881 let gz = flate2::read::GzDecoder::new(std::io::Cursor::new(body));
1882 let mut ar = tar::Archive::new(gz);
1883 if let Err(e) = ar.unpack(tmp.path()) {
1884 return error_response(400, format!("unpack archive: {e}"));
1885 }
1886 }
1887
1888 let toml_path = tmp.path().join("lex.toml");
1889 if !toml_path.exists() {
1890 return error_response(400, "archive must contain lex.toml at root");
1891 }
1892 let manifest = match Manifest::load(&toml_path) {
1893 Ok(m) => m,
1894 Err(e) => return error_response(400, format!("lex.toml: {e}")),
1895 };
1896 let (pkg_name, pkg_version) = match &manifest.package {
1897 Some(m) => (m.name.clone(), m.version.clone()),
1898 None => return error_response(400, "lex.toml must have a [package] section"),
1899 };
1900
1901 if load_pkg_record(&state.root, &pkg_name, &pkg_version).is_some() {
1905 return error_response(
1906 409,
1907 format!(
1908 "package {pkg_name}@{pkg_version} already published; \
1909 bump the version in lex.toml to publish a new release"
1910 ),
1911 );
1912 }
1913
1914 let src_dir = tmp.path().join("src");
1915 if !src_dir.exists() {
1916 return error_response(400, "archive must contain a src/ directory");
1917 }
1918 let mut lex_files: Vec<PathBuf> = Vec::new();
1919 collect_lex_files(&src_dir, &mut lex_files);
1920 if lex_files.is_empty() {
1921 return error_response(400, "no .lex files found in src/");
1922 }
1923
1924 let store = state.store.lock().unwrap();
1925 let branch = store.current_branch();
1926
1927 let old_head = match store.branch_head(&branch) {
1947 Ok(h) => h,
1948 Err(e) => return error_response(500, format!("branch_head: {e}")),
1949 };
1950 let old_pairs: Vec<(String, String)> =
1957 old_head.iter().map(|(sig, stage)| (sig.clone(), stage.clone())).collect();
1958 let mut old_fns_by_name: BTreeMap<String, Vec<lex_ast::FnDecl>> = BTreeMap::new();
1959 for fd in store.get_asts_for_sigs_bulk(&old_pairs)
1960 .into_iter()
1961 .filter_map(|r| r.ok())
1962 .filter_map(|s| match s { lex_ast::Stage::FnDecl(fd) => Some(fd), _ => None })
1963 {
1964 old_fns_by_name.entry(fd.name.clone()).or_default().push(fd);
1965 }
1966 fn structural_key(fd: &lex_ast::FnDecl) -> Option<String> {
1974 let mut anon = fd.clone();
1975 anon.name = String::new();
1976 lex_ast::sig_id(&lex_ast::Stage::FnDecl(anon))
1977 }
1978
1979 fn take_matching(
1989 map: &mut BTreeMap<String, Vec<lex_ast::FnDecl>>,
1990 name: &str,
1991 new_fd: &lex_ast::FnDecl,
1992 ) -> Option<lex_ast::FnDecl> {
1993 let candidates = map.get_mut(name)?;
1994 let idx = match candidates.len() {
1995 0 => return None,
1996 1 => 0,
1997 _ => {
1998 let want = structural_key(new_fd);
1999 candidates.iter().position(|c| structural_key(c) == want)?
2000 }
2001 };
2002 let matched = candidates.remove(idx);
2003 if candidates.is_empty() {
2004 map.remove(name);
2005 }
2006 Some(matched)
2007 }
2008
2009 let loaded = match load_package(&lex_files, tmp.path(), &pkg_name) {
2029 Ok(p) => p,
2030 Err(e) => return error_response(400, format!("load package: {e}")),
2031 };
2032 let mut stages = canonicalize_program(&loaded.program);
2033 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
2039 return error_with_detail(
2040 422,
2041 format!("type errors in package {pkg_name}"),
2042 serde_json::to_value(&errs).unwrap(),
2043 );
2044 }
2045 let new_fns: BTreeMap<String, lex_ast::FnDecl> = stages
2046 .iter()
2047 .filter_map(|s| match s {
2048 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
2049 _ => None,
2050 })
2051 .collect();
2052 let all_function_names: Vec<String> = new_fns.keys().cloned().collect();
2053
2054 let mut old_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2059 for (name, new_fd) in &new_fns {
2060 if let Some(fd) = take_matching(&mut old_fns_by_name, name, new_fd) {
2061 old_fns.insert(name.clone(), fd);
2062 }
2063 }
2064 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
2065
2066 let mut new_imports = lex_vcs::ImportMap::new();
2071 for (file, modules) in &loaded.imports_by_file {
2072 let entry = new_imports.entry(file.clone()).or_default();
2073 for m in modules {
2074 entry.insert(m.clone());
2075 }
2076 }
2077
2078 let outcome = match store.publish_program(&branch, &stages, &report, &new_imports, false) {
2079 Ok(outcome) => outcome,
2080 Err(lex_store::StoreError::TypeError(errs)) => {
2081 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
2082 }
2083 Err(e) => return write_error_response("publish_program", e),
2084 };
2085 let all_ops: Vec<serde_json::Value> = match serde_json::to_value(&outcome.ops) {
2086 Ok(serde_json::Value::Array(arr)) => arr,
2087 _ => Vec::new(),
2088 };
2089 let final_head_op = outcome.head_op;
2090
2091 let now = SystemTime::now()
2116 .duration_since(UNIX_EPOCH)
2117 .map(|d| d.as_secs())
2118 .unwrap_or(0);
2119 let record = PkgRecord {
2120 name: pkg_name.clone(),
2121 version: pkg_version,
2122 head_op: final_head_op.clone(),
2123 published_at: now,
2124 function_names: all_function_names,
2125 ops: all_ops.clone(),
2126 };
2127 if let Err(e) = save_pkg_record(&state.root, &record, body) {
2128 return error_response(500, format!("save package index: {e}"));
2129 }
2130
2131 json_response(200, &serde_json::json!({
2132 "package": pkg_name,
2133 "ops": all_ops,
2134 "head_op": final_head_op,
2135 }))
2136}
2137
2138fn pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
2140 let names = list_pkg_names(&state.root);
2141 let packages: Vec<serde_json::Value> = names.iter()
2142 .filter_map(|name| {
2143 let idx = load_pkg_index(&state.root, name)?;
2144 let latest = idx.latest.as_deref()?;
2145 let r = load_pkg_record(&state.root, name, latest)?;
2146 Some(serde_json::json!({
2147 "name": r.name,
2148 "version": r.version,
2149 "head_op": r.head_op,
2150 "published_at": r.published_at,
2151 }))
2152 })
2153 .collect();
2154 json_response(200, &serde_json::json!({ "packages": packages }))
2155}
2156
2157fn pkg_get_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2159 match load_latest_pkg_record(&state.root, name) {
2160 Some(r) => json_response(200, &serde_json::json!({
2161 "name": r.name,
2162 "version": r.version,
2163 "head_op": r.head_op,
2164 "published_at": r.published_at,
2165 "function_names": r.function_names,
2166 "ops": r.ops,
2167 })),
2168 None => error_response(404, format!("package {name:?} not found")),
2169 }
2170}
2171
2172fn pkg_versions_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2174 match load_pkg_index(&state.root, name) {
2175 Some(idx) => json_response(200, &serde_json::json!({
2176 "name": name,
2177 "latest": idx.latest,
2178 "versions": idx.versions,
2179 })),
2180 None => error_response(404, format!("package {name:?} not found")),
2181 }
2182}
2183
2184fn pkg_get_version_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2186 match load_pkg_record(&state.root, name, version) {
2187 Some(r) => json_response(200, &serde_json::json!({
2188 "name": r.name,
2189 "version": r.version,
2190 "head_op": r.head_op,
2191 "published_at": r.published_at,
2192 "function_names": r.function_names,
2193 "ops": r.ops,
2194 })),
2195 None => error_response(404, format!("package {name:?}@{version:?} not found")),
2196 }
2197}
2198
2199fn pkg_archive_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2201 let path = pkg_archive_path(&state.root, name, version);
2202 match std::fs::read(&path) {
2203 Ok(bytes) => Response::from_data(bytes)
2204 .with_status_code(200)
2205 .with_header(
2206 tiny_http::Header::from_bytes(
2207 &b"Content-Type"[..],
2208 &b"application/gzip"[..],
2209 )
2210 .unwrap(),
2211 ),
2212 Err(_) => error_response(404, format!("archive for {name:?}@{version:?} not found")),
2213 }
2214}
2215
2216fn pkg_head_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2218 match load_latest_pkg_record(&state.root, name) {
2219 Some(r) => json_response(200, &serde_json::json!({
2220 "name": r.name,
2221 "version": r.version,
2222 "head_op": r.head_op,
2223 })),
2224 None => error_response(404, format!("package {name:?} not found")),
2225 }
2226}
2227
2228fn pkg_delete_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2230 let record = match load_latest_pkg_record(&state.root, name) {
2231 Some(r) => r,
2232 None => return error_response(404, format!("package {name:?} not found")),
2233 };
2234
2235 let store = state.store.lock().unwrap();
2236 let branch = store.current_branch();
2237
2238 let head = match store.branch_head(&branch) {
2239 Ok(h) => h,
2240 Err(e) => return error_response(500, format!("branch_head: {e}")),
2241 };
2242
2243 let head_pairs: Vec<(String, String)> = head
2250 .iter()
2251 .map(|(sig, stage)| (sig.clone(), stage.clone()))
2252 .collect();
2253 let old_fns: BTreeMap<String, lex_ast::FnDecl> = store
2254 .get_asts_for_sigs_bulk(&head_pairs)
2255 .into_iter()
2256 .filter_map(|r| r.ok())
2257 .filter_map(|s| match s {
2258 lex_ast::Stage::FnDecl(fd)
2259 if record.function_names.contains(&fd.name) => Some((fd.name.clone(), fd)),
2260 _ => None,
2261 })
2262 .collect();
2263
2264 let new_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2265 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
2266 let empty_imports = lex_vcs::ImportMap::new();
2267
2268 match store.publish_program(&branch, &[], &report, &empty_imports, false) {
2269 Ok(outcome) => {
2270 let ver = record.version.clone();
2272 let _ = std::fs::remove_file(pkg_version_path(&state.root, name, &ver));
2273 let _ = std::fs::remove_file(pkg_archive_path(&state.root, name, &ver));
2274 if let Some(mut idx) = load_pkg_index(&state.root, name) {
2276 idx.versions.retain(|v| v.version != ver);
2277 idx.latest = idx.versions.last().map(|v| v.version.clone());
2278 if idx.versions.is_empty() {
2279 let _ = std::fs::remove_dir_all(pkg_name_dir(&state.root, name));
2280 } else {
2281 let bytes = serde_json::to_vec_pretty(&idx).unwrap_or_default();
2282 let _ = std::fs::write(pkg_index_path(&state.root, name), bytes);
2283 }
2284 }
2285 json_response(200, &serde_json::json!({
2286 "deleted": name,
2287 "version": ver,
2288 "ops": outcome.ops,
2289 "head_op": outcome.head_op,
2290 }))
2291 }
2292 Err(lex_store::StoreError::TypeError(errs)) => {
2293 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
2294 }
2295 Err(e) => write_error_response("retract package", e),
2296 }
2297}
2298
2299#[cfg(test)]
2300mod policy_ceiling_tests {
2301 use super::*;
2302 use lex_runtime::Policy;
2303 use std::path::PathBuf;
2304
2305 fn permissive_request() -> Policy {
2309 Policy {
2310 allow_effects: ["io", "fs_read", "fs_write", "net", "proc"]
2311 .iter()
2312 .map(|s| s.to_string())
2313 .collect(),
2314 allow_fs_read: vec![PathBuf::from("/")],
2315 allow_fs_write: vec![PathBuf::from("/")],
2316 allow_net_host: Vec::new(),
2317 allow_proc: Vec::new(),
2318 allow_approval: Vec::new(),
2319 budget: None,
2320 }
2321 }
2322
2323 #[test]
2324 fn ceiling_drops_effects_the_caller_was_not_granted() {
2325 let ceiling = Policy {
2326 allow_effects: ["io", "time"].iter().map(|s| s.to_string()).collect(),
2327 ..Policy::default()
2328 };
2329 let got = clamp_policy(permissive_request(), &ceiling);
2330 assert!(got.allow_effects.contains("io"));
2331 assert!(!got.allow_effects.contains("proc"), "proc must not survive a ceiling without it");
2332 assert!(!got.allow_effects.contains("fs_write"));
2333 assert!(!got.allow_effects.contains("net"));
2334 assert!(!got.allow_effects.contains("time"));
2336 }
2337
2338 #[test]
2339 fn ceiling_scopes_override_caller_scopes() {
2340 let ceiling = Policy {
2341 allow_effects: ["fs_read"].iter().map(|s| s.to_string()).collect(),
2342 allow_fs_read: vec![PathBuf::from("/srv/tenant")],
2343 ..Policy::default()
2344 };
2345 let got = clamp_policy(permissive_request(), &ceiling);
2346 assert_eq!(got.allow_fs_read, vec![PathBuf::from("/srv/tenant")]);
2349 assert!(got.allow_fs_write.is_empty());
2350 assert!(got.allow_proc.is_empty());
2351 assert!(got.allow_net_host.is_empty());
2352 }
2353
2354 #[test]
2355 fn ceiling_caps_budget_and_prefers_the_smaller() {
2356 let mut req = permissive_request();
2358 req.budget = None;
2359 let ceiling = Policy { budget: Some(1_000), ..Policy::default() };
2360 assert_eq!(clamp_policy(req, &ceiling).budget, Some(1_000));
2361
2362 let mut req2 = permissive_request();
2364 req2.budget = Some(50);
2365 let ceiling2 = Policy { budget: Some(1_000), ..Policy::default() };
2366 assert_eq!(clamp_policy(req2, &ceiling2).budget, Some(50));
2367 }
2368
2369 #[test]
2370 fn empty_ceiling_is_pure_only() {
2371 let got = clamp_policy(permissive_request(), &Policy::default());
2372 assert!(got.allow_effects.is_empty(), "an empty ceiling grants nothing");
2373 assert!(got.allow_proc.is_empty());
2374 assert!(got.allow_fs_write.is_empty());
2375 }
2376}
2377
2378#[cfg(test)]
2379mod public_read_tests {
2380 use super::*;
2381
2382 fn seed_pkg(root: &std::path::Path, name: &str, version: &str) {
2385 let record = PkgRecord {
2386 name: name.to_string(),
2387 version: version.to_string(),
2388 head_op: Some(format!("op-{name}")),
2389 published_at: 1,
2390 function_names: vec![format!("{name}.f")],
2391 ops: vec![],
2392 };
2393 save_pkg_record(root, &record, format!("ARCHIVE:{name}@{version}").as_bytes())
2394 .expect("seed package");
2395 }
2396
2397 #[test]
2398 fn new_package_defaults_to_private() {
2399 let tmp = tempfile::TempDir::new().unwrap();
2400 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2401 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2402 assert!(!pkg_is_public(tmp.path(), "does-not-exist"));
2404 }
2405
2406 #[test]
2407 fn set_visibility_round_trips_and_index_persists() {
2408 let tmp = tempfile::TempDir::new().unwrap();
2409 let state = State::open(tmp.path().to_path_buf()).unwrap();
2410 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2411
2412 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"public"}"#);
2413 assert!(pkg_is_public(tmp.path(), "lex-schema"));
2414 let idx = load_pkg_index(tmp.path(), "lex-schema").unwrap();
2416 assert_eq!(idx.latest.as_deref(), Some("0.9.2"));
2417 assert_eq!(idx.versions.len(), 1);
2418
2419 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"private"}"#);
2420 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2421 }
2422
2423 #[test]
2424 fn set_visibility_on_unknown_package_is_a_noop() {
2425 let tmp = tempfile::TempDir::new().unwrap();
2426 let state = State::open(tmp.path().to_path_buf()).unwrap();
2427 let _ = pkg_set_visibility_handler(&state, "ghost", r#"{"visibility":"public"}"#);
2429 assert!(load_pkg_index(tmp.path(), "ghost").is_none());
2430 }
2431
2432 #[test]
2433 fn public_listing_omits_private_packages() {
2434 let tmp = tempfile::TempDir::new().unwrap();
2435 let state = State::open(tmp.path().to_path_buf()).unwrap();
2436 seed_pkg(tmp.path(), "pub-pkg", "1.0.0");
2437 seed_pkg(tmp.path(), "priv-pkg", "1.0.0");
2438 let _ = pkg_set_visibility_handler(&state, "pub-pkg", r#"{"visibility":"public"}"#);
2439
2440 let names = public_pkg_names(tmp.path());
2441 assert_eq!(names, vec!["pub-pkg".to_string()]);
2442 }
2443
2444 #[test]
2445 fn resolve_public_maps_routes() {
2446 let get = Method::Get;
2447 assert_eq!(resolve_public(&get, "").unwrap(), PublicTarget::List);
2448 assert_eq!(resolve_public(&get, "/").unwrap(), PublicTarget::List);
2449 assert_eq!(
2450 resolve_public(&get, "/lex-schema").unwrap(),
2451 PublicTarget::Latest("lex-schema".into())
2452 );
2453 assert_eq!(
2454 resolve_public(&get, "/lex-schema/versions").unwrap(),
2455 PublicTarget::Versions("lex-schema".into())
2456 );
2457 assert_eq!(
2458 resolve_public(&get, "/lex-schema/head").unwrap(),
2459 PublicTarget::Head("lex-schema".into())
2460 );
2461 assert_eq!(
2462 resolve_public(&get, "/lex-schema/0.9.2").unwrap(),
2463 PublicTarget::Version("lex-schema".into(), "0.9.2".into())
2464 );
2465 assert_eq!(
2466 resolve_public(&get, "/lex-schema/0.9.2/archive").unwrap(),
2467 PublicTarget::Archive("lex-schema".into(), "0.9.2".into())
2468 );
2469 }
2470
2471 #[test]
2472 fn resolve_public_rejects_bad_method_and_traversal() {
2473 assert_eq!(resolve_public(&Method::Put, "/lex-schema"), Err(405));
2475 assert_eq!(resolve_public(&Method::Post, "").err(), Some(405));
2476 assert_eq!(resolve_public(&Method::Get, "/.."), Err(404));
2478 assert_eq!(resolve_public(&Method::Get, "/lex-schema/../etc"), Err(404));
2479 assert_eq!(resolve_public(&Method::Get, "/a/b/c/d"), Err(404));
2480 assert!(resolve_public(&Method::Get, "/lex schema").is_err());
2482 }
2483}