P4.4: bearer auth + write endpoints on distd

distd stops being read-only. http.sx learns the write-side request
surface: header capture with case-insensitive lookup, a Content-Length-
bounded body read loop (8K header cap, 512 MiB body cap -> 413, 411 for
length-less POST/PUT), and the matching status texts.

Auth (server/auth.sx): Authorization: Bearer is re-hashed and resolved
via find_token_by_hash, then gated through check_token — 401 for
missing/malformed/unknown credentials, 403 with a refusal-specific code
(auth.revoked/expired/missing_scope/app_forbidden/channel_forbidden);
successful auth stamps last_used_at. check_token's app gate now treats
an empty REQUEST app like an empty request channel (uploads are
app-agnostic until a release references them).

Write routes (POST, publish scope): /api/upload content-addresses the
body; /api/apps/<slug>/releases publishes over already-uploaded objects
through commit_publish — the back half extracted from run_publish so
CLI and HTTP publishes share one find/create-app -> transaction ->
audit -> persist pipeline; channels/<name>/promote|rollback delegate to
the P3.5 CLI pipelines. Reads stay public.

make test 17/17 (new: server_write.sx pinned acceptance over curl).
This commit is contained in:
agra
2026-06-12 11:18:31 +03:00
parent d8b7a7bfb3
commit e2a5150542
8 changed files with 1135 additions and 126 deletions

View File

@@ -118,6 +118,12 @@ check_empty_request_channel :: () -> bool {
return refusal(scoped_token(), "publish", "acme-app", "", 500) == "";
}
// An empty REQUEST app passes the app gate likewise (an upload is
// app-agnostic until a release references it).
check_empty_request_app :: () -> bool {
return refusal(scoped_token(), "publish", "", "", 500) == "";
}
// An unscoped token (empty app_slug/channel) authorizes any app/channel.
check_unscoped_matches_all :: () -> bool {
t := scoped_token();
@@ -276,6 +282,7 @@ main :: () -> i32 {
failures += run_case("check: app scope mismatch refused", check_app_mismatch());
failures += run_case("check: channel scope mismatch refused", check_channel_mismatch());
failures += run_case("check: empty request channel passes", check_empty_request_channel());
failures += run_case("check: empty request app passes", check_empty_request_app());
failures += run_case("check: unscoped token matches all", check_unscoped_matches_all());
failures += run_case("validate: accepts good tokens", check_validate_accepts());
failures += run_case("validate: rejects each bad form", check_validate_rejects());