Files
distribution/src/server/distd.sx
agra 48a13c43ee distd serves through std.http — the readiness loop lands (PLAN-HTTPZ A1)
The hand-rolled sequential accept loop, its SO_RCVTIMEO band-aid, and
the whole src/server/http.sx module are retired: distd is now a
std.http handler. Server.init gets the store directory through the ctx
word; route() fills a Response instead of writing to a socket; every
handler ports mechanically (respond_error/load_or_503/respond_render
take *Response; bodies allocate from the per-request arena, never the
stack, since serialization happens after the handler returns).
Downloads keep X-Checksum-SHA256 via extra_headers; auth takes the
extracted Authorization value; the 411 contract (POST/PUT must declare
Content-Length) moves into the handler, pinned as before.

Config: 512 MiB read cap (whole-body artifact uploads), 120s request
deadline, 5s keepalive, 200 requests per connection. Idle connections
now cost nothing — timeouts evict, never block.

http_client gains its own 10s read timeout (the old shared helper's
secs->ms change had silently shrunk it to 10ms).

tests/server_http.sx pins the architecture: a request answers within
1s while SIX idle preconnects are held open (the retired loop paid
250ms-2s per idle socket serially), and two requests ride one
keep-alive connection. make test 24/24 green.
2026-06-12 22:00:31 +03:00

1060 lines
41 KiB
Plaintext

