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
23fn stage_fns(stages: &[lex_ast::Stage]) -> BTreeMap<String, lex_ast::FnDecl> {
25 stages.iter().filter_map(|s| match s {
26 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
27 _ => None,
28 }).collect()
29}
30
31fn stage_types(stages: &[lex_ast::Stage]) -> BTreeMap<String, lex_ast::TypeDecl> {
34 stages.iter().filter_map(|s| match s {
35 lex_ast::Stage::TypeDecl(td) => Some((td.name.clone(), td.clone())),
36 _ => None,
37 }).collect()
38}
39
40pub struct State {
41 pub store: Mutex<Store>,
42 pub root: PathBuf,
47 pub sessions: Mutex<HashMap<MergeSessionId, ApiMergeSession>>,
54 pub policy_ceiling: Option<Policy>,
75 pub(crate) ops_since: Mutex<crate::ops_since_http::OpsSinceCache>,
79 pub blob_limits: Option<BlobLimits>,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct BlobLimits {
92 pub max_blob_bytes: u64,
94 pub store_quota_bytes: u64,
96 pub max_manifest_entries: usize,
98}
99
100pub const CAPS: &[&str] = &[CAP_FILES_V1];
104
105pub const CAP_FILES_V1: &str = "files-v1";
107
108pub(crate) fn has_cap(header: Option<&str>, cap: &str) -> bool {
110 header.is_some_and(|h| h.split(',').any(|c| c.trim().eq_ignore_ascii_case(cap)))
111}
112
113pub struct ApiMergeSession {
120 pub inner: MergeSession,
121 pub src_branch: String,
122 pub dst_branch: String,
123}
124
125impl State {
126 pub fn open(root: PathBuf) -> anyhow::Result<Self> {
127 Self::open_with_ceiling(root, None)
128 }
129
130 pub fn open_with_ceiling(
135 root: PathBuf,
136 policy_ceiling: Option<Policy>,
137 ) -> anyhow::Result<Self> {
138 Ok(Self {
139 store: Mutex::new(Store::open(&root)?),
140 root,
141 sessions: Mutex::new(HashMap::new()),
142 policy_ceiling,
143 ops_since: Mutex::new(Default::default()),
144 blob_limits: None,
145 })
146 }
147
148 pub fn with_blob_limits(mut self, limits: Option<BlobLimits>) -> Self {
150 self.blob_limits = limits;
151 self
152 }
153
154 pub fn new_with_tenant(tenant_id: &str, store_root: PathBuf) -> anyhow::Result<Self> {
164 validate_tenant_id(tenant_id)?;
165 Self::open(store_root.join(tenant_id))
166 }
167
168 pub fn new_with_tenant_and_ceiling(
173 tenant_id: &str,
174 store_root: PathBuf,
175 policy_ceiling: Option<Policy>,
176 ) -> anyhow::Result<Self> {
177 validate_tenant_id(tenant_id)?;
178 Self::open_with_ceiling(store_root.join(tenant_id), policy_ceiling)
179 }
180}
181
182fn clamp_policy(requested: Policy, ceiling: &Policy) -> Policy {
198 let allow_effects: BTreeSet<String> = requested
199 .allow_effects
200 .intersection(&ceiling.allow_effects)
201 .cloned()
202 .collect();
203 let budget = match (requested.budget, ceiling.budget) {
204 (Some(r), Some(c)) => Some(r.min(c)),
205 (None, Some(c)) => Some(c),
206 (Some(r), None) => Some(r),
207 (None, None) => None,
208 };
209 Policy {
210 allow_effects,
211 allow_fs_read: ceiling.allow_fs_read.clone(),
212 allow_fs_write: ceiling.allow_fs_write.clone(),
213 allow_net_host: ceiling.allow_net_host.clone(),
214 allow_proc: ceiling.allow_proc.clone(),
215 allow_approval: ceiling.allow_approval.clone(),
216 budget,
217 }
218}
219
220fn validate_tenant_id(tenant_id: &str) -> anyhow::Result<()> {
221 if tenant_id.is_empty() {
222 anyhow::bail!("tenant_id must not be empty");
223 }
224 if tenant_id.len() > 64 {
225 anyhow::bail!("tenant_id must be at most 64 bytes");
226 }
227 if !tenant_id
228 .bytes()
229 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
230 {
231 anyhow::bail!(
232 "tenant_id {tenant_id:?} contains characters outside [A-Za-z0-9_-]"
233 );
234 }
235 Ok(())
236}
237
238#[derive(Debug, Serialize, Deserialize)]
239struct ErrorEnvelope {
240 error: String,
241 #[serde(skip_serializing_if = "Option::is_none")]
242 detail: Option<serde_json::Value>,
243}
244
245pub(crate) fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
246 let bytes = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
247 Response::from_data(bytes)
248 .with_status_code(status)
249 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
250}
251
252pub(crate) fn error_response(status: u16, msg: impl Into<String>) -> Response<std::io::Cursor<Vec<u8>>> {
253 json_response(status, &serde_json::to_value(ErrorEnvelope {
254 error: msg.into(), detail: None,
255 }).unwrap())
256}
257
258pub(crate) fn error_with_detail(status: u16, msg: impl Into<String>, detail: serde_json::Value)
259 -> Response<std::io::Cursor<Vec<u8>>>
260{
261 json_response(status, &serde_json::to_value(ErrorEnvelope {
262 error: msg.into(), detail: Some(detail),
263 }).unwrap())
264}
265
266pub const UNSATISFIABLE_PAIR_HINT: &str =
269 "republish from source to retire the stranded entry (#995)";
270
271pub(crate) fn unsatisfiable_pair_response(err: &lex_store::StoreError)
284 -> Option<Response<std::io::Cursor<Vec<u8>>>>
285{
286 let lex_store::StoreError::UnsatisfiablePair { sig_id, stage_id, filed_under } = err else {
287 return None;
288 };
289 Some(error_with_detail(422, "UnsatisfiablePair", serde_json::json!({
290 "sig_id": sig_id,
291 "stage_id": stage_id,
292 "filed_under": filed_under,
293 "message": err.to_string(),
294 "hint": UNSATISFIABLE_PAIR_HINT,
295 })))
296}
297
298fn write_error_response(prefix: &str, err: lex_store::StoreError)
304 -> Response<std::io::Cursor<Vec<u8>>>
305{
306 if let Some(resp) = unsatisfiable_pair_response(&err) {
307 return resp;
308 }
309 if let lex_store::StoreError::Contention { branch, attempts } = &err {
310 let body = serde_json::to_vec(&ErrorEnvelope {
311 error: format!("{prefix}: branch '{branch}' is contended (attempts={attempts})"),
312 detail: Some(serde_json::json!({
313 "kind": "contention",
314 "branch": branch,
315 "attempts": attempts,
316 })),
317 }).unwrap_or_else(|_| b"{}".to_vec());
318 return Response::from_data(body)
319 .with_status_code(503)
320 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
321 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"1"[..]).unwrap());
322 }
323 if let lex_store::StoreError::BudgetExceeded { session_id, cap, spent_after } = &err {
331 let body = serde_json::to_vec(&ErrorEnvelope {
332 error: format!(
333 "{prefix}: session `{session_id}` budget exceeded \
334 (spent_after={spent_after}, cap={cap})"
335 ),
336 detail: Some(serde_json::json!({
337 "kind": "budget_exceeded",
338 "session_id": session_id,
339 "cap": cap,
340 "spent_after": spent_after,
341 })),
342 }).unwrap_or_else(|_| b"{}".to_vec());
343 return Response::from_data(body)
344 .with_status_code(503)
345 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
346 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"0"[..]).unwrap());
347 }
348 error_response(500, format!("{prefix}: {err}"))
349}
350
351pub fn handle(state: Arc<State>, mut req: Request) -> std::io::Result<()> {
352 let method = req.method().clone();
353 let url = req.url().to_string();
354 let path = url.split('?').next().unwrap_or("").to_string();
355 let query = url.split_once('?').map(|(_, q)| q.to_string()).unwrap_or_default();
356
357 let x_lex_user = req.headers().iter()
362 .find(|h| h.field.equiv("x-lex-user"))
363 .map(|h| h.value.as_str().to_string());
364 let x_lex_caps = req.headers().iter()
367 .find(|h| h.field.equiv("x-lex-caps"))
368 .map(|h| h.value.as_str().to_string());
369
370 if matches!(method, Method::Post) && path == "/v1/pkg/publish" {
372 let mut body_bytes: Vec<u8> = Vec::new();
373 let _ = req.as_reader().read_to_end(&mut body_bytes);
374 let resp = pkg_publish_handler(&state, &body_bytes);
375 return req.respond(resp);
376 }
377
378 let mut body = String::new();
379 let _ = req.as_reader().read_to_string(&mut body);
380
381 let resp = route(&state, &method, &path, &query, &body, x_lex_user.as_deref(), x_lex_caps.as_deref());
382 req.respond(resp)
383}
384
385pub fn handle_with_auth<F>(state: Arc<State>, req: Request, auth: F) -> std::io::Result<()>
389where
390 F: FnOnce(&str, &[Header]) -> bool,
391{
392 let path = req.url().split('?').next().unwrap_or("").to_string();
393 if !auth(&path, req.headers()) {
394 return req.respond(
395 Response::from_data(br#"{"error":"unauthorized"}"#.to_vec())
396 .with_status_code(401)
397 .with_header(
398 Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
399 ),
400 );
401 }
402 handle(state, req)
403}
404
405fn route(
406 state: &State,
407 method: &Method,
408 path: &str,
409 query: &str,
410 body: &str,
411 x_lex_user: Option<&str>,
412 x_lex_caps: Option<&str>,
413) -> Response<std::io::Cursor<Vec<u8>>> {
414 match (method, path) {
415 (Method::Get, "/") => crate::web::activity_handler(state),
417 (Method::Get, "/web/branches") => crate::web::branches_handler(state),
418 (Method::Get, "/web/trust") => crate::web::trust_handler(state),
419 (Method::Get, "/web/attention") => crate::web::attention_handler(state),
420 (Method::Get, p) if p.starts_with("/web/branch/") => {
421 let name = &p["/web/branch/".len()..];
422 crate::web::branch_handler(state, name)
423 }
424 (Method::Get, p) if p.starts_with("/web/stage/") => {
425 let id = &p["/web/stage/".len()..];
426 crate::web::stage_html_handler(state, id)
427 }
428 (Method::Post, p) if p.starts_with("/web/stage/") && (
433 p.ends_with("/pin") || p.ends_with("/defer")
434 || p.ends_with("/block") || p.ends_with("/unblock")
435 ) => {
436 let prefix_len = "/web/stage/".len();
437 let last_slash = p.rfind('/').unwrap_or(p.len());
438 let id = &p[prefix_len..last_slash];
439 let verb = &p[last_slash + 1..];
440 let decision = match verb {
441 "pin" => crate::web::WebStageDecision::Pin,
442 "defer" => crate::web::WebStageDecision::Defer,
443 "block" => crate::web::WebStageDecision::Block,
444 "unblock" => crate::web::WebStageDecision::Unblock,
445 _ => unreachable!("matched in outer guard"),
446 };
447 crate::web::stage_decision_handler(state, id, body, decision, x_lex_user)
448 }
449 (Method::Get, "/v1/health") => json_response(200, &serde_json::json!({"ok": true, "caps": CAPS})),
451 (Method::Post, "/v1/parse") => parse_handler(body),
452 (Method::Post, "/v1/check") => check_handler(body),
453 (Method::Post, "/v1/publish") => publish_handler(state, body),
454 (Method::Post, "/v1/patch") => patch_handler(state, body),
455 (Method::Get, p) if p.starts_with("/v1/stage/") => {
456 let suffix = &p["/v1/stage/".len()..];
457 if let Some(id) = suffix.strip_suffix("/attestations") {
460 stage_attestations_handler(state, id)
461 } else {
462 stage_handler(state, suffix)
463 }
464 }
465 (Method::Post, "/v1/run") => run_handler(state, body, false),
466 (Method::Post, "/v1/replay") => run_handler(state, body, true),
467 (Method::Get, p) if p.starts_with("/v1/trace/") => {
468 let id = &p["/v1/trace/".len()..];
469 trace_handler(state, id)
470 }
471 (Method::Get, "/v1/diff") => diff_handler(state, query),
472 (Method::Post, "/v1/merge/start") => merge_start_handler(state, body),
473 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/resolve") => {
474 let id = &p["/v1/merge/".len()..p.len() - "/resolve".len()];
475 merge_resolve_handler(state, id, body)
476 }
477 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/commit") => {
478 let id = &p["/v1/merge/".len()..p.len() - "/commit".len()];
479 merge_commit_handler(state, id)
480 }
481 (Method::Post, "/v1/ops/batch") => ops_batch_handler(state, body),
483 (Method::Post, "/v1/attestations/batch") => attestations_batch_handler(state, body),
484 (Method::Post, "/v1/stages/batch") => crate::sync_http::stages_batch_handler(state, body),
487 (Method::Post, "/v1/stages/fetch") => crate::sync_http::stages_fetch_handler(state, body),
488 (Method::Post, "/v1/stages/missing") => crate::sync_http::stages_missing_handler(state, body),
489 (Method::Post, "/v1/intents/batch") => crate::sync_http::intents_batch_handler(state, body),
490 (Method::Post, "/v1/intents/fetch") => crate::sync_http::intents_fetch_handler(state, body),
491 (Method::Post, "/v1/locks/batch") => crate::sync_http::locks_batch_handler(state, body),
494 (Method::Post, "/v1/locks/fetch") => crate::sync_http::locks_fetch_handler(state, body),
495 (Method::Post, "/v1/blobs/missing") => crate::sync_http::blobs_missing_handler(state, body),
498 (Method::Post, "/v1/blobs/batch") => crate::sync_http::blobs_batch_handler(state, body),
499 (Method::Post, "/v1/blobs/fetch") => crate::sync_http::blobs_fetch_handler(state, body),
500 (Method::Post, "/v1/issues/batch") => crate::sync_http::issues_batch_handler(state, body),
503 (Method::Post, "/v1/issues/fetch") => crate::sync_http::issues_fetch_handler(state, body),
504 (Method::Get, "/v1/issues/list") => crate::sync_http::issues_list_handler(state),
505 (Method::Get, "/v1/issues") => crate::issues_http::issues_state_handler(state),
509 (Method::Get, "/v1/projects") => crate::issues_http::projects_handler(state),
510 (Method::Get, p) if p.starts_with("/v1/issues/") => {
511 crate::issues_http::issue_detail_handler(state, &p["/v1/issues/".len()..])
512 }
513 (Method::Get, "/v1/review/inbox") => crate::review_http::review_inbox_handler(state, query),
517 (Method::Post, "/v1/review/verdict") => crate::review_http::review_verdict_handler(state, body),
518 (Method::Get, "/v1/branches") => crate::branches_http::branches_list_handler(state),
519 (Method::Post, "/v1/branches") => crate::branches_http::branch_create_handler(state, body),
520 (Method::Post, p) if p.starts_with("/v1/branches/") && p.ends_with("/checkout") => {
521 let name = &p["/v1/branches/".len()..p.len() - "/checkout".len()];
522 crate::branches_http::branch_checkout_handler(state, name)
523 }
524 (Method::Get, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
527 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
528 crate::branches_http::branch_head_handler(state, name)
529 }
530 (Method::Post, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
531 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
532 crate::branches_http::branch_advance_head_handler(state, name, body)
533 }
534 (Method::Get, "/v1/ops/since") => crate::ops_since_http::ops_since_handler(state, query, x_lex_caps),
538 (Method::Get, "/v1/attestations/since") => attestations_since_handler(state, query),
539 (Method::Get, "/v1/pkg") => pkg_list_handler(state),
543 (Method::Put, p) if p.starts_with("/v1/pkg/") && p.ends_with("/visibility") => {
547 let name = &p["/v1/pkg/".len()..p.len() - "/visibility".len()];
548 pkg_set_visibility_handler(state, name, body)
549 }
550 (Method::Post, p) if p.starts_with("/v1/pkg/") && p.ends_with("/release") => {
553 let name = &p["/v1/pkg/".len()..p.len() - "/release".len()];
554 pkg_release_handler(state, name, body)
555 }
556 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/head") => {
557 let name = &p["/v1/pkg/".len()..p.len() - "/head".len()];
558 pkg_head_handler(state, name)
559 }
560 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/versions") => {
561 let name = &p["/v1/pkg/".len()..p.len() - "/versions".len()];
562 pkg_versions_handler(state, name)
563 }
564 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/api-diff") => {
565 let name = &p["/v1/pkg/".len()..p.len() - "/api-diff".len()];
566 pkg_api_diff_handler(state, name, query)
567 }
568 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/archive") => {
570 let inner = &p["/v1/pkg/".len()..p.len() - "/archive".len()];
571 if let Some((name, version)) = inner.split_once('/') {
573 pkg_archive_handler(state, name, version)
574 } else {
575 error_response(400, "expected /v1/pkg/{name}/{version}/archive")
576 }
577 }
578 (Method::Get, p) if p.starts_with("/v1/pkg/") && p["/v1/pkg/".len()..].contains('/') => {
580 let inner = &p["/v1/pkg/".len()..];
581 if let Some((name, version)) = inner.split_once('/') {
582 pkg_get_version_handler(state, name, version)
583 } else {
584 error_response(400, "expected /v1/pkg/{name}/{version}")
585 }
586 }
587 (Method::Get, p) if p.starts_with("/v1/pkg/") => {
588 let name = &p["/v1/pkg/".len()..];
589 pkg_get_handler(state, name)
590 }
591 (Method::Delete, p) if p.starts_with("/v1/pkg/") => {
592 let name = &p["/v1/pkg/".len()..];
593 pkg_delete_handler(state, name)
594 }
595 _ => error_response(404, format!("unknown route: {method:?} {path}")),
596 }
597}
598
599#[derive(Deserialize)]
600struct ParseReq { source: String }
601
602fn parse_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
603 let req: ParseReq = match serde_json::from_str(body) {
604 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
605 };
606 match load_program_from_str(&req.source) {
607 Ok(prog) => {
608 let stages = canonicalize_program(&prog);
609 json_response(200, &serde_json::to_value(&stages).unwrap())
610 }
611 Err(e) => error_response(400, format!("syntax error: {e}")),
612 }
613}
614
615pub(crate) fn check_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
616 let req: ParseReq = match serde_json::from_str(body) {
617 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
618 };
619 let prog = match load_program_from_str(&req.source) {
620 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
621 };
622 let stages = canonicalize_program(&prog);
623 match lex_types::check_program(&stages) {
624 Ok(_) => json_response(200, &serde_json::json!({"ok": true})),
625 Err(errs) => json_response(422, &serde_json::to_value(&errs).unwrap()),
626 }
627}
628
629#[derive(Deserialize)]
630struct PublishReq { source: String, #[serde(default)] activate: bool }
631
632pub(crate) fn publish_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
633 let req: PublishReq = match serde_json::from_str(body) {
634 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
635 };
636 let prog = match load_program_from_str(&req.source) {
637 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
638 };
639 let mut stages = canonicalize_program(&prog);
643 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
644 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
645 }
646 let example_errors = lex_runtime::evaluate_examples(&stages);
652 if !example_errors.is_empty() {
653 return error_with_detail(422, "example mismatch",
654 serde_json::to_value(&example_errors).unwrap_or_default());
655 }
656
657 let store = state.store.lock().unwrap();
658 let branch = store.current_branch();
659
660 let old_head = match store.branch_head(&branch) {
662 Ok(h) => h,
663 Err(e) => return error_response(500, format!("branch_head: {e}")),
664 };
665 let old_pairs: Vec<(String, String)> =
672 old_head.iter().map(|(sig, stg)| (sig.clone(), stg.clone())).collect();
673 let old_head_stages: Vec<lex_ast::Stage> =
674 store.get_asts_for_sigs_bulk(&old_pairs).into_iter().filter_map(Result::ok).collect();
675 let old_fns = stage_fns(&old_head_stages);
676 let new_fns = stage_fns(&stages);
677 let old_types = stage_types(&old_head_stages);
678 let new_types = stage_types(&stages);
679 let report =
680 lex_vcs::compute_diff_with_types(&old_fns, &new_fns, &old_types, &new_types, false);
681
682 let mut new_imports: lex_vcs::ImportMap = lex_vcs::ImportMap::new();
684 {
685 let entry = new_imports.entry("<source>".into()).or_default();
686 for s in &stages {
687 if let lex_ast::Stage::Import(im) = s {
688 entry.insert(lex_vcs::ImportRef {
689 reference: im.reference.clone(),
690 alias: im.alias.clone(),
691 });
692 }
693 }
694 }
695
696 match store.publish_program(&branch, &stages, &report, &new_imports, req.activate) {
697 Ok(outcome) => {
698 record_examples_for_publish(&store, &stages, &outcome);
702 json_response(200, &serde_json::json!({
703 "ops": outcome.ops,
704 "head_op": outcome.head_op,
705 }))
706 }
707 Err(lex_store::StoreError::TypeError(errs)) => {
715 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
716 }
717 Err(e) => write_error_response("publish_program", e),
718 }
719}
720
721#[derive(Deserialize)]
722struct PatchReq {
723 stage_id: String,
724 patch: lex_ast::Patch,
725 #[serde(default)] activate: bool,
726}
727
728fn patch_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
731 let req: PatchReq = match serde_json::from_str(body) {
732 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
733 };
734 let store = state.store.lock().unwrap();
735
736 let original = match store.get_ast(&req.stage_id) {
738 Ok(s) => s, Err(e) => return error_response(404, format!("stage: {e}")),
739 };
740
741 let patched = match lex_ast::apply_patch(&original, &req.patch) {
743 Ok(s) => s,
744 Err(e) => return error_with_detail(422, "patch failed",
745 serde_json::to_value(&e).unwrap_or_default()),
746 };
747
748 let branch = store.current_branch();
759
760 let sig = match lex_ast::sig_id(&patched) {
762 Some(s) => s,
763 None => return error_response(500, "patched stage has no sig_id"),
764 };
765
766 let new_id = match store.publish(&patched) {
769 Ok(id) => id, Err(e) => return error_response(500, format!("publish: {e}")),
770 };
771
772 let original_effects: std::collections::BTreeSet<String> = match &original {
774 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
775 _ => std::collections::BTreeSet::new(),
776 };
777 let patched_effects: std::collections::BTreeSet<String> = match &patched {
778 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
779 _ => std::collections::BTreeSet::new(),
780 };
781 let head_now = match store.get_branch(&branch) {
782 Ok(b) => b.and_then(|b| b.head_op),
783 Err(e) => return error_response(500, format!("get_branch: {e}")),
784 };
785 let kind = if original_effects != patched_effects {
786 let from_budget = lex_vcs::operation_budget_from_effects(&original_effects);
793 let to_budget = lex_vcs::operation_budget_from_effects(&patched_effects);
794 let to_sig_id = lex_ast::sig_id(&patched).filter(|s| *s != sig);
798 lex_vcs::OperationKind::ChangeEffectSig {
799 sig_id: sig.clone(),
800 from_stage_id: req.stage_id.clone(),
801 to_stage_id: new_id.clone(),
802 from_effects: original_effects,
803 to_effects: patched_effects,
804 from_budget,
805 to_budget,
806 to_sig_id,
807 }
808 } else {
809 let budget = lex_vcs::operation_budget_from_effects(&original_effects);
810 lex_vcs::OperationKind::ModifyBody {
811 sig_id: sig.clone(),
812 from_stage_id: req.stage_id.clone(),
813 to_stage_id: new_id.clone(),
814 from_budget: budget,
815 to_budget: budget,
816 to_sig_id: lex_ast::sig_id(&patched).filter(|s| *s != sig),
819 }
820 };
821 let transition = lex_store::transition_for_kind(&kind);
825 let op = lex_vcs::Operation::new(
826 kind,
827 head_now.into_iter().collect::<Vec<_>>(),
828 );
829 let op_id = match store.apply_operation_gated(&branch, op, transition) {
830 Ok(id) => id,
831 Err(lex_store::StoreError::TypeError(errs)) => return error_with_detail(
832 422, "type errors after patch", serde_json::to_value(&errs).unwrap_or_default()),
833 Err(e) => return write_error_response("apply_operation_gated", e),
834 };
835 if req.activate {
836 if let Err(e) = store.activate(&new_id) {
837 return error_response(500, format!("activate: {e}"));
838 }
839 }
840
841 let status = format!("{:?}",
842 store.get_status(&new_id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
843 json_response(200, &serde_json::json!({
844 "old_stage_id": req.stage_id,
845 "new_stage_id": new_id,
846 "sig_id": sig,
847 "status": status,
848 "op_id": op_id,
849 }))
850}
851
852pub(crate) fn stage_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
853 let store = state.store.lock().unwrap();
854 let meta = match store.get_metadata(id) {
855 Ok(m) => m, Err(e) => return error_response(404, format!("{e}")),
856 };
857 let ast = match store.get_ast(id) {
858 Ok(a) => a, Err(e) => return error_response(404, format!("{e}")),
859 };
860 let status = format!("{:?}", store.get_status(id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
861 json_response(200, &serde_json::json!({
862 "metadata": meta,
863 "ast": ast,
864 "status": status,
865 }))
866}
867
868pub(crate) fn stage_attestations_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
877 let store = state.store.lock().unwrap();
878 if let Err(e) = store.get_metadata(id) {
879 return error_response(404, format!("{e}"));
880 }
881 let log = match store.attestation_log() {
882 Ok(l) => l,
883 Err(e) => return error_response(500, format!("attestation log: {e}")),
884 };
885 let mut listing = match log.list_for_stage(&id.to_string()) {
886 Ok(v) => v,
887 Err(e) => return error_response(500, format!("list_for_stage: {e}")),
888 };
889 listing.sort_by_key(|a| std::cmp::Reverse(a.timestamp));
890 json_response(200, &serde_json::json!({"attestations": listing}))
891}
892
893#[derive(Deserialize, Default)]
894struct PolicyJson {
895 #[serde(default)] allow_effects: Vec<String>,
896 #[serde(default)] allow_fs_read: Vec<String>,
897 #[serde(default)] allow_fs_write: Vec<String>,
898 #[serde(default)] budget: Option<u64>,
899}
900
901impl PolicyJson {
902 fn into_policy(self) -> Policy {
903 Policy {
904 allow_effects: self.allow_effects.into_iter().collect::<BTreeSet<_>>(),
905 allow_fs_read: self.allow_fs_read.into_iter().map(PathBuf::from).collect(),
906 allow_fs_write: self.allow_fs_write.into_iter().map(PathBuf::from).collect(),
907 allow_net_host: Vec::new(),
908 allow_proc: Vec::new(),
909 allow_approval: Vec::new(),
910 budget: self.budget,
911 }
912 }
913}
914
915#[derive(Deserialize)]
916struct RunReq {
917 source: String,
918 #[serde(rename = "fn")] func: String,
919 #[serde(default)] args: Vec<serde_json::Value>,
920 #[serde(default)] policy: PolicyJson,
921 #[serde(default)] overrides: IndexMap<String, serde_json::Value>,
922}
923
924pub(crate) fn run_handler(state: &State, body: &str, with_overrides: bool) -> Response<std::io::Cursor<Vec<u8>>> {
925 let req: RunReq = match serde_json::from_str(body) {
926 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
927 };
928 let prog = match load_program_from_str(&req.source) {
929 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
930 };
931 let stages = canonicalize_program(&prog);
932 if let Err(errs) = lex_types::check_program(&stages) {
933 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
934 }
935 let bc = compile_program(&stages);
936 let mut policy = req.policy.into_policy();
937 if let Some(ceiling) = &state.policy_ceiling {
943 policy = clamp_policy(policy, ceiling);
944 }
945 if let Err(violations) = check_policy(&bc, &policy) {
946 return error_with_detail(403, "policy violation", serde_json::to_value(&violations).unwrap());
947 }
948
949 let mut recorder = lex_trace::Recorder::new();
950 if with_overrides && !req.overrides.is_empty() {
951 recorder = recorder.with_overrides(req.overrides);
952 }
953 let handle = recorder.handle();
954 let handler = DefaultHandler::new(policy);
955 let mut vm = Vm::with_handler(&bc, Box::new(handler));
956 vm.set_tracer(Box::new(recorder));
957
958 let vargs: Vec<Value> = req.args.iter().map(json_to_value).collect();
959 let started = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
960 let result = vm.call(&req.func, vargs);
961 let ended = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
962
963 let store = state.store.lock().unwrap();
964 let (root_out, root_err, status) = match &result {
965 Ok(v) => (Some(value_to_json(v)), None, 200u16),
966 Err(e) => (None, Some(format!("{e}")), 200u16),
967 };
968 let tree = handle.finalize(req.func.clone(), serde_json::Value::Null,
969 root_out.clone(), root_err.clone(), started, ended);
970 let run_id = match store.save_trace(&tree) {
971 Ok(id) => id,
972 Err(e) => return error_response(500, format!("save_trace: {e}")),
973 };
974
975 let mut body = serde_json::json!({
976 "run_id": run_id,
977 "output": root_out,
978 });
979 if let Some(err) = root_err {
980 body["error"] = serde_json::Value::String(err);
981 }
982 json_response(status, &body)
983}
984
985fn trace_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
986 let store = state.store.lock().unwrap();
987 match store.load_trace(id) {
988 Ok(t) => json_response(200, &serde_json::to_value(&t).unwrap()),
989 Err(e) => error_response(404, format!("{e}")),
990 }
991}
992
993fn diff_handler(state: &State, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
994 let mut a = None;
995 let mut b = None;
996 for kv in query.split('&') {
997 if let Some((k, v)) = kv.split_once('=') {
998 match k { "a" => a = Some(v.to_string()), "b" => b = Some(v.to_string()), _ => {} }
999 }
1000 }
1001 let (Some(a), Some(b)) = (a, b) else {
1002 return error_response(400, "missing a or b query params");
1003 };
1004 let store = state.store.lock().unwrap();
1005 let ta = match store.load_trace(&a) { Ok(t) => t, Err(e) => return error_response(404, format!("a: {e}")) };
1006 let tb = match store.load_trace(&b) { Ok(t) => t, Err(e) => return error_response(404, format!("b: {e}")) };
1007 match lex_trace::diff_runs(&ta, &tb) {
1008 Some(d) => json_response(200, &serde_json::to_value(&d).unwrap()),
1009 None => json_response(200, &serde_json::json!({"divergence": null})),
1010 }
1011}
1012
1013fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
1014
1015fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
1016
1017#[derive(Deserialize)]
1018struct MergeStartReq {
1019 src_branch: String,
1020 dst_branch: String,
1021}
1022
1023fn merge_start_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1034 let req: MergeStartReq = match serde_json::from_str(body) {
1035 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
1036 };
1037 let store = state.store.lock().unwrap();
1038 let src_head = match store.get_branch(&req.src_branch) {
1039 Ok(Some(b)) => b.head_op,
1040 Ok(None) => return error_response(404, format!("unknown src branch `{}`", req.src_branch)),
1041 Err(e) => return error_response(500, format!("src branch read: {e}")),
1042 };
1043 let dst_head = match store.get_branch(&req.dst_branch) {
1044 Ok(Some(b)) => b.head_op,
1045 Ok(None) => return error_response(404, format!("unknown dst branch `{}`", req.dst_branch)),
1046 Err(e) => return error_response(500, format!("dst branch read: {e}")),
1047 };
1048 let log = match lex_vcs::OpLog::open(store.root()) {
1049 Ok(l) => l,
1050 Err(e) => return error_response(500, format!("op log: {e}")),
1051 };
1052 let merge_id = mint_merge_id();
1056 let mut session = match MergeSession::start(
1057 merge_id.clone(),
1058 &log,
1059 src_head.as_ref(),
1060 dst_head.as_ref(),
1061 ) {
1062 Ok(s) => s,
1063 Err(e) => return error_response(500, format!("merge start: {e}")),
1064 };
1065 let manifest_outcome = match store.manifest_merge(
1071 session.lca.as_deref(),
1072 dst_head.as_deref(),
1073 src_head.as_deref(),
1074 ) {
1075 Ok(o) => o,
1076 Err(lex_store::StoreError::AmbiguousManifest { op_id }) => {
1077 return error_with_detail(422, "AmbiguousManifest", serde_json::json!({
1078 "op_id": op_id,
1079 "reason": "a merge ancestor has an ambiguous files manifest; \
1080 append a set_files op resolving it before merging",
1081 }));
1082 }
1083 Err(e) => return error_response(500, format!("manifest merge: {e}")),
1084 };
1085 let (file_conflicts, needs_setfiles): (Vec<lex_vcs::FileConflict>, bool) =
1086 match manifest_outcome {
1087 lex_store::ManifestMergeOutcome::NoChange => (Vec::new(), false),
1088 lex_store::ManifestMergeOutcome::Needed { conflicts, .. } => (conflicts, true),
1089 };
1090 session.attach_file_conflicts(file_conflicts, needs_setfiles);
1091
1092 let conflicts: Vec<&lex_vcs::ConflictRecord> = session.remaining_conflicts();
1093 let auto_resolved_count = session.auto_resolved.len();
1094 let remaining_file_conflicts: Vec<&lex_vcs::FileConflict> = session.remaining_file_conflicts();
1095 let body = serde_json::json!({
1096 "merge_id": merge_id,
1097 "src_head": session.src_head,
1098 "dst_head": session.dst_head,
1099 "lca": session.lca,
1100 "conflicts": conflicts,
1101 "auto_resolved_count": auto_resolved_count,
1102 "file_conflicts": remaining_file_conflicts,
1103 "needs_setfiles": session.needs_setfiles(),
1104 });
1105 drop(conflicts);
1106 drop(remaining_file_conflicts);
1107 drop(store);
1108 let wrapped = ApiMergeSession {
1109 inner: session,
1110 src_branch: req.src_branch,
1111 dst_branch: req.dst_branch,
1112 };
1113 state.sessions.lock().unwrap().insert(merge_id, wrapped);
1114 json_response(200, &body)
1115}
1116
1117#[derive(Deserialize)]
1118struct MergeResolveReq {
1119 #[serde(default)]
1124 resolutions: Vec<MergeResolveEntry>,
1125 #[serde(default)]
1131 file_resolutions: Vec<MergeFileResolveEntry>,
1132}
1133
1134#[derive(Deserialize)]
1135struct MergeResolveEntry {
1136 conflict_id: String,
1137 resolution: lex_vcs::Resolution,
1138}
1139
1140#[derive(Deserialize)]
1141struct MergeFileResolveEntry {
1142 path: lex_vcs::FilePath,
1143 resolution: lex_vcs::FileResolution,
1144}
1145
1146fn merge_resolve_handler(
1157 state: &State,
1158 merge_id: &str,
1159 body: &str,
1160) -> Response<std::io::Cursor<Vec<u8>>> {
1161 let req: MergeResolveReq = match serde_json::from_str(body) {
1162 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
1163 };
1164 let mut sessions = state.sessions.lock().unwrap();
1165 let Some(wrapped) = sessions.get_mut(merge_id) else {
1166 return error_response(404, format!("unknown merge_id `{merge_id}`"));
1167 };
1168 let pairs: Vec<(String, lex_vcs::Resolution)> = req.resolutions.into_iter()
1169 .map(|e| (e.conflict_id, e.resolution))
1170 .collect();
1171 let store = state.store.lock().unwrap();
1176 let checker = lex_store::MergeResolutionChecker::new(&store, wrapped.dst_branch.clone());
1177 let verdicts = wrapped.inner.resolve_checked(pairs, &checker);
1178 drop(store);
1179 let file_pairs: Vec<(lex_vcs::FilePath, lex_vcs::FileResolution)> = req.file_resolutions
1180 .into_iter()
1181 .map(|e| (e.path, e.resolution))
1182 .collect();
1183 let file_verdicts = wrapped.inner.resolve_files(file_pairs);
1184 let remaining: Vec<&lex_vcs::ConflictRecord> = wrapped.inner.remaining_conflicts();
1185 let remaining_files: Vec<&lex_vcs::FileConflict> = wrapped.inner.remaining_file_conflicts();
1186 let body = serde_json::json!({
1187 "verdicts": verdicts,
1188 "remaining_conflicts": remaining,
1189 "file_verdicts": file_verdicts,
1190 "remaining_file_conflicts": remaining_files,
1191 });
1192 json_response(200, &body)
1193}
1194
1195fn merge_commit_handler(
1217 state: &State,
1218 merge_id: &str,
1219) -> Response<std::io::Cursor<Vec<u8>>> {
1220 use std::collections::BTreeMap;
1221 let wrapped = match state.sessions.lock().unwrap().remove(merge_id) {
1222 Some(w) => w,
1223 None => return error_response(404, format!("unknown merge_id `{merge_id}`")),
1224 };
1225 let dst_branch = wrapped.dst_branch.clone();
1226 let src_head = wrapped.inner.src_head.clone();
1227 let dst_head = wrapped.inner.dst_head.clone();
1228 let lca = wrapped.inner.lca.clone();
1229 let auto_resolved = wrapped.inner.auto_resolved.clone();
1230
1231 let mut entries: BTreeMap<lex_vcs::SigId, Option<lex_vcs::StageId>> = BTreeMap::new();
1234
1235 for outcome in &auto_resolved {
1237 if let lex_vcs::MergeOutcome::Src { sig_id, stage_id } = outcome {
1238 entries.insert(sig_id.clone(), stage_id.clone());
1239 }
1240 }
1241
1242 let commit_out = match wrapped.inner.commit() {
1244 Ok(r) => r,
1245 Err(lex_vcs::CommitError::ConflictsRemaining(ids)) => {
1246 return error_with_detail(
1250 422,
1251 "conflicts remaining",
1252 serde_json::json!({"unresolved": ids}),
1253 );
1254 }
1255 Err(lex_vcs::CommitError::FileConflictsRemaining(paths)) => {
1256 return error_with_detail(
1257 422,
1258 "file conflicts remaining",
1259 serde_json::json!({"unresolved_files": paths}),
1260 );
1261 }
1262 };
1263
1264 for (conflict_id, resolution) in commit_out.resolved {
1265 match resolution {
1266 lex_vcs::Resolution::TakeOurs => {
1267 }
1269 lex_vcs::Resolution::TakeTheirs => {
1270 match resolve_take_theirs(state, &src_head, &conflict_id) {
1280 Ok(stage_id) => {
1281 entries.insert(conflict_id.clone(), stage_id);
1282 }
1283 Err(e) => return error_response(500, format!("resolve take_theirs: {e}")),
1284 }
1285 }
1286 lex_vcs::Resolution::Custom { op } => {
1287 match op.kind.merge_target() {
1296 Some((sig, stage)) => {
1297 if sig != conflict_id {
1298 return error_with_detail(
1299 422,
1300 "custom op targets a different sig than the conflict",
1301 serde_json::json!({
1302 "conflict_id": conflict_id,
1303 "op_targets": sig,
1304 }),
1305 );
1306 }
1307 entries.insert(conflict_id, stage);
1308 }
1309 None => {
1310 return error_with_detail(
1311 422,
1312 "custom op kind doesn't yield a single sig→stage delta",
1313 serde_json::json!({
1314 "conflict_id": conflict_id,
1315 "kind": serde_json::to_value(&op.kind).unwrap_or(serde_json::Value::Null),
1316 }),
1317 );
1318 }
1319 }
1320 }
1321 lex_vcs::Resolution::Defer => {
1322 return error_response(500, "internal: Defer slipped past commit gate");
1324 }
1325 }
1326 }
1327
1328 let resolved_count = entries.len();
1329 let mut parents: Vec<lex_vcs::OpId> = Vec::new();
1330 if let Some(d) = dst_head.clone() { parents.push(d); }
1331 if let Some(s) = src_head.clone() { parents.push(s); }
1332 let op = lex_vcs::Operation::new(
1333 lex_vcs::OperationKind::Merge { resolved: resolved_count },
1334 parents,
1335 );
1336 let transition = lex_vcs::StageTransition::Merge { entries };
1337 let store = state.store.lock().unwrap();
1338
1339 let manifest_blob: Option<String> = if commit_out.needs_setfiles {
1348 match store.manifest_merge(lca.as_deref(), dst_head.as_deref(), src_head.as_deref()) {
1349 Ok(lex_store::ManifestMergeOutcome::NoChange) => None,
1350 Ok(lex_store::ManifestMergeOutcome::Needed { auto_entries, conflicts }) => {
1351 let file_resolutions: BTreeMap<lex_vcs::FilePath, lex_vcs::FileResolution> =
1352 commit_out.resolved_files.into_iter().collect();
1353 match store.build_merged_manifest(
1354 auto_entries,
1355 &conflicts,
1356 &file_resolutions,
1357 "pending-merge",
1358 ) {
1359 Ok(blob_id) => Some(blob_id),
1360 Err(e) => return write_error_response("build merged manifest", e),
1361 }
1362 }
1363 Err(lex_store::StoreError::AmbiguousManifest { op_id }) => {
1364 return error_with_detail(422, "AmbiguousManifest", serde_json::json!({
1365 "op_id": op_id,
1366 }));
1367 }
1368 Err(e) => return write_error_response("manifest merge", e),
1369 }
1370 } else {
1371 None
1372 };
1373
1374 match store.apply_merge_op_gated_with_manifest(&dst_branch, op, transition, manifest_blob.as_deref(), None) {
1380 Ok(new_head_op) => json_response(200, &serde_json::json!({
1381 "new_head_op": new_head_op,
1382 "dst_branch": dst_branch,
1383 })),
1384 Err(lex_store::StoreError::TypeError(errs)) => error_with_detail(
1385 422, "merged program has type errors", serde_json::to_value(&errs).unwrap_or_default()),
1386 Err(e @ lex_store::StoreError::DependencyConflict { .. }) => {
1392 let detail = match &e {
1393 lex_store::StoreError::DependencyConflict { package, dst_version, src_version } => {
1394 serde_json::json!({
1395 "kind": "dependency_conflict",
1396 "package": package,
1397 "dst_branch": dst_branch,
1398 "dst_version": dst_version,
1399 "src_branch": wrapped.src_branch,
1400 "src_version": src_version,
1401 })
1402 }
1403 _ => serde_json::Value::Null,
1404 };
1405 error_with_detail(409, e.to_string(), detail)
1406 }
1407 Err(e @ lex_store::StoreError::AmbiguousManifest { .. }) => {
1408 error_with_detail(422, "AmbiguousManifest", serde_json::json!({"detail": e.to_string()}))
1409 }
1410 Err(e @ lex_store::StoreError::InvalidManifest(_))
1411 | Err(e @ lex_store::StoreError::MissingBlobs(_)) => {
1412 error_with_detail(422, e.to_string(), serde_json::Value::Null)
1413 }
1414 Err(e) => write_error_response("apply merge op", e),
1415 }
1416}
1417
1418fn resolve_take_theirs(
1423 state: &State,
1424 src_head: &Option<lex_vcs::OpId>,
1425 sig: &lex_vcs::SigId,
1426) -> std::io::Result<Option<lex_vcs::StageId>> {
1427 let store = state.store.lock().unwrap();
1428 let log = lex_vcs::OpLog::open(store.root())?;
1429 let Some(head) = src_head.as_ref() else { return Ok(None); };
1430 let mut current: Option<lex_vcs::StageId> = None;
1433 for record in log.walk_forward(head, None)? {
1434 match &record.produces {
1435 lex_vcs::StageTransition::Create { sig_id, stage_id }
1436 if sig_id == sig => { current = Some(stage_id.clone()); }
1437 lex_vcs::StageTransition::Replace { sig_id, to, .. }
1438 if sig_id == sig => { current = Some(to.clone()); }
1439 lex_vcs::StageTransition::Remove { sig_id, .. }
1440 if sig_id == sig => { current = None; }
1441 lex_vcs::StageTransition::Rename { from, to, body_stage_id }
1442 if from == sig || to == sig => {
1443 if from == sig { current = None; }
1444 if to == sig { current = Some(body_stage_id.clone()); }
1445 }
1446 lex_vcs::StageTransition::Merge { entries } => {
1447 if let Some(opt) = entries.get(sig) {
1448 current = opt.clone();
1449 }
1450 }
1451 _ => {}
1452 }
1453 }
1454 Ok(current)
1455}
1456
1457fn mint_merge_id() -> MergeSessionId {
1458 use std::sync::atomic::{AtomicU64, Ordering};
1459 static COUNTER: AtomicU64 = AtomicU64::new(0);
1460 let nanos = SystemTime::now()
1461 .duration_since(UNIX_EPOCH)
1462 .map(|d| d.as_nanos())
1463 .unwrap_or(0);
1464 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1465 format!("merge_{nanos:x}_{n:x}")
1466}
1467
1468pub(crate) fn ops_batch_handler(state: &State, body: &str)
1504 -> Response<std::io::Cursor<Vec<u8>>>
1505{
1506 let records: Vec<lex_vcs::OperationRecord> = match serde_json::from_str(body) {
1507 Ok(r) => r,
1508 Err(e) => return error_response(400,
1509 format!("body must be a JSON array of OperationRecord: {e}")),
1510 };
1511 let store = state.store.lock().unwrap();
1512 let log = match lex_vcs::OpLog::open(store.root()) {
1513 Ok(l) => l,
1514 Err(e) => return error_response(500, format!("opening op log: {e}")),
1515 };
1516
1517 let mut batch_ids: std::collections::BTreeSet<lex_vcs::OpId> =
1525 std::collections::BTreeSet::new();
1526 for rec in &records {
1527 let expected = rec.op.op_id();
1528 if expected != rec.op_id {
1529 return error_with_detail(409, "OpIdMismatch", serde_json::json!({
1530 "supplied": rec.op_id,
1531 "expected": expected,
1532 }));
1533 }
1534 for parent in &rec.op.parents {
1535 let known = match log.get(parent) {
1536 Ok(Some(_)) => true,
1537 Ok(None) => false,
1538 Err(e) => return error_response(500, format!("op log read: {e}")),
1539 };
1540 if !known && !batch_ids.contains(parent) {
1541 return error_with_detail(422, "MissingParent", serde_json::json!({
1542 "op_id": rec.op_id,
1543 "missing_parent": parent,
1544 }));
1545 }
1546 }
1547 if let Err(resp) = crate::sync_http::check_set_files(&store, state.blob_limits, rec) {
1548 return resp;
1549 }
1550 batch_ids.insert(rec.op_id.clone());
1551 }
1552
1553 let mut added = 0usize;
1556 let mut added_ids: Vec<&lex_vcs::OpId> = Vec::new();
1557 for rec in &records {
1558 let already_present = matches!(log.get(&rec.op_id), Ok(Some(_)));
1559 match log.put(rec) {
1560 Ok(()) => {
1561 if !already_present {
1562 added += 1;
1563 added_ids.push(&rec.op_id);
1564 }
1565 }
1566 Err(e) => return error_response(500, format!("op log write: {e}")),
1567 }
1568 }
1569
1570 json_response(200, &serde_json::json!({
1571 "received": records.len(),
1572 "added": added,
1573 "skipped": records.len() - added,
1574 "added_ids": added_ids,
1575 }))
1576}
1577
1578pub(crate) fn attestations_batch_handler(state: &State, body: &str)
1600 -> Response<std::io::Cursor<Vec<u8>>>
1601{
1602 let attestations: Vec<lex_vcs::Attestation> = match serde_json::from_str(body) {
1603 Ok(a) => a,
1604 Err(e) => return error_response(400,
1605 format!("body must be a JSON array of Attestation: {e}")),
1606 };
1607 let store = state.store.lock().unwrap();
1608 let log = match store.attestation_log() {
1609 Ok(l) => l,
1610 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1611 };
1612 let op_log = match lex_vcs::OpLog::open(store.root()) {
1613 Ok(l) => l,
1614 Err(e) => return error_response(500, format!("opening op log: {e}")),
1615 };
1616
1617 for att in &attestations {
1619 let expected = lex_vcs::Attestation::with_timestamp(
1622 att.stage_id.clone(),
1623 att.op_id.clone(),
1624 att.intent_id.clone(),
1625 att.kind.clone(),
1626 att.result.clone(),
1627 att.produced_by.clone(),
1628 att.cost.clone(),
1629 att.timestamp,
1630 ).attestation_id;
1631 if expected != att.attestation_id {
1632 return error_with_detail(409, "AttestationIdMismatch", serde_json::json!({
1633 "supplied": att.attestation_id,
1634 "expected": expected,
1635 }));
1636 }
1637 if let Some(op_id) = &att.op_id {
1641 match op_log.get(op_id) {
1642 Ok(Some(_)) => {}
1643 Ok(None) => return error_with_detail(422, "UnknownOp", serde_json::json!({
1644 "attestation_id": att.attestation_id,
1645 "op_id": op_id,
1646 })),
1647 Err(e) => return error_response(500, format!("op log read: {e}")),
1648 }
1649 }
1650 }
1651
1652 let mut added = 0usize;
1656 let mut added_ids: Vec<&lex_vcs::AttestationId> = Vec::new();
1657 for att in &attestations {
1658 let already_present = matches!(log.get(&att.attestation_id), Ok(Some(_)));
1659 match log.put(att) {
1660 Ok(()) => {
1661 if !already_present {
1662 added += 1;
1663 added_ids.push(&att.attestation_id);
1664 }
1665 }
1666 Err(e) => return error_response(500, format!("attestation log write: {e}")),
1667 }
1668 }
1669
1670 json_response(200, &serde_json::json!({
1671 "received": attestations.len(),
1672 "added": added,
1673 "skipped": attestations.len() - added,
1674 "added_ids": added_ids,
1675 }))
1676}
1677
1678pub(crate) fn attestations_since_handler(state: &State, query: &str)
1691 -> Response<std::io::Cursor<Vec<u8>>>
1692{
1693 let mut after_op: Option<String> = None;
1694 let mut limit: Option<usize> = None;
1695 for kv in query.split('&') {
1696 let Some((k, v)) = kv.split_once('=') else { continue };
1697 match k {
1698 "after-op" => after_op = Some(v.to_string()),
1699 "limit" => {
1700 limit = Some(match v.parse::<usize>() {
1701 Ok(n) => n,
1702 Err(_) => return error_response(400,
1703 format!("limit must be a positive integer, got `{v}`")),
1704 });
1705 }
1706 _ => {}
1707 }
1708 }
1709
1710 let store = state.store.lock().unwrap();
1711 let log = match store.attestation_log() {
1712 Ok(l) => l,
1713 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1714 };
1715
1716 let exclude: std::collections::BTreeSet<String> = match &after_op {
1720 None => std::collections::BTreeSet::new(),
1721 Some(cutoff) => {
1722 let op_log = match lex_vcs::OpLog::open(store.root()) {
1723 Ok(l) => l,
1724 Err(e) => return error_response(500, format!("opening op log: {e}")),
1725 };
1726 match op_log.walk_back(cutoff, None) {
1727 Ok(records) => records.into_iter().map(|r| r.op_id).collect(),
1728 Err(_) => {
1729 std::collections::BTreeSet::new()
1733 }
1734 }
1735 }
1736 };
1737
1738 let all = match log.list_all() {
1739 Ok(v) => v,
1740 Err(e) => return error_response(500, format!("listing attestations: {e}")),
1741 };
1742 let mut filtered: Vec<lex_vcs::Attestation> = all
1743 .into_iter()
1744 .filter(|a| match &a.op_id {
1745 Some(op_id) => !exclude.contains(op_id),
1746 None => true,
1750 })
1751 .collect();
1752 filtered.sort_by(|a, b| {
1756 a.timestamp.cmp(&b.timestamp)
1757 .then_with(|| a.attestation_id.cmp(&b.attestation_id))
1758 });
1759 if let Some(n) = limit {
1760 filtered.truncate(n);
1761 }
1762
1763 json_response(200, &serde_json::to_value(&filtered).unwrap_or_default())
1764}
1765
1766#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
1775struct DepSpec {
1776 #[serde(default, skip_serializing_if = "Option::is_none")]
1777 registry: Option<String>,
1778 #[serde(default, skip_serializing_if = "Option::is_none")]
1779 version: Option<String>,
1780 #[serde(default, skip_serializing_if = "Option::is_none")]
1781 git: Option<String>,
1782 #[serde(default, skip_serializing_if = "Option::is_none")]
1783 branch: Option<String>,
1784 #[serde(default, skip_serializing_if = "Option::is_none")]
1785 tag: Option<String>,
1786 #[serde(default, skip_serializing_if = "Option::is_none")]
1787 rev: Option<String>,
1788 #[serde(default, skip_serializing_if = "Option::is_none")]
1789 path: Option<String>,
1790}
1791
1792impl DepSpec {
1793 fn to_toml_inline(&self) -> Option<String> {
1798 let mut parts: Vec<String> = Vec::new();
1799 let mut push = |k: &str, v: &Option<String>| {
1800 if let Some(val) = v {
1801 parts.push(format!("{k} = {}", toml_str(val)));
1802 }
1803 };
1804 push("registry", &self.registry);
1805 push("version", &self.version);
1806 push("git", &self.git);
1807 push("branch", &self.branch);
1808 push("tag", &self.tag);
1809 push("rev", &self.rev);
1810 push("path", &self.path);
1811 if parts.is_empty() {
1812 None
1813 } else {
1814 Some(format!("{{ {} }}", parts.join(", ")))
1815 }
1816 }
1817}
1818
1819fn toml_str(s: &str) -> String {
1821 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
1822}
1823
1824#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1826struct PkgRecord {
1827 name: String,
1828 version: String,
1829 head_op: Option<String>,
1830 published_at: u64,
1831 function_names: Vec<String>,
1833 #[serde(default)]
1838 dependencies: Vec<String>,
1839 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
1845 dependency_specs: std::collections::BTreeMap<String, DepSpec>,
1846 ops: Vec<serde_json::Value>,
1848}
1849
1850#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
1857#[serde(rename_all = "lowercase")]
1858pub enum Visibility {
1859 #[default]
1860 Private,
1861 Public,
1862}
1863
1864#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
1869struct PkgIndex {
1870 latest: Option<String>,
1872 versions: Vec<PkgVersionSummary>,
1874 #[serde(default)]
1878 visibility: Visibility,
1879}
1880
1881#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1882struct PkgVersionSummary {
1883 version: String,
1884 head_op: Option<String>,
1885 published_at: u64,
1886}
1887
1888fn pkg_name_dir(root: &std::path::Path, name: &str) -> PathBuf {
1889 root.join("packages").join(name)
1890}
1891
1892fn pkg_index_path(root: &std::path::Path, name: &str) -> PathBuf {
1893 pkg_name_dir(root, name).join("index.json")
1894}
1895
1896fn pkg_version_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1897 pkg_name_dir(root, name).join(format!("{version}.json"))
1898}
1899
1900fn pkg_archive_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1901 pkg_name_dir(root, name).join(format!("{version}.tar.gz"))
1902}
1903
1904fn load_pkg_index(root: &std::path::Path, name: &str) -> Option<PkgIndex> {
1905 let bytes = std::fs::read(pkg_index_path(root, name)).ok()?;
1906 serde_json::from_slice(&bytes).ok()
1907}
1908
1909fn load_pkg_record(root: &std::path::Path, name: &str, version: &str) -> Option<PkgRecord> {
1910 let bytes = std::fs::read(pkg_version_path(root, name, version)).ok()?;
1911 serde_json::from_slice(&bytes).ok()
1912}
1913
1914fn load_latest_pkg_record(root: &std::path::Path, name: &str) -> Option<PkgRecord> {
1915 let index = load_pkg_index(root, name)?;
1916 let latest = index.latest.clone()?;
1917 load_pkg_record(root, name, &latest)
1918}
1919
1920fn pkg_is_public(root: &std::path::Path, name: &str) -> bool {
1924 load_pkg_index(root, name).map(|i| i.visibility) == Some(Visibility::Public)
1925}
1926
1927fn valid_pkg_segment(s: &str) -> bool {
1932 !s.is_empty()
1933 && s.len() <= 128
1934 && s != "."
1935 && s != ".."
1936 && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
1937}
1938
1939#[derive(Deserialize)]
1940struct VisibilityReq {
1941 visibility: Visibility,
1942}
1943
1944fn pkg_set_visibility_handler(
1951 state: &State,
1952 name: &str,
1953 body: &str,
1954) -> Response<std::io::Cursor<Vec<u8>>> {
1955 if !valid_pkg_segment(name) {
1956 return error_response(400, format!("invalid package name {name:?}"));
1957 }
1958 let req: VisibilityReq = match serde_json::from_str(body) {
1959 Ok(r) => r,
1960 Err(e) => return error_response(400, format!("bad request: {e}")),
1961 };
1962 let mut index = match load_pkg_index(&state.root, name) {
1963 Some(i) => i,
1964 None => return error_response(404, format!("package {name:?} not found")),
1965 };
1966 index.visibility = req.visibility;
1967 let bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1968 match std::fs::write(pkg_index_path(&state.root, name), bytes) {
1969 Ok(()) => json_response(
1970 200,
1971 &serde_json::json!({ "name": name, "visibility": index.visibility }),
1972 ),
1973 Err(e) => error_response(500, format!("write index: {e}")),
1974 }
1975}
1976
1977#[derive(serde::Deserialize)]
1978struct ReleaseReq {
1979 version: String,
1980 #[serde(default)]
1981 branch: Option<String>,
1982 #[serde(default)]
1985 dependencies: Vec<String>,
1986 #[serde(default)]
1991 dependency_specs: std::collections::BTreeMap<String, DepSpec>,
1992}
1993
1994fn pkg_release_handler(state: &State, name: &str, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2003 if !valid_pkg_segment(name) {
2004 return error_response(400, format!("invalid package name {name:?}"));
2005 }
2006 let req: ReleaseReq = match serde_json::from_str(body) {
2007 Ok(r) => r,
2008 Err(e) => return error_response(400, format!("bad request: {e}")),
2009 };
2010 let version = req.version.trim().to_string();
2011 if version.is_empty() || !valid_pkg_segment(&version) {
2012 return error_response(400, "version must be a non-empty, path-safe string (e.g. 1.2.0)");
2013 }
2014 if load_pkg_record(&state.root, name, &version).is_some() {
2016 return error_response(
2017 409,
2018 format!("{name}@{version} already released; releases are immutable — bump the version"),
2019 );
2020 }
2021
2022 let store = state.store.lock().unwrap();
2023 let branch = req.branch.unwrap_or_else(|| store.current_branch());
2024 let head_op = match store.get_branch(&branch) {
2025 Ok(Some(b)) => b.head_op,
2026 Ok(None) => return error_response(404, format!("unknown branch {branch:?}")),
2027 Err(e) => return error_response(500, format!("get_branch: {e}")),
2028 };
2029 let Some(head_op) = head_op else {
2030 return error_response(400, format!("branch {branch:?} has no commits to release"));
2031 };
2032
2033 let predecessor = load_pkg_index(&state.root, name)
2044 .map(|i| i.versions)
2045 .unwrap_or_default()
2046 .into_iter()
2047 .filter_map(|v| lex_syntax::semver::parse_exact(&v.version).map(|p| (p, v)))
2048 .filter(|(p, _)| lex_syntax::semver::parse_exact(&version).map(|n| *p < n).unwrap_or(false))
2049 .max_by_key(|(p, _)| *p)
2050 .map(|(_, v)| v);
2051 if let Some(prev) = predecessor {
2052 if let (Some(prev_head), Some(declared)) = (
2053 prev.head_op.clone(),
2054 lex_syntax::semver::bump_between(&prev.version, &version),
2055 ) {
2056 if let (Ok(prev_api), Ok(new_api)) = (
2057 lex_store::api::public_api_at_op(&store, &prev_head),
2058 lex_store::api::public_api_at_op(&store, &head_op),
2059 ) {
2060 use lex_store::api::ApiChange;
2061 use lex_syntax::semver::Bump;
2062 let (required, why) = match lex_store::api::classify_api_change(&prev_api, &new_api) {
2063 ApiChange::Breaking(d) => (Bump::Major, d),
2064 ApiChange::Additive(d) => (Bump::Minor, d),
2065 ApiChange::None => (Bump::Patch, String::new()),
2066 };
2067 if declared < required {
2068 let need = match required {
2069 Bump::Major => "major",
2070 Bump::Minor => "minor",
2071 Bump::Patch => "patch",
2072 };
2073 return error_response(
2074 422,
2075 format!(
2076 "version bump too small: {} → {version} is a {declared:?} bump, \
2077 but the API change ({why}) requires a {need} bump",
2078 prev.version
2079 ),
2080 );
2081 }
2082 }
2083 }
2084 }
2085
2086 let head = store.branch_head(&branch).unwrap_or_default();
2089 let pairs: Vec<(String, String)> = head.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
2090 let function_names: Vec<String> = store
2091 .get_asts_for_sigs_bulk(&pairs)
2092 .into_iter()
2093 .filter_map(|r| r.ok())
2094 .filter_map(|s| match s {
2095 lex_ast::Stage::FnDecl(fd) => Some(fd.name),
2096 _ => None,
2097 })
2098 .collect();
2099 let mut deps: std::collections::BTreeSet<String> = req.dependencies.into_iter().collect();
2103 if let Ok(extracted) = lex_store::api::external_dependencies_at_op(&store, &head_op) {
2104 deps.extend(extracted);
2105 }
2106 deps.extend(req.dependency_specs.keys().cloned());
2109 let dependencies: Vec<String> = deps.into_iter().collect();
2110 drop(store);
2111
2112 let published_at = std::time::SystemTime::now()
2113 .duration_since(std::time::UNIX_EPOCH)
2114 .map(|d| d.as_secs())
2115 .unwrap_or(0);
2116 let record = PkgRecord {
2117 name: name.to_string(),
2118 version: version.clone(),
2119 head_op: Some(head_op.clone()),
2120 published_at,
2121 function_names,
2122 dependencies,
2123 dependency_specs: req.dependency_specs,
2124 ops: Vec::new(),
2125 };
2126 if let Err(e) = save_pkg_record(&state.root, &record, None) {
2127 return error_response(500, format!("write release: {e}"));
2128 }
2129 json_response(
2130 201,
2131 &serde_json::json!({
2132 "name": name,
2133 "version": version,
2134 "head_op": head_op,
2135 "branch": branch,
2136 }),
2137 )
2138}
2139
2140fn public_pkg_names(root: &std::path::Path) -> Vec<String> {
2143 list_pkg_names(root)
2144 .into_iter()
2145 .filter(|name| pkg_is_public(root, name))
2146 .collect()
2147}
2148
2149fn public_pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
2153 let packages: Vec<serde_json::Value> = public_pkg_names(&state.root)
2154 .iter()
2155 .filter_map(|name| {
2156 let r = load_latest_pkg_record(&state.root, name)?;
2157 Some(serde_json::json!({
2158 "name": r.name,
2159 "version": r.version,
2160 "head_op": r.head_op,
2161 "published_at": r.published_at,
2162 }))
2163 })
2164 .collect();
2165 json_response(200, &serde_json::json!({ "packages": packages }))
2166}
2167
2168#[derive(Debug, PartialEq, Eq)]
2173enum PublicTarget {
2174 List,
2175 Latest(String),
2176 Versions(String),
2177 ApiDiff(String),
2178 Head(String),
2179 Version(String, String),
2180 Archive(String, String),
2181}
2182
2183impl PublicTarget {
2184 fn pkg_name(&self) -> Option<&str> {
2186 match self {
2187 PublicTarget::List => None,
2188 PublicTarget::Latest(n)
2189 | PublicTarget::Versions(n)
2190 | PublicTarget::ApiDiff(n)
2191 | PublicTarget::Head(n)
2192 | PublicTarget::Version(n, _)
2193 | PublicTarget::Archive(n, _) => Some(n),
2194 }
2195 }
2196}
2197
2198fn resolve_public(method: &Method, path: &str) -> Result<PublicTarget, u16> {
2203 if !matches!(method, Method::Get) {
2204 return Err(405);
2205 }
2206 let rest = path.trim_matches('/');
2207 if rest.is_empty() {
2208 return Ok(PublicTarget::List);
2209 }
2210 let segs: Vec<&str> = rest.split('/').collect();
2211 if !segs.iter().all(|s| valid_pkg_segment(s)) {
2212 return Err(404);
2213 }
2214 match segs.as_slice() {
2215 [n] => Ok(PublicTarget::Latest(n.to_string())),
2216 [n, "versions"] => Ok(PublicTarget::Versions(n.to_string())),
2217 [n, "api-diff"] => Ok(PublicTarget::ApiDiff(n.to_string())),
2218 [n, "head"] => Ok(PublicTarget::Head(n.to_string())),
2219 [n, v, "archive"] => Ok(PublicTarget::Archive(n.to_string(), v.to_string())),
2220 [n, v] => Ok(PublicTarget::Version(n.to_string(), v.to_string())),
2221 _ => Err(404),
2222 }
2223}
2224
2225pub fn route_public(
2236 state: &State,
2237 method: &Method,
2238 path: &str,
2239 query: &str,
2240) -> Response<std::io::Cursor<Vec<u8>>> {
2241 let target = match resolve_public(method, path) {
2242 Ok(t) => t,
2243 Err(405) => return error_response(405, "public read is GET-only"),
2244 Err(_) => return error_response(404, "not found"),
2245 };
2246 if let PublicTarget::List = target {
2248 return public_pkg_list_handler(state);
2249 }
2250 if let Some(name) = target.pkg_name() {
2252 if !pkg_is_public(&state.root, name) {
2253 return error_response(404, format!("package {name:?} not found"));
2254 }
2255 }
2256 match target {
2257 PublicTarget::List => unreachable!("handled above"),
2258 PublicTarget::Latest(n) => pkg_get_handler(state, &n),
2259 PublicTarget::Versions(n) => pkg_versions_handler(state, &n),
2260 PublicTarget::ApiDiff(n) => pkg_api_diff_handler(state, &n, query),
2261 PublicTarget::Head(n) => pkg_head_handler(state, &n),
2262 PublicTarget::Version(n, v) => pkg_get_version_handler(state, &n, &v),
2263 PublicTarget::Archive(n, v) => pkg_archive_handler(state, &n, &v),
2264 }
2265}
2266
2267fn save_pkg_record(
2268 root: &std::path::Path,
2269 record: &PkgRecord,
2270 archive: Option<&[u8]>,
2273) -> std::io::Result<()> {
2274 let dir = pkg_name_dir(root, &record.name);
2275 std::fs::create_dir_all(&dir)?;
2276
2277 let rec_bytes = serde_json::to_vec_pretty(record).unwrap_or_default();
2279 std::fs::write(pkg_version_path(root, &record.name, &record.version), rec_bytes)?;
2280
2281 if let Some(archive) = archive {
2283 std::fs::write(pkg_archive_path(root, &record.name, &record.version), archive)?;
2284 }
2285
2286 let mut index = load_pkg_index(root, &record.name).unwrap_or_default();
2288 index.latest = Some(record.version.clone());
2289 if !index.versions.iter().any(|v| v.version == record.version) {
2290 index.versions.push(PkgVersionSummary {
2291 version: record.version.clone(),
2292 head_op: record.head_op.clone(),
2293 published_at: record.published_at,
2294 });
2295 }
2296 let idx_bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
2297 std::fs::write(pkg_index_path(root, &record.name), idx_bytes)
2298}
2299
2300fn list_pkg_names(root: &std::path::Path) -> Vec<String> {
2301 let dir = root.join("packages");
2302 let Ok(entries) = std::fs::read_dir(&dir) else {
2303 return Vec::new();
2304 };
2305 let mut names: Vec<String> = entries
2306 .filter_map(|e| e.ok())
2307 .filter(|e| e.path().is_dir())
2308 .filter_map(|e| e.file_name().into_string().ok())
2309 .collect();
2310 names.sort();
2311 names
2312}
2313
2314fn collect_lex_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
2315 let Ok(entries) = std::fs::read_dir(dir) else { return };
2316 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
2317 entries.sort_by_key(|e| e.path());
2318 for entry in entries {
2319 let path = entry.path();
2320 if path.is_dir() {
2321 collect_lex_files(&path, out);
2322 } else if path.extension().and_then(|x| x.to_str()) == Some("lex") {
2323 out.push(path);
2324 }
2325 }
2326}
2327
2328fn pkg_publish_handler(state: &State, body: &[u8]) -> Response<std::io::Cursor<Vec<u8>>> {
2331 let tmp = match tempfile::TempDir::new() {
2332 Ok(t) => t,
2333 Err(e) => return error_response(500, format!("create temp dir: {e}")),
2334 };
2335 {
2336 let gz = flate2::read::GzDecoder::new(std::io::Cursor::new(body));
2337 let mut ar = tar::Archive::new(gz);
2338 if let Err(e) = ar.unpack(tmp.path()) {
2339 return error_response(400, format!("unpack archive: {e}"));
2340 }
2341 }
2342
2343 let toml_path = tmp.path().join("lex.toml");
2344 if !toml_path.exists() {
2345 return error_response(400, "archive must contain lex.toml at root");
2346 }
2347 let manifest = match Manifest::load(&toml_path) {
2348 Ok(m) => m,
2349 Err(e) => return error_response(400, format!("lex.toml: {e}")),
2350 };
2351 let (pkg_name, pkg_version) = match &manifest.package {
2352 Some(m) => (m.name.clone(), m.version.clone()),
2353 None => return error_response(400, "lex.toml must have a [package] section"),
2354 };
2355
2356 if load_pkg_record(&state.root, &pkg_name, &pkg_version).is_some() {
2360 return error_response(
2361 409,
2362 format!(
2363 "package {pkg_name}@{pkg_version} already published; \
2364 bump the version in lex.toml to publish a new release"
2365 ),
2366 );
2367 }
2368
2369 let src_dir = tmp.path().join("src");
2370 if !src_dir.exists() {
2371 return error_response(400, "archive must contain a src/ directory");
2372 }
2373 let mut lex_files: Vec<PathBuf> = Vec::new();
2374 collect_lex_files(&src_dir, &mut lex_files);
2375 if lex_files.is_empty() {
2376 return error_response(400, "no .lex files found in src/");
2377 }
2378
2379 let store = state.store.lock().unwrap();
2380 let branch = store.current_branch();
2381
2382 let old_head = match store.branch_head(&branch) {
2402 Ok(h) => h,
2403 Err(e) => return error_response(500, format!("branch_head: {e}")),
2404 };
2405 let old_pairs: Vec<(String, String)> =
2412 old_head.iter().map(|(sig, stage)| (sig.clone(), stage.clone())).collect();
2413 let mut old_fns_by_name: BTreeMap<String, Vec<lex_ast::FnDecl>> = BTreeMap::new();
2414 for fd in store.get_asts_for_sigs_bulk(&old_pairs)
2415 .into_iter()
2416 .filter_map(|r| r.ok())
2417 .filter_map(|s| match s { lex_ast::Stage::FnDecl(fd) => Some(fd), _ => None })
2418 {
2419 old_fns_by_name.entry(fd.name.clone()).or_default().push(fd);
2420 }
2421 let mut old_types_by_name: BTreeMap<String, lex_ast::TypeDecl> = BTreeMap::new();
2424 for td in store.get_asts_for_sigs_bulk(&old_pairs)
2425 .into_iter()
2426 .filter_map(|r| r.ok())
2427 .filter_map(|s| match s { lex_ast::Stage::TypeDecl(td) => Some(td), _ => None })
2428 {
2429 old_types_by_name.insert(td.name.clone(), td);
2430 }
2431 fn structural_key(fd: &lex_ast::FnDecl) -> Option<String> {
2439 let mut anon = fd.clone();
2440 anon.name = String::new();
2441 lex_ast::sig_id(&lex_ast::Stage::FnDecl(anon))
2442 }
2443
2444 fn take_matching(
2454 map: &mut BTreeMap<String, Vec<lex_ast::FnDecl>>,
2455 name: &str,
2456 new_fd: &lex_ast::FnDecl,
2457 ) -> Option<lex_ast::FnDecl> {
2458 let candidates = map.get_mut(name)?;
2459 let idx = match candidates.len() {
2460 0 => return None,
2461 1 => 0,
2462 _ => {
2463 let want = structural_key(new_fd);
2464 candidates.iter().position(|c| structural_key(c) == want)?
2465 }
2466 };
2467 let matched = candidates.remove(idx);
2468 if candidates.is_empty() {
2469 map.remove(name);
2470 }
2471 Some(matched)
2472 }
2473
2474 let loaded = match load_package(&lex_files, tmp.path(), &pkg_name, true) {
2498 Ok(p) => p,
2499 Err(e) => return error_response(400, format!("load package: {e}")),
2500 };
2501 let mut stages = canonicalize_program(&loaded.program);
2502 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
2508 return error_with_detail(
2509 422,
2510 format!("type errors in package {pkg_name}"),
2511 serde_json::to_value(&errs).unwrap(),
2512 );
2513 }
2514 let new_fns = stage_fns(&stages);
2515 let all_function_names: Vec<String> = new_fns.keys().cloned().collect();
2516
2517 let mut old_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2522 for (name, new_fd) in &new_fns {
2523 if let Some(fd) = take_matching(&mut old_fns_by_name, name, new_fd) {
2524 old_fns.insert(name.clone(), fd);
2525 }
2526 }
2527 let new_types = stage_types(&stages);
2528 let old_types: BTreeMap<String, lex_ast::TypeDecl> = new_types
2532 .keys()
2533 .filter_map(|n| old_types_by_name.get(n).map(|td| (n.clone(), td.clone())))
2534 .collect();
2535 let report =
2536 lex_vcs::compute_diff_with_types(&old_fns, &new_fns, &old_types, &new_types, false);
2537
2538 let mut new_imports = lex_vcs::ImportMap::new();
2546 for (file, modules) in &loaded.imports_by_file {
2547 let entry = new_imports.entry(file.clone()).or_default();
2548 for (reference, alias) in modules {
2549 entry.insert(lex_vcs::ImportRef {
2550 reference: reference.clone(),
2551 alias: alias.clone(),
2552 });
2553 }
2554 }
2555
2556 let outcome = match store.publish_program_with_intent(
2559 &branch,
2560 &stages,
2561 &report,
2562 &new_imports,
2563 false,
2564 None,
2565 None,
2566 &loaded.module_prefixes,
2567 ) {
2568 Ok(outcome) => outcome,
2569 Err(lex_store::StoreError::TypeError(errs)) => {
2570 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
2571 }
2572 Err(e) => return write_error_response("publish_program", e),
2573 };
2574 let all_ops: Vec<serde_json::Value> = match serde_json::to_value(&outcome.ops) {
2575 Ok(serde_json::Value::Array(arr)) => arr,
2576 _ => Vec::new(),
2577 };
2578 let final_head_op = outcome.head_op;
2579
2580 let now = SystemTime::now()
2605 .duration_since(UNIX_EPOCH)
2606 .map(|d| d.as_secs())
2607 .unwrap_or(0);
2608 let dependencies: Vec<String> = final_head_op
2611 .as_ref()
2612 .and_then(|h| lex_store::api::external_dependencies_at_op(&store, h).ok())
2613 .unwrap_or_default();
2614 let record = PkgRecord {
2615 name: pkg_name.clone(),
2616 version: pkg_version,
2617 head_op: final_head_op.clone(),
2618 published_at: now,
2619 function_names: all_function_names,
2620 dependencies,
2621 dependency_specs: Default::default(),
2625 ops: all_ops.clone(),
2626 };
2627 if let Err(e) = save_pkg_record(&state.root, &record, Some(body)) {
2628 return error_response(500, format!("save package index: {e}"));
2629 }
2630
2631 json_response(200, &serde_json::json!({
2632 "package": pkg_name,
2633 "ops": all_ops,
2634 "head_op": final_head_op,
2635 }))
2636}
2637
2638fn pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
2640 let names = list_pkg_names(&state.root);
2641 let packages: Vec<serde_json::Value> = names.iter()
2642 .filter_map(|name| {
2643 let idx = load_pkg_index(&state.root, name)?;
2644 let latest = idx.latest.as_deref()?;
2645 let r = load_pkg_record(&state.root, name, latest)?;
2646 Some(serde_json::json!({
2647 "name": r.name,
2648 "version": r.version,
2649 "head_op": r.head_op,
2650 "published_at": r.published_at,
2651 }))
2652 })
2653 .collect();
2654 json_response(200, &serde_json::json!({ "packages": packages }))
2655}
2656
2657fn pkg_get_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2659 match load_latest_pkg_record(&state.root, name) {
2660 Some(r) => json_response(200, &serde_json::json!({
2661 "name": r.name,
2662 "version": r.version,
2663 "head_op": r.head_op,
2664 "published_at": r.published_at,
2665 "function_names": r.function_names,
2666 "ops": r.ops,
2667 })),
2668 None => error_response(404, format!("package {name:?} not found")),
2669 }
2670}
2671
2672fn pkg_versions_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2674 match load_pkg_index(&state.root, name) {
2675 Some(idx) => json_response(200, &serde_json::json!({
2676 "name": name,
2677 "latest": idx.latest,
2678 "versions": idx.versions,
2679 })),
2680 None => error_response(404, format!("package {name:?} not found")),
2681 }
2682}
2683
2684fn pkg_api_diff_handler(state: &State, name: &str, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2689 let mut from: Option<String> = None;
2690 let mut to: Option<String> = None;
2691 for kv in query.split('&') {
2692 match kv.split_once('=') {
2693 Some(("from", v)) => from = Some(v.to_string()),
2694 Some(("to", v)) => to = Some(v.to_string()),
2695 _ => {}
2696 }
2697 }
2698 let (Some(from), Some(to)) = (from, to) else {
2699 return error_response(400, "api-diff requires ?from=<version>&to=<version>");
2700 };
2701 let head_of = |v: &str| load_pkg_record(&state.root, name, v).and_then(|r| r.head_op);
2702 let (Some(from_head), Some(to_head)) = (head_of(&from), head_of(&to)) else {
2703 return error_response(404, format!("{name}: unknown release in {from}..{to}"));
2704 };
2705
2706 let store = state.store.lock().unwrap();
2707 let (prev_api, new_api) = match (
2708 lex_store::api::public_api_at_op(&store, &from_head),
2709 lex_store::api::public_api_at_op(&store, &to_head),
2710 ) {
2711 (Ok(a), Ok(b)) => (a, b),
2712 _ => return error_response(500, "could not read package APIs for the given releases"),
2713 };
2714 let (change, detail) = match lex_store::api::classify_api_change(&prev_api, &new_api) {
2715 lex_store::api::ApiChange::Breaking(d) => ("breaking", d),
2716 lex_store::api::ApiChange::Additive(d) => ("additive", d),
2717 lex_store::api::ApiChange::None => ("none", String::new()),
2718 };
2719 let renames = lex_store::api::detect_renames(&prev_api, &new_api);
2720 json_response(200, &serde_json::json!({
2721 "name": name, "from": from, "to": to,
2722 "change": change, "detail": detail, "renames": renames,
2723 }))
2724}
2725
2726fn pkg_get_version_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2728 match load_pkg_record(&state.root, name, version) {
2729 Some(r) => json_response(200, &serde_json::json!({
2730 "name": r.name,
2731 "version": r.version,
2732 "head_op": r.head_op,
2733 "published_at": r.published_at,
2734 "function_names": r.function_names,
2735 "dependencies": r.dependencies,
2736 "ops": r.ops,
2737 })),
2738 None => error_response(404, format!("package {name:?}@{version:?} not found")),
2739 }
2740}
2741
2742fn pkg_archive_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2744 let gzip = |bytes: Vec<u8>| {
2745 Response::from_data(bytes).with_status_code(200).with_header(
2746 tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"application/gzip"[..]).unwrap(),
2747 )
2748 };
2749
2750 if let Ok(bytes) = std::fs::read(pkg_archive_path(&state.root, name, version)) {
2752 return gzip(bytes);
2753 }
2754
2755 if let Some(record) = load_pkg_record(&state.root, name, version) {
2759 if let Some(head_op) = record.head_op.clone() {
2760 match render_op_log_archive(state, name, version, &head_op, &record.dependency_specs) {
2761 Ok(bytes) => return gzip(bytes),
2762 Err(e) => {
2763 return error_response(500, format!("rendering archive for {name:?}@{version:?}: {e}"));
2764 }
2765 }
2766 }
2767 }
2768
2769 error_response(404, format!("archive for {name:?}@{version:?} not found"))
2770}
2771
2772const DEFAULT_ARCHIVE_MODULE: &str = "src/lib.lex";
2777
2778fn render_op_log_archive(
2783 state: &State,
2784 name: &str,
2785 version: &str,
2786 head_op: &str,
2787 dependency_specs: &std::collections::BTreeMap<String, DepSpec>,
2788) -> Result<Vec<u8>, String> {
2789 let (files, lock): (Vec<(String, String)>, Option<String>) = {
2795 let store = state.store.lock().unwrap();
2796 let lock = store.committed_lock_inherited(head_op).ok().flatten();
2802 let head = lex_store::render::package_head_at_op(&store, head_op)
2803 .map_err(|e| format!("reading head {head_op}: {e}"))?;
2804 let files = match lex_store::render::render_source(&store, &head)
2805 .map_err(|e| format!("rendering source at {head_op}: {e}"))?
2806 {
2807 lex_store::render::RenderedSource::Single { path, src } => {
2808 vec![(path.unwrap_or_else(|| DEFAULT_ARCHIVE_MODULE.to_string()), src)]
2811 }
2812 lex_store::render::RenderedSource::Multi(tree) => tree.into_iter().collect(),
2813 };
2814 (files, lock)
2815 };
2816
2817 let mut manifest = format!("[package]\nname = \"{name}\"\nversion = \"{version}\"\n");
2822 let dep_lines: Vec<String> = dependency_specs
2823 .iter()
2824 .filter_map(|(dep_name, spec)| spec.to_toml_inline().map(|inline| format!("{dep_name} = {inline}")))
2825 .collect();
2826 if !dep_lines.is_empty() {
2827 manifest.push_str("\n[dependencies]\n");
2828 for line in dep_lines {
2829 manifest.push_str(&line);
2830 manifest.push('\n');
2831 }
2832 }
2833 let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
2834 {
2835 let mut ar = tar::Builder::new(&mut enc);
2836 let mut append = |p: &str, data: &[u8]| -> std::io::Result<()> {
2837 let mut h = tar::Header::new_gnu();
2838 h.set_size(data.len() as u64);
2839 h.set_mode(0o644);
2840 h.set_cksum();
2841 ar.append_data(&mut h, p, data)
2842 };
2843 append("lex.toml", manifest.as_bytes()).map_err(|e| e.to_string())?;
2844 if let Some(lock) = &lock {
2845 append("lex.lock", lock.as_bytes()).map_err(|e| e.to_string())?;
2846 }
2847 for (path, src) in &files {
2848 append(path, src.as_bytes()).map_err(|e| e.to_string())?;
2849 }
2850 ar.finish().map_err(|e| e.to_string())?;
2851 }
2852 enc.finish().map_err(|e| e.to_string())
2853}
2854
2855fn pkg_head_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2857 match load_latest_pkg_record(&state.root, name) {
2858 Some(r) => json_response(200, &serde_json::json!({
2859 "name": r.name,
2860 "version": r.version,
2861 "head_op": r.head_op,
2862 })),
2863 None => error_response(404, format!("package {name:?} not found")),
2864 }
2865}
2866
2867fn pkg_delete_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2869 let record = match load_latest_pkg_record(&state.root, name) {
2870 Some(r) => r,
2871 None => return error_response(404, format!("package {name:?} not found")),
2872 };
2873
2874 let store = state.store.lock().unwrap();
2875 let branch = store.current_branch();
2876
2877 let head = match store.branch_head(&branch) {
2878 Ok(h) => h,
2879 Err(e) => return error_response(500, format!("branch_head: {e}")),
2880 };
2881
2882 let head_pairs: Vec<(String, String)> = head
2889 .iter()
2890 .map(|(sig, stage)| (sig.clone(), stage.clone()))
2891 .collect();
2892 let old_fns: BTreeMap<String, lex_ast::FnDecl> = store
2893 .get_asts_for_sigs_bulk(&head_pairs)
2894 .into_iter()
2895 .filter_map(|r| r.ok())
2896 .filter_map(|s| match s {
2897 lex_ast::Stage::FnDecl(fd)
2898 if record.function_names.contains(&fd.name) => Some((fd.name.clone(), fd)),
2899 _ => None,
2900 })
2901 .collect();
2902
2903 let new_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2904 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
2905 let empty_imports = lex_vcs::ImportMap::new();
2906
2907 match store.publish_program(&branch, &[], &report, &empty_imports, false) {
2908 Ok(outcome) => {
2909 let ver = record.version.clone();
2911 let _ = std::fs::remove_file(pkg_version_path(&state.root, name, &ver));
2912 let _ = std::fs::remove_file(pkg_archive_path(&state.root, name, &ver));
2913 if let Some(mut idx) = load_pkg_index(&state.root, name) {
2915 idx.versions.retain(|v| v.version != ver);
2916 idx.latest = idx.versions.last().map(|v| v.version.clone());
2917 if idx.versions.is_empty() {
2918 let _ = std::fs::remove_dir_all(pkg_name_dir(&state.root, name));
2919 } else {
2920 let bytes = serde_json::to_vec_pretty(&idx).unwrap_or_default();
2921 let _ = std::fs::write(pkg_index_path(&state.root, name), bytes);
2922 }
2923 }
2924 json_response(200, &serde_json::json!({
2925 "deleted": name,
2926 "version": ver,
2927 "ops": outcome.ops,
2928 "head_op": outcome.head_op,
2929 }))
2930 }
2931 Err(lex_store::StoreError::TypeError(errs)) => {
2932 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
2933 }
2934 Err(e) => write_error_response("retract package", e),
2935 }
2936}
2937
2938#[cfg(test)]
2939mod dep_spec_tests {
2940 use super::DepSpec;
2941
2942 #[test]
2943 fn a_dual_spec_renders_both_git_and_vcs_refs_vcs_first() {
2944 let spec = DepSpec {
2945 registry: Some("vcs.lexlang.org/lex-official/lex-schema".into()),
2946 version: Some("^0.9".into()),
2947 git: Some("https://github.com/alpibrusl/lex-schema".into()),
2948 ..Default::default()
2949 };
2950 assert_eq!(
2951 spec.to_toml_inline().as_deref(),
2952 Some("{ registry = \"vcs.lexlang.org/lex-official/lex-schema\", version = \"^0.9\", git = \"https://github.com/alpibrusl/lex-schema\" }"),
2953 );
2954 }
2955
2956 #[test]
2957 fn bare_git_and_bare_registry_specs_render_their_own_keys() {
2958 let git = DepSpec { git: Some("https://x/g".into()), tag: Some("v1".into()), ..Default::default() };
2959 assert_eq!(git.to_toml_inline().as_deref(), Some("{ git = \"https://x/g\", tag = \"v1\" }"));
2960 let reg = DepSpec { registry: Some("vcs/r".into()), version: Some("1.0.0".into()), ..Default::default() };
2961 assert_eq!(reg.to_toml_inline().as_deref(), Some("{ registry = \"vcs/r\", version = \"1.0.0\" }"));
2962 }
2963
2964 #[test]
2965 fn an_empty_spec_renders_nothing() {
2966 assert_eq!(DepSpec::default().to_toml_inline(), None);
2967 }
2968}
2969
2970#[cfg(test)]
2971mod policy_ceiling_tests {
2972 use super::*;
2973 use lex_runtime::Policy;
2974 use std::path::PathBuf;
2975
2976 fn permissive_request() -> Policy {
2980 Policy {
2981 allow_effects: ["io", "fs_read", "fs_write", "net", "proc"]
2982 .iter()
2983 .map(|s| s.to_string())
2984 .collect(),
2985 allow_fs_read: vec![PathBuf::from("/")],
2986 allow_fs_write: vec![PathBuf::from("/")],
2987 allow_net_host: Vec::new(),
2988 allow_proc: Vec::new(),
2989 allow_approval: Vec::new(),
2990 budget: None,
2991 }
2992 }
2993
2994 #[test]
2995 fn ceiling_drops_effects_the_caller_was_not_granted() {
2996 let ceiling = Policy {
2997 allow_effects: ["io", "time"].iter().map(|s| s.to_string()).collect(),
2998 ..Policy::default()
2999 };
3000 let got = clamp_policy(permissive_request(), &ceiling);
3001 assert!(got.allow_effects.contains("io"));
3002 assert!(!got.allow_effects.contains("proc"), "proc must not survive a ceiling without it");
3003 assert!(!got.allow_effects.contains("fs_write"));
3004 assert!(!got.allow_effects.contains("net"));
3005 assert!(!got.allow_effects.contains("time"));
3007 }
3008
3009 #[test]
3010 fn ceiling_scopes_override_caller_scopes() {
3011 let ceiling = Policy {
3012 allow_effects: ["fs_read"].iter().map(|s| s.to_string()).collect(),
3013 allow_fs_read: vec![PathBuf::from("/srv/tenant")],
3014 ..Policy::default()
3015 };
3016 let got = clamp_policy(permissive_request(), &ceiling);
3017 assert_eq!(got.allow_fs_read, vec![PathBuf::from("/srv/tenant")]);
3020 assert!(got.allow_fs_write.is_empty());
3021 assert!(got.allow_proc.is_empty());
3022 assert!(got.allow_net_host.is_empty());
3023 }
3024
3025 #[test]
3026 fn ceiling_caps_budget_and_prefers_the_smaller() {
3027 let mut req = permissive_request();
3029 req.budget = None;
3030 let ceiling = Policy { budget: Some(1_000), ..Policy::default() };
3031 assert_eq!(clamp_policy(req, &ceiling).budget, Some(1_000));
3032
3033 let mut req2 = permissive_request();
3035 req2.budget = Some(50);
3036 let ceiling2 = Policy { budget: Some(1_000), ..Policy::default() };
3037 assert_eq!(clamp_policy(req2, &ceiling2).budget, Some(50));
3038 }
3039
3040 #[test]
3041 fn empty_ceiling_is_pure_only() {
3042 let got = clamp_policy(permissive_request(), &Policy::default());
3043 assert!(got.allow_effects.is_empty(), "an empty ceiling grants nothing");
3044 assert!(got.allow_proc.is_empty());
3045 assert!(got.allow_fs_write.is_empty());
3046 }
3047}
3048
3049#[cfg(test)]
3050mod public_read_tests {
3051 use super::*;
3052
3053 fn seed_pkg(root: &std::path::Path, name: &str, version: &str) {
3056 let record = PkgRecord {
3057 name: name.to_string(),
3058 version: version.to_string(),
3059 head_op: Some(format!("op-{name}")),
3060 published_at: 1,
3061 function_names: vec![format!("{name}.f")],
3062 dependencies: vec![],
3063 dependency_specs: Default::default(),
3064 ops: vec![],
3065 };
3066 save_pkg_record(root, &record, Some(format!("ARCHIVE:{name}@{version}").as_bytes()))
3067 .expect("seed package");
3068 }
3069
3070 #[test]
3071 fn new_package_defaults_to_private() {
3072 let tmp = tempfile::TempDir::new().unwrap();
3073 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
3074 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
3075 assert!(!pkg_is_public(tmp.path(), "does-not-exist"));
3077 }
3078
3079 #[test]
3080 fn set_visibility_round_trips_and_index_persists() {
3081 let tmp = tempfile::TempDir::new().unwrap();
3082 let state = State::open(tmp.path().to_path_buf()).unwrap();
3083 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
3084
3085 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"public"}"#);
3086 assert!(pkg_is_public(tmp.path(), "lex-schema"));
3087 let idx = load_pkg_index(tmp.path(), "lex-schema").unwrap();
3089 assert_eq!(idx.latest.as_deref(), Some("0.9.2"));
3090 assert_eq!(idx.versions.len(), 1);
3091
3092 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"private"}"#);
3093 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
3094 }
3095
3096 #[test]
3097 fn set_visibility_on_unknown_package_is_a_noop() {
3098 let tmp = tempfile::TempDir::new().unwrap();
3099 let state = State::open(tmp.path().to_path_buf()).unwrap();
3100 let _ = pkg_set_visibility_handler(&state, "ghost", r#"{"visibility":"public"}"#);
3102 assert!(load_pkg_index(tmp.path(), "ghost").is_none());
3103 }
3104
3105 #[test]
3106 fn public_listing_omits_private_packages() {
3107 let tmp = tempfile::TempDir::new().unwrap();
3108 let state = State::open(tmp.path().to_path_buf()).unwrap();
3109 seed_pkg(tmp.path(), "pub-pkg", "1.0.0");
3110 seed_pkg(tmp.path(), "priv-pkg", "1.0.0");
3111 let _ = pkg_set_visibility_handler(&state, "pub-pkg", r#"{"visibility":"public"}"#);
3112
3113 let names = public_pkg_names(tmp.path());
3114 assert_eq!(names, vec!["pub-pkg".to_string()]);
3115 }
3116
3117 #[test]
3118 fn resolve_public_maps_routes() {
3119 let get = Method::Get;
3120 assert_eq!(resolve_public(&get, "").unwrap(), PublicTarget::List);
3121 assert_eq!(resolve_public(&get, "/").unwrap(), PublicTarget::List);
3122 assert_eq!(
3123 resolve_public(&get, "/lex-schema").unwrap(),
3124 PublicTarget::Latest("lex-schema".into())
3125 );
3126 assert_eq!(
3127 resolve_public(&get, "/lex-schema/versions").unwrap(),
3128 PublicTarget::Versions("lex-schema".into())
3129 );
3130 assert_eq!(
3131 resolve_public(&get, "/lex-schema/head").unwrap(),
3132 PublicTarget::Head("lex-schema".into())
3133 );
3134 assert_eq!(
3135 resolve_public(&get, "/lex-schema/0.9.2").unwrap(),
3136 PublicTarget::Version("lex-schema".into(), "0.9.2".into())
3137 );
3138 assert_eq!(
3139 resolve_public(&get, "/lex-schema/0.9.2/archive").unwrap(),
3140 PublicTarget::Archive("lex-schema".into(), "0.9.2".into())
3141 );
3142 }
3143
3144 #[test]
3145 fn resolve_public_rejects_bad_method_and_traversal() {
3146 assert_eq!(resolve_public(&Method::Put, "/lex-schema"), Err(405));
3148 assert_eq!(resolve_public(&Method::Post, "").err(), Some(405));
3149 assert_eq!(resolve_public(&Method::Get, "/.."), Err(404));
3151 assert_eq!(resolve_public(&Method::Get, "/lex-schema/../etc"), Err(404));
3152 assert_eq!(resolve_public(&Method::Get, "/a/b/c/d"), Err(404));
3153 assert!(resolve_public(&Method::Get, "/lex schema").is_err());
3155 }
3156}