// ===================================================================== // distd.sx — the distribution server over the local store (subplan 04, // Slices 1-4), run as `dist server run`. // // Serves the state the CLI publishes — the store database's metadata and // the content-addressed objects — over HTTP (src/server/http.sx). Reads // are public: // // GET / HTML index: apps, channels, releases, links // GET /healthz {"status":"ok"} — no store access // GET /api/apps {"apps":[...]} every app in the store // GET /api/apps/ {"app":..,"releases":[..],"channels":[..]} // GET /download/ the object's bytes (application/octet-stream, // X-Checksum-SHA256 header) // GET /install// install page (UA-aware, // iOS actions honest per the app's IosMode) // GET /install///manifest.plist enterprise OTA manifest // (404 unless ios_mode is enterprise) // // Writes require `Authorization: Bearer ` with the `publish` scope // (src/server/auth.sx; tokens are minted with `dist token create`): // // POST /api/upload raw bytes -> content- // addressed object; responds {"status":"stored","sha256":..} // POST /api/apps//releases JSON body {version, // channel, artifacts:[{platform, sha256, ...}]} over ALREADY- // uploaded objects -> the same commit pipeline as `dist ci publish` // POST /api/apps//channels//promote {"release_id":..} // POST /api/apps//channels//rollback (empty body) // // The channel operations delegate to the P3.5 CLI pipelines and the // release POST commits through publish.sx's `commit_publish`, so HTTP and // CLI semantics cannot drift. // // Anything else is a JSON error in the CLI's error shape // (`{"status":"error","error":{code,message}}`) with the matching HTTP // status — the API and the CLI report failures identically. // // FRESHNESS: the store database is RELOADED on every /api request, so a // `dist ci publish` / `release promote` between requests is visible // immediately — the store on disk stays the single source of truth (no // cache to invalidate, LAN-scale traffic). // // RESPONSE BUFFERS are heap slices from the per-request arena, never big // stack arrays: a stack array of 64K+ in one frame crashes the sx LLVM // backend (DAGCombiner segfault). Small fixed buffers (4K) are fine. // ===================================================================== #import "modules/std.sx"; #import "modules/std/json.sx"; #import "modules/std/fs.sx"; #import "../domain/platform.sx"; #import "../domain/app.sx"; #import "../domain/release.sx"; #import "../domain/artifact.sx"; #import "../domain/channel.sx"; #import "../domain/token.sx"; #import "../domain/audit.sx"; #import "../domain/validate.sx"; #import "../repo/repo.sx"; #import "../store/store.sx"; #import "../validation/artifact_file.sx"; #import "../publish/publish.sx"; sock :: #import "modules/std/socket.sx"; au :: #import "auth.sx"; db :: #import "../repo/db.sx"; jout :: #import "../json_out.sx"; // Also reached through an alias so publish helpers read as `pl.…` at the // call sites that mirror dist.sx's. pl :: #import "../publish/publish.sx"; ops :: #import "../release/ops.sx"; adm :: #import "admin.sx"; // Aliased so the json reader is called as `jsrv.parse` — a bare `parse` // would bind to `std.cli`'s once both modules share the `dist` program. jsrv :: #import "modules/std/json.sx"; // Response-body capacity for the /api JSON renders (heap, per-request). RENDER_CAP :: 262144; ServeErr :: error { Bind, } // Server-side stderr log line (fd 2 via the socket module's write; stdout // stays clean for whoever launched the process). slog :: (s: string) { if s.len > 0 { sock.write(2, s.ptr, xx s.len); } } // True iff `s` is exactly 64 lowercase-hex chars (a storage key). is_hex64 :: (s: string) -> bool { if s.len != 64 { return false; } i := 0; while i < s.len { c := s[i]; digit := c >= 48 and c <= 57; // '0'..'9' lower := c >= 97 and c <= 102; // 'a'..'f' if !digit and !lower { return false; } i += 1; } return true; } starts_with :: (s: string, prefix: string) -> bool { if prefix.len > s.len { return false; } i := 0; while i < prefix.len { if s[i] != prefix[i] { return false; } i += 1; } return true; } // The path remainder after `prefix` (caller has checked starts_with). tail_after :: (s: string, prefix: string) -> string { return string.{ ptr = @s[prefix.len], len = s.len - prefix.len }; } // A `RENDER_CAP` heap slice from the per-request context allocator. render_buf :: () -> string { raw : [*]u8 = xx context.allocator.alloc_bytes(RENDER_CAP); return string.{ ptr = raw, len = RENDER_CAP }; } // ── responses ───────────────────────────────────────────────────────── // JSON error body in the CLI's error shape, sent with the HTTP status. respond_error :: (resp: *http.Response, code: i64, fail_code: string, fail_message: string) { f : jout.CliFailure = .{ code = fail_code, message = fail_message }; // The body must outlive this frame (the server serializes after the // handler returns), so the render buffer comes from the per-request // arena, never the stack. raw : [*]u8 = xx context.allocator.alloc_bytes(4096); werr := false; n := jout.write_error(f, string.{ ptr = raw, len = 4096 }) catch { werr = true; 0 }; body := "{\"status\":\"error\"}"; if !werr { body = string.{ ptr = raw, len = n }; } resp.status = code; resp.content_type = "application/json"; resp.body = body; } // ── /api renders (builders own the `try`, callers catch) ───────────── // Reload the persisted model. Null means the store has no readable // database — the 503 error response has already been sent. load_or_503 :: (resp: *http.Response, store_dir: string) -> ?Repo { if !db.store_exists(store_dir) { respond_error(resp, 503, "store.load", concat("no store database (nothing published yet): ", store_dir)); return null; } loaded, le := db.load(store_dir); if le { respond_error(resp, 503, "store.load", concat("the store database could not be loaded: ", store_dir)); return null; } return loaded; } // `{"apps":[...]}` into `dst`, returning the bytes written. render_apps_json :: (repo: *Repo, dst: []u8) -> (i64, !JsonError) { alloc := context.allocator; root : Object = .{}; arr : Array = .{}; i := 0; while i < repo.apps.len { arr.add(db.app_to_json(repo.apps.items[i], alloc), alloc); i += 1; } root.put("apps", .array(arr), alloc); rootv : Value = .object(root); n := try write_to_buffer(rootv, dst); return n; } // `{"app":..,"releases":[..],"channels":[..]}` for `app` into `dst`. render_app_detail_json :: (repo: *Repo, app: App, dst: []u8) -> (i64, !JsonError) { alloc := context.allocator; root : Object = .{}; root.put("app", db.app_to_json(app, alloc), alloc); rels : Array = .{}; i := 0; while i < repo.releases.len { r := repo.releases.items[i]; if r.app_id == app.id { rels.add(db.release_to_json(r, alloc), alloc); } i += 1; } root.put("releases", .array(rels), alloc); chans : Array = .{}; i = 0; while i < repo.channels.len { c := repo.channels.items[i]; if c.app_id == app.id { chans.add(db.channel_to_json(c, alloc), alloc); } i += 1; } root.put("channels", .array(chans), alloc); rootv : Value = .object(root); n := try write_to_buffer(rootv, dst); return n; } // Send `n` rendered bytes of `buf` as 200 JSON — or the overflow error // when the render didn't fit RENDER_CAP. respond_render :: (resp: *http.Response, buf: string, n: i64, overflowed: bool) { if overflowed { respond_error(resp, 500, "http.response_overflow", "response exceeded the server's render buffer"); return; } resp.status = 200; resp.content_type = "application/json"; resp.body = string.{ ptr = buf.ptr, len = n }; } // ── HTML index (the install-page seed, subplan 04 Slice 5) ─────────── // Escape &, <, >, " for HTML text/attribute positions. Returns `s` // unchanged (no copy) when nothing needs escaping. html_escape :: (s: string) -> string { acc := ""; start := 0; i := 0; while i < s.len { c := s[i]; rep := ""; if c == 38 { rep = "&"; } // & if c == 60 { rep = "<"; } // < if c == 62 { rep = ">"; } // > if c == 34 { rep = """; } // " if rep.len > 0 { if i > start { acc = concat(acc, string.{ ptr = @s[start], len = i - start }); } acc = concat(acc, rep); start = i + 1; } i += 1; } if start == 0 { return s; } if start < s.len { acc = concat(acc, string.{ ptr = @s[start], len = s.len - start }); } return acc; } // First 12 hex chars of a digest, for display next to the full-link. short_sha :: (sha: string) -> string { if sha.len < 12 { return sha; } return string.{ ptr = sha.ptr, len = 12 }; } INDEX_HEAD :: "dist