// =====================================================================
// 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/<slug> {"app":..,"releases":[..],"channels":[..]}
// GET /download/<sha256> the object's bytes (application/octet-stream,
// X-Checksum-SHA256 header)
// GET /install/<slug>/<channel> install page (UA-aware,
// iOS actions honest per the app's IosMode)
// GET /install/<slug>/<channel>/manifest.plist enterprise OTA manifest
// (404 unless ios_mode is enterprise)
//
// Writes require `Authorization: Bearer <token>` 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/<slug>/releases JSON body {version,
// channel, artifacts:[{platform, sha256, ...}]} over ALREADY-
// uploaded objects -> the same commit pipeline as `dist ci publish`
// POST /api/apps/<slug>/channels/<name>/promote {"release_id":..}
// POST /api/apps/<slug>/channels/<name>/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 = "&amp;"; } // &
if c == 60 { rep = "&lt;"; } // <
if c == 62 { rep = "&gt;"; } // >
if c == 34 { rep = "&quot;"; } // "
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 :: "<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>dist</title><style>body{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#101216;color:#d6dae1;margin:2rem auto;max-width:60rem;padding:0 1rem}h1{font-size:1.1rem;letter-spacing:.04em}h2{font-size:1rem;margin-top:2rem;border-bottom:1px solid #2a2f3a;padding-bottom:.3rem}table{border-collapse:collapse;width:100%;font-size:.85rem}td,th{text-align:left;padding:.3rem .8rem .3rem 0;border-bottom:1px solid #1d212a;vertical-align:top}th{color:#8b93a3;font-weight:normal}a{color:#7ab7ff;text-decoration:none}a:hover{text-decoration:underline}small,.dim{color:#737b8c}</style></head><body><h1>dist — release console</h1>";
INDEX_FOOT :: "<p class=\"dim\"><small><a href=\"/admin\">admin console</a> &middot; API: <a href=\"/api/apps\">/api/apps</a> &middot; <a href=\"/healthz\">/healthz</a></small></p></body></html>";
// 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, "<p class=\"dim\">nothing published yet</p>");
}
a := 0;
while a < repo.apps.len {
app := repo.apps.items[a];
page = concat(page, concat("<h2>", concat(html_escape(app.slug), concat(" <small>", concat(html_escape(app.display_name), "</small></h2>")))));
// Channels: name (linked to the install page) -> current release.
page = concat(page, "<table><tr><th>channel</th><th>current release</th></tr>");
c := 0;
while c < repo.channels.len {
ch := repo.channels.items[c];
if ch.app_id == app.id {
link := concat("<a href=\"/install/", concat(html_escape(app.slug), concat("/", concat(html_escape(ch.name), concat("\">", concat(html_escape(ch.name), "</a>"))))));
page = concat(page, concat("<tr><td>", concat(link, concat("</td><td>", concat(html_escape(ch.current_release_id), "</td></tr>")))));
}
c += 1;
}
page = concat(page, "</table>");
// Releases with their artifacts as download links.
page = concat(page, "<table><tr><th>release</th><th>platform</th><th>artifact</th><th>size</th><th>sha256</th></tr>");
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("<tr><td>", concat(html_escape(rel.version), concat(" <small>", concat(html_escape(rel.id), "</small></td><td>"))));
row = concat(row, concat(db.platform_str(art.platform), "</td><td>"));
row = concat(row, concat("<a href=\"/download/", concat(art.sha256, concat("\">", concat(html_escape(art.filename), "</a></td><td>")))));
row = concat(row, concat(int_to_string(art.size_bytes), "</td><td><small>"));
row = concat(row, concat(short_sha(art.sha256), "&hellip;</small></td></tr>"));
page = concat(page, row);
}
k += 1;
}
}
r += 1;
}
page = concat(page, "</table>");
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("<p class=\"artifact\"><a href=\"/download/", concat(art.sha256, "\">"));
row = concat(row, concat(html_escape(art.filename), "</a>"));
if label.len > 0 { row = concat(row, concat(" <small>", concat(label, "</small>"))); }
row = concat(row, concat(" <small>", concat(int_to_string(art.size_bytes), " bytes</small>")));
row = concat(row, concat("<br><small class=\"sha\">sha256 ", concat(art.sha256, "</small></p>")));
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("<p><a class=\"action\" href=\"", concat(html_escape(app.testflight_url), "\">Open in TestFlight</a></p>"));
s = concat(s, "<p><small>Installation runs through Apple&#39;s TestFlight flow.</small></p>");
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("<p><a class=\"action\" href=\"", concat(html_escape(itms), "\">Install on enrolled device</a></p>"));
s = concat(s, "<p><small>Over-the-air enterprise install; requires MDM enrollment and HTTPS.</small></p>");
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, "<p><small>IPA artifact download only &mdash; it cannot be installed on an iPhone or iPad from this page.</small></p>");
}
INSTALL_HEAD :: "<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>install</title><style>body{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#101216;color:#d6dae1;margin:2rem auto;max-width:40rem;padding:0 1rem}h1{font-size:1.1rem}h2{font-size:1rem;margin-top:1.6rem;border-bottom:1px solid #2a2f3a;padding-bottom:.3rem}a{color:#7ab7ff;text-decoration:none}a:hover{text-decoration:underline}a.action{display:inline-block;border:1px solid #7ab7ff;border-radius:4px;padding:.4rem .8rem;margin:.2rem 0}small,.dim{color:#737b8c}.sha{word-break:break-all}.detected h2{color:#9fe09f}</style></head><body>";
// 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("<h1>", concat(html_escape(app.display_name), concat(" <small>", concat(html_escape(rel.version), concat(" &middot; ", concat(html_escape(ctx.channel_name), "</small></h1>")))))));
// 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, "<section class=\"detected\">"); }
else { page = concat(page, "<section>"); }
page = concat(page, concat("<h2>", platform_label(p)));
if marked { page = concat(page, " <small>your device</small>"); }
page = concat(page, "</h2>");
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, "</section>");
}
ai += 1;
}
k += 1;
}
page = concat(page, "<p class=\"dim\"><small><a href=\"/\">&larr; all apps</a></small></p>");
return concat(page, "</body></html>");
}
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 := "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\"><dict><key>items</key><array><dict>";
x = concat(x, "<key>assets</key><array><dict><key>kind</key><string>software-package</string><key>url</key><string>");
x = concat(x, html_escape(url));
x = concat(x, "</string></dict></array><key>metadata</key><dict><key>bundle-identifier</key><string>");
x = concat(x, html_escape(bid));
x = concat(x, "</string><key>bundle-version</key><string>");
x = concat(x, html_escape(ctx.release.version));
x = concat(x, "</string><key>kind</key><string>software</string><key>title</key><string>");
x = concat(x, html_escape(ctx.app.display_name));
x = concat(x, "</string></dict></dict></array></dict></plist>\n");
resp.content_type = "application/xml";
resp.body = x;
}
// GET /install/<slug>/<channel>[/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/<slug>/<channel>");
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/<slug>/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/<slug>/channels/<name>/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/<slug>/channels/<name>/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;
}