-module(http_server). -export([route/1, route/2, ok_response/1, not_found_response/0, welcome_body/0, capabilities_body/0, capabilities_path/0, match_prefix/2, actors_prefix/0, actor_doc_response/1, artifacts_prefix/0, artifact_response/1, projections_list_path/0, projections_prefix/0, projections_list_response/0, projection_response/1, activity_path/0, unauthorized_response/0, post_activity_response/0]). %% HTTP request router per design §16.1. %% %% Request shape (mirrors what the SX-side `http-listen` builds and %% the http:listen/2 BIF bridge marshals into a proplist): %% [{method, Binary}, {path, Binary}, {query, Binary}, %% {headers, [{Name, Value}, ...]}, {body, Binary}] %% %% Response shape: %% [{status, Integer}, {headers, [{Name, Value}, ...]}, {body, Binary}] %% %% Real dispatch (actor docs, outbox listings, /activity POST, %% /.well-known/sx-capabilities, etc.) lands in Step 8c+. Step 8b %% wires the route/1 shape and a single hello-world handler that %% proves the request→response round-trip. %% %% Method/path comparison uses integer-segment binaries because %% `<<"GET">>` truncates to a single byte in this port. route(Req) -> route(Req, []). %% route/2 — Cfg proplist carries optional `:publish_token` (binary) %% for POST /activity auth. Other state (logs, projections, etc.) is %% not yet threaded through — POST /activity returns a stub 200 %% once auth succeeds; real outbox:publish glue lands separately. route(Req, Cfg) -> M = field(method, Req), P = field(path, Req), case {M, P} of {<<80,79,83,84>>, <<47,97,99,116,105,118,105,116,121>>} -> handle_post_activity(Req, Cfg); _ -> dispatch(M, P) end. %% 71 69 84 = "GET" | 47 = "/" dispatch(<<71, 69, 84>>, <<47>>) -> ok_response(welcome_body()); %% GET /.well-known/sx-capabilities dispatch(<<71, 69, 84>>, <<47,46,119,101,108,108,45,107,110,111,119,110, 47,115,120,45,99,97,112,97,98,105,108,105,116,105,101,115>>) -> ok_response(capabilities_body()); %% GET /projections — list stub. Comes before the /projections/{name} %% prefix clause because the bare path has no trailing slash. dispatch(<<71, 69, 84>>, <<47,112,114,111,106,101,99,116,105,111,110,115>>) -> projections_list_response(); %% GET /actors/{id} or /artifacts/{cid} or /projections/{name} dispatch(<<71, 69, 84>>, Path) -> case match_prefix(actors_prefix(), Path) of {ok, Id} when byte_size(Id) > 0 -> actor_doc_response(Id); _ -> case match_prefix(artifacts_prefix(), Path) of {ok, Cid} when byte_size(Cid) > 0 -> artifact_response(Cid); _ -> case match_prefix(projections_prefix(), Path) of {ok, Name} when byte_size(Name) > 0 -> projection_response(Name); _ -> not_found_response() end end end; dispatch(_, _) -> not_found_response(). %% "fed-sx kernel m1\n" — 17 bytes, hand-spelled. %% f e d - s x _ k e r n e l _ m 1 \n welcome_body() -> <<102,101,100,45,115,120,32,107,101,114,110,101,108,32,109,49,10>>. %% "/.well-known/sx-capabilities" — exposed for callers that build %% requests in tests or that need the canonical path string. capabilities_path() -> <<47,46,119,101,108,108,45,107,110,111,119,110, 47,115,120,45,99,97,112,97,98,105,108,105,116,105,101,115>>. %% Capability descriptor body. Returned as plain text per design %% §16; future content-negotiation work (Step 8d) layers JSON / %% dag-cbor / SX representations on top. %% %% Lines (each terminated by \n = 10): %% "kernel: fed-sx-m1\n" %% "version: 0.0.1\n" %% "verbs: Create Update Delete\n" capabilities_body() -> <<107,101,114,110,101,108,58,32,102,101,100,45,115,120,45,109,49,10, 118,101,114,115,105,111,110,58,32,48,46,48,46,49,10, 118,101,114,98,115,58,32,67,114,101,97,116,101,32,85,112,100,97,116,101,32,68,101,108,101,116,101,10>>. ok_response(Body) -> [{status, 200}, {headers, []}, {body, Body}]. not_found_response() -> [{status, 404}, {headers, []}, {body, <<110,111,116,32,102,111,117,110,100,10>>}]. % "not found\n" %% Internal property-list field lookup. Returns nil when missing %% so the route falls into the not_found arm gracefully. field(K, [{K, V} | _]) -> V; field(K, [_ | Rest]) -> field(K, Rest); field(_, []) -> nil. %% ── Dynamic-segment routing ───────────────────────────────────── %% %% match_prefix(Prefix, Path) — if Path starts with the entire %% Prefix binary, return {ok, Rest} where Rest is the remaining %% bytes; else return nomatch. Pure byte-level pattern match, %% no regex / no parsing. Path-segment splitting comes in later %% sub-deliverables (8c-art, 8c-proj) where it's needed. match_prefix(<<>>, Rest) -> {ok, Rest}; match_prefix(<>, <>) -> match_prefix(PRest, PathRest); match_prefix(_, _) -> nomatch. %% "/actors/" — 8 bytes: 47 97 99 116 111 114 115 47 actors_prefix() -> <<47,97,99,116,111,114,115,47>>. %% Actor doc stub. Real implementation (Step 8c continuation) will %% fetch the actor-state projection entry and serialise it; v1 %% returns the id as the body so route resolution can be exercised %% end-to-end without the projection wiring. actor_doc_response(Id) -> %% "actor: " — 7 bytes Pre = <<97,99,116,111,114,58,32>>, Body = <
>,
    ok_response(Body).