dist — release console

"; INDEX_FOOT :: "

admin console · API: /api/apps · /healthz

"; // Server-rendered index: every app with its channels and its releases' // artifacts as direct /download links. Dense and read-only, per the // product shape — the full admin UI is subplan 06. render_index :: (repo: *Repo) -> string { page := INDEX_HEAD; if repo.apps.len == 0 { page = concat(page, "

nothing published yet

"); } a := 0; while a < repo.apps.len { app := repo.apps.items[a]; page = concat(page, concat("

", concat(html_escape(app.slug), concat(" ", concat(html_escape(app.display_name), "

"))))); // Channels: name (linked to the install page) -> current release. page = concat(page, ""); c := 0; while c < repo.channels.len { ch := repo.channels.items[c]; if ch.app_id == app.id { link := concat("", concat(html_escape(ch.name), "")))))); page = concat(page, concat(""))))); } c += 1; } page = concat(page, "
channelcurrent release
", concat(link, concat("", concat(html_escape(ch.current_release_id), "
"); // Releases with their artifacts as download links. page = concat(page, ""); r := 0; while r < repo.releases.len { rel := repo.releases.items[r]; if rel.app_id == app.id { k := 0; while k < repo.artifacts.len { art := repo.artifacts.items[k]; if art.release_id == rel.id { row := concat("")); page = concat(page, row); } k += 1; } } r += 1; } page = concat(page, "
releaseplatformartifactsizesha256
", concat(html_escape(rel.version), concat(" ", concat(html_escape(rel.id), "")))); row = concat(row, concat(db.platform_str(art.platform), "")); row = concat(row, concat("", concat(html_escape(art.filename), ""))))); row = concat(row, concat(int_to_string(art.size_bytes), "")); row = concat(row, concat(short_sha(art.sha256), "…
"); a += 1; } return concat(page, INDEX_FOOT); } handle_index :: (resp: *http.Response, store_dir: string) { rq := load_or_503(resp, store_dir); if rq == null { return; } repo := rq!; resp.content_type = "text/html; charset=utf-8"; resp.body = render_index(@repo); } // ── routes ──────────────────────────────────────────────────────────── handle_apps_index :: (resp: *http.Response, store_dir: string) { rq := load_or_503(resp, store_dir); if rq == null { return; } repo := rq!; buf := render_buf(); werr := false; n := render_apps_json(@repo, buf) catch { werr = true; 0 }; respond_render(resp, buf, n, werr); } handle_app_detail :: (resp: *http.Response, store_dir: string, slug: string) { rq := load_or_503(resp, store_dir); if rq == null { return; } repo := rq!; aq := repo.find_app_by_slug(slug); if aq == null { respond_error(resp, 404, "api.unknown_app", concat("no app with that slug in the store: ", slug)); return; } buf := render_buf(); werr := false; n := render_app_detail_json(@repo, aq!, buf) catch { werr = true; 0 }; respond_render(resp, buf, n, werr); } handle_download :: (resp: *http.Response, store_dir: string, sha: string) { if !is_hex64(sha) { respond_error(resp, 404, "download.bad_key", "download key must be a 64-char lowercase-hex sha256"); return; } opath := path_join(store_dir, concat("objects/", sha)); bq := read_file(opath); if bq == null { respond_error(resp, 404, "download.unknown_object", concat("no object with that digest in the store: ", sha)); return; } resp.content_type = "application/octet-stream"; resp.extra_headers = concat("X-Checksum-SHA256: ", concat(sha, "\r\n")); resp.body = bq!; } // ── install pages (subplan 04 Slice 5) ──────────────────────────────── // True iff `needle` occurs in `hay` (UA sniffing; both are short). contains :: (hay: string, needle: string) -> bool { if needle.len == 0 { return true; } if needle.len > hay.len { return false; } i := 0; while i + needle.len <= hay.len { j := 0; ok := true; while j < needle.len { if hay[i + j] != needle[j] { ok = false; break; } j += 1; } if ok { return true; } i += 1; } return false; } // The requesting device's platform, sniffed from the User-Agent. Order // matters: iPhone UAs contain "like Mac OS X" and Android UAs contain // "Linux", so the mobile checks run first. Null = no idea. ua_platform :: (ua: string) -> ?Platform { if contains(ua, "iPhone") or contains(ua, "iPad") or contains(ua, "iPod") { return .ios; } if contains(ua, "Android") { return .android_apk; } if contains(ua, "Macintosh") { return .macos; } if contains(ua, "Windows") { return .windows; } if contains(ua, "Linux") { return .linux; } return null; } platform_label :: (p: Platform) -> string { if p == .ios { return "iOS"; } if p == .android_apk { return "Android"; } if p == .macos { return "macOS"; } if p == .linux { return "Linux"; } return "Windows"; } // The release the (app, channel) pair currently serves, resolving each // step or answering the precise 404. Null = response already sent. // `host` (the request's Host header) feeds the absolute URLs the // enterprise install flow needs. InstallCtx :: struct { app: App; channel_name: string; release: Release; host: string = "localhost"; } resolve_install :: (resp: *http.Response, repo: *Repo, slug: string, chan_name: string) -> ?InstallCtx { aq := repo.find_app_by_slug(slug); if aq == null { respond_error(resp, 404, "install.unknown_app", concat("no app with that slug in the store: ", slug)); return null; } app := aq!; cq := repo.get_channel(app.id, chan_name); if cq == null { respond_error(resp, 404, "install.unknown_channel", concat("the app has no channel with that name: ", chan_name)); return null; } rq := repo.get_release(cq!.current_release_id); if rq == null { respond_error(resp, 404, "install.no_release", concat("the channel does not point at a published release: ", chan_name)); return null; } return InstallCtx.{ app = app, channel_name = chan_name, release = rq! }; } // One artifact row: filename (download link), size, full sha256 — the // digest is part of the contract, not decoration. install_artifact_row :: (art: Artifact, label: string) -> string { row := concat("

")); row = concat(row, concat(html_escape(art.filename), "")); if label.len > 0 { row = concat(row, concat(" ", concat(label, ""))); } row = concat(row, concat(" ", concat(int_to_string(art.size_bytes), " bytes"))); row = concat(row, concat("
sha256 ", concat(art.sha256, "

"))); return row; } // The iOS section body for the app's install mode — the honest version // of "install on iPhone" (see IosMode in the domain). render_ios_actions :: (ctx: *InstallCtx, art: Artifact) -> string { app := ctx.app; if app.ios_mode == .testflight { s := concat("

Open in TestFlight

")); s = concat(s, "

Installation runs through Apple's TestFlight flow.

"); return concat(s, install_artifact_row(art, "(IPA artifact)")); } if app.ios_mode == .enterprise { plist := concat("https://", concat(ctx.host, concat("/install/", concat(app.slug, concat("/", concat(ctx.channel_name, "/manifest.plist")))))); itms := concat("itms-services://?action=download-manifest&url=", plist); s := concat("

Install on enrolled device

")); s = concat(s, "

Over-the-air enterprise install; requires MDM enrollment and HTTPS.

"); return concat(s, install_artifact_row(art, "(IPA artifact)")); } // artifact_only — a download, never an install action s := install_artifact_row(art, ""); return concat(s, "