%% "/artifacts/" — 11 bytes
artifacts_prefix() ->
    <<47,97,114,116,105,102,97,99,116,115,47>>.

%% Artifact stub. Real implementation will fetch the bytes from
%% the registry (or a CID-keyed store) and content-negotiate.
%% v1 echoes the CID so route resolution can be tested.
artifact_response(Cid) ->
    %% "artifact: " — 10 bytes
    Pre = <<97,114,116,105,102,97,99,116,58,32>>,
    Body = <
>,
    ok_response(Body).

%% "/projections" — 12 bytes (no trailing slash; the list endpoint)
projections_list_path() ->
    <<47,112,114,111,106,101,99,116,105,111,110,115>>.

%% "/projections/" — 13 bytes (the per-projection prefix)
projections_prefix() ->
    <<47,112,114,111,106,101,99,116,105,111,110,115,47>>.

%% Stub list response — real implementation queries the registry
%% for active projections and serialises the name+CID list.
projections_list_response() ->
    %% "projections: (empty)\n" — hand-spelled
    Body = <<112,114,111,106,101,99,116,105,111,110,115,58,32,
             40,101,109,112,116,121,41,10>>,
    ok_response(Body).

projection_response(Name) ->
    %% "projection: " — 12 bytes
    Pre = <<112,114,111,106,101,99,116,105,111,110,58,32>>,
    Body = <
>,
    ok_response(Body).

%% "/activity" — 9 bytes
activity_path() ->
    <<47,97,99,116,105,118,105,116,121>>.

%% 401 Unauthorized response. Body: "unauthorized\n" = 13 bytes.
unauthorized_response() ->
    [{status, 401}, {headers, []},
     {body, <<117,110,97,117,116,104,111,114,105,122,101,100,10>>}].

%% Stub success body for POST /activity. Real impl will return
%% the published activity's CID once outbox:publish is wired
%% through a server-state context (Step 8c-post-publish).
post_activity_response() ->
    %% "published (stub)\n" — hand-spelled
    Body = <<112,117,98,108,105,115,104,101,100,32,
             40,115,116,117,98,41,10>>,
    ok_response(Body).

%% Auth helpers.

handle_post_activity(Req, Cfg) ->
    case check_bearer(Req, Cfg) of
        ok ->
            post_activity_response();
        {error, _} ->
            unauthorized_response()
    end.

check_bearer(Req, Cfg) ->
    case bearer_token(Req) of
        {ok, Got} ->
            case expected_token(Cfg) of
                {ok, Want} when Got =:= Want -> ok;
                _ -> {error, bad_token}
            end;
        not_found -> {error, no_auth}
    end.

%% Look up the Authorization header, strip "Bearer ", return token.
bearer_token(Req) ->
    case field(headers, Req) of
        nil -> not_found;
        Hs ->
            %% "authorization" — 13 bytes, lowercase as the BIF wrapper
            %% normalises headers to lowercase keys.
            AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>,
            case find_header(AuthKey, Hs) of
                not_found -> not_found;
                {ok, V} -> strip_bearer(V)
            end
    end.

find_header(_, []) -> not_found;
find_header(K, [{K, V} | _]) -> {ok, V};
find_header(K, [_ | Rest]) -> find_header(K, Rest).

%% "Bearer " — 7 bytes — strip and return the rest as the token.
%% Anything else returns not_found (treated as missing auth).
strip_bearer(V) ->
    Prefix = <<66,101,97,114,101,114,32>>,
    case match_prefix(Prefix, V) of
        {ok, Token} when byte_size(Token) > 0 -> {ok, Token};
        _ -> not_found
    end.

expected_token(Cfg) ->
    case field(publish_token, Cfg) of
        nil -> not_found;
        T -> {ok, T}
    end.