IPA artifact download only — it cannot be installed on an iPhone or iPad from this page.

"); } INSTALL_HEAD :: "install"; // The install page: the channel's current release, one section per // platform artifact, the requester's platform first and marked. render_install :: (ctx: *InstallCtx, repo: *Repo, detected: ?Platform) -> string { app := ctx.app; rel := ctx.release; page := INSTALL_HEAD; page = concat(page, concat("

", concat(html_escape(app.display_name), concat(" ", concat(html_escape(rel.version), concat(" · ", concat(html_escape(ctx.channel_name), "

"))))))); // platform sections in fixed order — except the detected platform, // which moves to the front order : [5]Platform = .[ .ios, .android_apk, .macos, .linux, .windows ]; if detected != null { d := detected!; order[0] = d; slot := 1; j := 0; while j < 5 { cand : Platform = .ios; if j == 1 { cand = .android_apk; } if j == 2 { cand = .macos; } if j == 3 { cand = .linux; } if j == 4 { cand = .windows; } if cand != d { order[slot] = cand; slot += 1; } j += 1; } } k := 0; while k < 5 { p := order[k]; // the release's artifact for this platform, if any ai := 0; while ai < repo.artifacts.len { art := repo.artifacts.items[ai]; if art.release_id == rel.id and art.platform == p { marked := detected != null and k == 0; if marked { page = concat(page, "
"); } else { page = concat(page, "
"); } page = concat(page, concat("

", platform_label(p))); if marked { page = concat(page, " your device"); } page = concat(page, "

"); if p == .ios { page = concat(page, render_ios_actions(ctx, art)); } else { label := ""; if p == .android_apk { label = "(APK)"; } page = concat(page, install_artifact_row(art, label)); } page = concat(page, "
"); } ai += 1; } k += 1; } page = concat(page, "

← all apps

"); return concat(page, ""); } handle_install_page :: (resp: *http.Response, store_dir: string, req: *http.Request, slug: string, chan_name: string) { rq := load_or_503(resp, store_dir); if rq == null { return; } repo := rq!; ctxq := resolve_install(resp, @repo, slug, chan_name); if ctxq == null { return; } ctx := ctxq!; ua := http.find_header(req, "user-agent"); hostv := http.find_header(req, "host"); if hostv.len > 0 { ctx.host = hostv; } resp.content_type = "text/html; charset=utf-8"; resp.body = render_install(@ctx, @repo, ua_platform(ua)); } // The enterprise OTA manifest: Apple's plist shape, with the software // package URL pointing back at this server over HTTPS (itms-services // refuses plain http; TLS terminates at the reverse proxy). handle_manifest_plist :: (resp: *http.Response, store_dir: string, req: *http.Request, slug: string, chan_name: string) { rq := load_or_503(resp, store_dir); if rq == null { return; } repo := rq!; ctxq := resolve_install(resp, @repo, slug, chan_name); if ctxq == null { return; } ctx := ctxq!; if ctx.app.ios_mode != .enterprise { respond_error(resp, 404, "install.not_enterprise", "the app's iOS install mode is not enterprise; no OTA manifest exists"); return; } bid := bundle_id_for(ctx.app, .ios); if bid.len == 0 { respond_error(resp, 404, "install.no_bundle_id", "the app has no iOS bundle id; set one with: dist app set --ios-bundle-id"); return; } // the release's ios artifact found := false; art : Artifact = .{}; ai := 0; while ai < repo.artifacts.len { a := repo.artifacts.items[ai]; if a.release_id == ctx.release.id and a.platform == .ios { art = a; found = true; break; } ai += 1; } if !found { respond_error(resp, 404, "install.no_ios_artifact", "the channel's current release carries no iOS artifact"); return; } host := "localhost"; hostv2 := http.find_header(req, "host"); if hostv2.len > 0 { host = hostv2; } url := concat("https://", concat(host, concat("/download/", art.sha256))); x := "\n\nitems"; x = concat(x, "assetskindsoftware-packageurl"); x = concat(x, html_escape(url)); x = concat(x, "metadatabundle-identifier"); x = concat(x, html_escape(bid)); x = concat(x, "bundle-version"); x = concat(x, html_escape(ctx.release.version)); x = concat(x, "kindsoftwaretitle"); x = concat(x, html_escape(ctx.app.display_name)); x = concat(x, "\n"); resp.content_type = "application/xml"; resp.body = x; } // GET /install//[/manifest.plist] handle_install_route :: (resp: *http.Response, store_dir: string, req: *http.Request, tail: string) { s1 := seg_split(tail); if s1.head.len == 0 or s1.rest.len == 0 { respond_error(resp, 404, "http.not_found", "install pages live at /install//"); return; } s2 := seg_split(s1.rest); if s2.rest.len == 0 { handle_install_page(resp, store_dir, req, s1.head, s2.head); return; } if s2.rest == "manifest.plist" { handle_manifest_plist(resp, store_dir, req, s1.head, s2.head); return; } respond_error(resp, 404, "http.not_found", concat("no install route for ", tail)); } // ── write surface (POST, token-gated) ───────────────────────────────── // `s` split at its first '/': head before it, rest after it ("" when no // slash exists). Seg :: struct { head: string; rest: string; } seg_split :: (s: string) -> Seg { i := 0; while i < s.len { if s[i] == 47 { // '/' return Seg.{ head = string.{ ptr = s.ptr, len = i }, rest = string.{ ptr = @s[i + 1], len = s.len - i - 1 }, }; } i += 1; } return Seg.{ head = s, rest = "" }; } // Authenticate a write request or answer it. Null means the refusal // response has already been sent. auth_or_respond :: (resp: *http.Response, store_dir: string, req: *http.Request, app_slug: string, channel: string) -> ?Token { fail : jout.CliFailure = .{}; t, ae := au.authenticate(store_dir, http.find_header(req, "authorization"), "publish", app_slug, channel, @fail); if ae { status : i64 = 401; if ae == error.Forbidden { status = 403; } if ae == error.Unavailable { status = 503; } respond_error(resp, status, fail.code, fail.message); return null; } return t; } // JSON-object body, or null after answering 400. body_object :: (resp: *http.Response, body: string) -> ?Object { v, pe := jsrv.parse(body, context.allocator); if pe { respond_error(resp, 400, "api.bad_json", "request body is not valid JSON"); return null; } if v != .object { respond_error(resp, 400, "api.bad_json", "request body must be a JSON object"); return null; } return v.object; } wb_find :: (o: Object, key: string) -> ?Value { i := 0; while i < o.len { if o.items[i].key == key { return o.items[i].val; } i += 1; } return null; } // Required string member, or null after answering 400. wb_req_str :: (resp: *http.Response, o: Object, key: string) -> ?string { vq := wb_find(o, key); if vq != null { v := vq!; if v == .str { if v.str.len > 0 { return v.str; } } } respond_error(resp, 400, "api.missing_field", concat("body requires a non-empty string member: ", key)); return null; } wb_opt_str :: (o: Object, key: string) -> string { vq := wb_find(o, key); if vq == null { return ""; } v := vq!; if v != .str { return ""; } return v.str; } wb_opt_int :: (o: Object, key: string, default_: i64) -> i64 { vq := wb_find(o, key); if vq == null { return default_; } v := vq!; if v != .int_ { return default_; } return v.int_; } // POST /api/upload — content-address the raw body into the store. The // upload is app-agnostic (the bytes are inert until a release references // them), so auth demands only the publish scope. handle_upload :: (resp: *http.Response, store_dir: string, req: *http.Request) { if auth_or_respond(resp, store_dir, req, "", "") == null { return; } if req.body.len == 0 { respond_error(resp, 400, "upload.empty", "upload body is empty"); return; } st := Store.init(store_dir); werr := false; key := ""; k, se := st.put_bytes(req.body); if se { werr = true; } if !se { key = k; } if werr { respond_error(resp, 500, "store.write", "upload bytes could not be content-addressed into the store"); return; } resp.content_type = "application/json"; resp.body = concat("{\"status\":\"stored\",\"sha256\":\"", concat(key, concat("\",\"size_bytes\":", concat(int_to_string(req.body.len), "}")))); } // POST /api/apps//releases — publish a release whose artifacts name // already-uploaded objects by digest. Mirrors the CLI manifest checks // (platform, digest, size, content type, filename extension) against the // STORE rather than local files, then commits through the shared pipeline. handle_release_create :: (resp: *http.Response, store_dir: string, req: *http.Request, slug: string) { oq := body_object(resp, req.body); if oq == null { return; } o := oq!; versionq := wb_req_str(resp, o, "version"); if versionq == null { return; } version := versionq!; channelq := wb_req_str(resp, o, "channel"); if channelq == null { return; } channel := channelq!; tokq := auth_or_respond(resp, store_dir, req, slug, channel); if tokq == null { return; } tok := tokq!; artsq := wb_find(o, "artifacts"); arts_ok := false; arts_arr : Array = .{}; if artsq != null { av := artsq!; if av == .array { if av.array.len > 0 { arts_ok = true; arts_arr = av.array; } } } if !arts_ok { respond_error(resp, 400, "api.missing_field", "body requires a non-empty artifacts array"); return; } alloc := context.allocator; resolved : List(ResolvedArtifact) = .{}; i := 0; while i < arts_arr.len { if arts_arr.items[i] != .object { respond_error(resp, 400, "api.bad_json", "each artifact must be a JSON object"); return; } ao := arts_arr.items[i].object; pidq := wb_req_str(resp, ao, "platform"); if pidq == null { return; } platform, perr := parse_platform(pidq!); if perr { respond_error(resp, 400, "api.unknown_platform", concat("unknown platform id: ", pidq!)); return; } shaq := wb_req_str(resp, ao, "sha256"); if shaq == null { return; } sha := shaq!; if !is_hex64(sha) { respond_error(resp, 400, "api.bad_digest", "artifact sha256 must be a 64-char lowercase-hex digest"); return; } // The named object must already live in the store (uploaded via // /api/upload or an earlier publish). opath := path_join(store_dir, concat("objects/", sha)); ob := read_file(opath); if ob == null { respond_error(resp, 404, "api.unknown_object", concat("no object with that digest in the store (upload it first): ", sha)); return; } actual_size := ob!.len; declared := wb_opt_int(ao, "size_bytes", -1); if declared >= 0 and declared != actual_size { respond_error(resp, 400, "api.size_mismatch", concat("declared size_bytes does not match the stored object: ", sha)); return; } ct := wb_opt_str(ao, "content_type"); if ct.len == 0 { ct = pl.default_content_type(platform); } if !is_allowed_content_type(ct) { respond_error(resp, 400, "api.content_type_denied", concat("artifact content type is not on the allow-list: ", ct)); return; } fname := wb_opt_str(ao, "filename"); if fname.len == 0 { fname = concat(slug, concat("-", concat(version, concat(".", expected_ext(platform))))); } if !ext_matches_platform(fname, platform) { respond_error(resp, 400, "api.extension_mismatch", concat("artifact filename extension does not match its platform: ", fname)); return; } resolved.append(ResolvedArtifact.{ platform = platform, key = sha, size_bytes = actual_size, filename = fname, content_type = ct, metadata = wb_opt_str(ao, "metadata"), }, alloc); i += 1; } fail : jout.CliFailure = .{}; actor := concat("token:", tok.name); arts_view : []ResolvedArtifact = .{ ptr = resolved.items, len = resolved.len }; outcome, ce := pl.commit_publish(store_dir, slug, version, channel, actor, "/download/", arts_view, @fail); if ce { status : i64 = 500; if ce == error.Transaction { status = 400; if fail.code == "transaction.integrity" { status = 409; } } if ce == error.Persist { if fail.code == "persist.load" { status = 503; } } respond_error(resp, status, fail.code, fail.message); return; } if !ce { buf := render_buf(); werr := false; n := pl.write_json(@outcome, buf) catch { werr = true; 0 }; respond_render(resp, buf, n, werr); } } // HTTP status for a failed P3.5 channel operation. op_http_status :: (e: ops.OpError) -> i64 { if e == error.Load { return 503; } if e == error.NotFound { return 404; } if e == error.Invalid { return 409; } return 500; } // POST /api/apps//channels//promote — body {"release_id":..}; // delegates to the CLI's promote pipeline. handle_promote :: (resp: *http.Response, store_dir: string, req: *http.Request, slug: string, chan_name: string) { oq := body_object(resp, req.body); if oq == null { return; } relq := wb_req_str(resp, oq!, "release_id"); if relq == null { return; } if auth_or_respond(resp, store_dir, req, slug, chan_name) == null { return; } fail : jout.CliFailure = .{}; o, e := ops.run_promote(store_dir, slug, chan_name, relq!, @fail); if e { respond_error(resp, op_http_status(e), fail.code, fail.message); return; } if !e { buf := render_buf(); werr := false; n := ops.write_promote_json(@o, buf) catch { werr = true; 0 }; respond_render(resp, buf, n, werr); } } // POST /api/apps//channels//rollback — empty body; delegates // to the CLI's rollback pipeline. handle_rollback :: (resp: *http.Response, store_dir: string, req: *http.Request, slug: string, chan_name: string) { if auth_or_respond(resp, store_dir, req, slug, chan_name) == null { return; } fail : jout.CliFailure = .{}; o, e := ops.run_rollback(store_dir, slug, chan_name, @fail); if e { respond_error(resp, op_http_status(e), fail.code, fail.message); return; } if !e { buf := render_buf(); werr := false; n := ops.write_rollback_json(@o, buf) catch { werr = true; 0 }; respond_render(resp, buf, n, werr); } } // Route a write request under POST /api/. Returns false when no write // route matches (the caller 404s). route_post :: (resp: *http.Response, store_dir: string, req: *http.Request) -> bool { path := req.path; if path == "/api/upload" { handle_upload(resp, store_dir, req); return true; } if starts_with(path, "/api/apps/") { s1 := seg_split(tail_after(path, "/api/apps/")); if s1.head.len == 0 { return false; } if s1.rest == "releases" { handle_release_create(resp, store_dir, req, s1.head); return true; } if starts_with(s1.rest, "channels/") { s2 := seg_split(tail_after(s1.rest, "channels/")); if s2.head.len == 0 { return false; } if s2.rest == "promote" { handle_promote(resp, store_dir, req, s1.head, s2.head); return true; } if s2.rest == "rollback" { handle_rollback(resp, store_dir, req, s1.head, s2.head); return true; } } } return false; } // Route one parsed request into `resp`: GET reads, POST writes. The // outcome status lives in resp.status (respond_error sets it on every // refusal path). route :: (store_dir: string, req: *http.Request, resp: *http.Response) { method := req.method; path := req.path; if method == "GET" { if path == "/" { handle_index(resp, store_dir); return; } if path == "/healthz" { resp.content_type = "application/json"; resp.body = "{\"status\":\"ok\"}"; return; } if path == "/api/apps" { handle_apps_index(resp, store_dir); return; } if starts_with(path, "/api/apps/") { handle_app_detail(resp, store_dir, tail_after(path, "/api/apps/")); return; } if starts_with(path, "/download/") { handle_download(resp, store_dir, tail_after(path, "/download/")); return; } if starts_with(path, "/install/") { handle_install_route(resp, store_dir, req, tail_after(path, "/install/")); return; } if path == "/admin" or starts_with(path, "/admin/") { adm.handle_admin(store_dir, path, resp); return; } respond_error(resp, 404, "http.not_found", concat("no route for ", path)); return; } if method == "POST" { if route_post(resp, store_dir, req) { return; } respond_error(resp, 404, "http.not_found", concat("no write route for ", path)); return; } respond_error(resp, 405, "http.method_not_allowed", "distd routes are GET (reads) or POST (token-gated writes)"); } // ── server loop (std.http, PLAN-HTTPZ A1) ───────────────────────────── // The std.http handler: thread the store directory through the ctx // word, keep the 411 contract (POST/PUT must declare Content-Length — // std.http treats an absent header as a zero-length body), route, log. // Per-request allocations land in std.http's per-dispatch arena. DistdCtx :: struct { store_dir: string; } distd_handle :: (req: *http.Request, resp: *http.Response, ctx: usize) { dctx : *DistdCtx = xx ctx; store_dir := dctx.store_dir; refused := false; if req.method == "POST" or req.method == "PUT" { if http.find_header(req, "content-length").len == 0 { respond_error(resp, 411, "http.length_required", "POST/PUT requires a Content-Length header"); refused = true; } } if !refused { route(store_dir, req, resp); } line := concat("distd: ", concat(req.method, concat(" ", concat(req.path, concat(" -> ", concat(int_to_string(resp.status), "\n")))))); slog(line); } // Serve the store over std.http's readiness loop (PLAN-HTTPZ A1): idle // connections cost nothing, timeouts evict instead of blocking, // keep-alive holds between requests. Returns only when the socket // can't be opened. run_server :: (store_dir: string, port: i64) -> !ServeErr { dctx : DistdCtx = .{ store_dir = store_dir }; cfg : http.Config = .{ port = port, max_conn = 256, read_buf_cap = 536870912, // 512 MiB: artifact uploads arrive whole-body timeout_request_ms = 120000, // a large upload must complete within this timeout_keepalive_ms = 5000, request_count = 200, }; srv, se := http.Server.init(cfg, distd_handle, xx @dctx); if se { raise error.Bind; } slog(concat("distd: serving store ", concat(store_dir, concat(" on http://0.0.0.0:", concat(int_to_string(port), "\n"))))); srv.run(); return; }