fed-sx-m2: Step 4b — token -> ActorId map + 8 new tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 19s

POST /activity now routes through nx_kernel:publish_to/2 when the
bearer token resolves to an explicit ActorId via Cfg's :tokens
proplist:

  Cfg = [{tokens, [{<<"alice-token">>, alice},
                   {<<"bob-token">>,   bob}]}]

resolve_token/2 returns {ok, ActorId} on a :tokens hit. On a miss
it falls back to the M1 :publish_token single-token field — match
returns {ok, legacy}, routing through nx_kernel:publish/1 (which
fans out to bucket 0) so every M1 test continues to pass.

handle_post_activity threads the resolved ActorRef to
publish_if_kernel/3 which dispatches publish_to/2 for explicit
actor ids and publish/1 for the legacy atom. The no-kernel
auth-only path (which preserves the post_activity_response_for stub
for unit-style tests of http_server alone) is unchanged.

Dead expected_token/1 helper removed (was only called by the old
check_bearer arm that resolve_token replaces).

8 new cases in next/tests/http_multi_actor.sh (25/25 total):
  - two-actor Cfg, Alice token -> 200 with cid:
  - Alice token publishes to alice (log_tip alice=1, bob=0)
  - Bob token publishes to bob (log_tip alice=0, bob=1)
  - interleaved Alice + Bob + Alice -> {2, 1}
  - unknown token + no :publish_token -> 401
  - legacy :publish_token still works (M1 back-compat)
  - tokens map AND legacy :publish_token coexist (each resolves to
    its own actor; legacy lands on alice bucket via publish/1)
  - no kernel + valid :tokens entry -> auth-only stub 200

Conformance 761/761. 116/116 across 10 Step-4-adjacent suites
(http_multi_actor, http_route, http_publish, http_post_format,
http_marshal, http_publish_fold, http_listen_bif, http_server_start,
nx_kernel_multi, bootstrap_start, actor_lifecycle).
This commit is contained in:
2026-06-06 14:31:27 +00:00
parent 0b8772ec69
commit 271632c923
3 changed files with 128 additions and 23 deletions

View File

@@ -302,29 +302,38 @@ post_activity_response() ->
handle_post_activity(Req, Cfg) ->
case check_bearer(Req, Cfg) of
ok ->
{ok, ActorRef} ->
F = accept_format_from(Req),
publish_if_kernel(Req, F);
publish_if_kernel(Req, F, ActorRef);
{error, _} ->
unauthorized_response()
end.
%% publish_if_kernel/2 — if the nx_kernel gen_server is registered,
%% publish_if_kernel/3 — if the nx_kernel gen_server is registered,
%% delegate the publish there and translate the result. Otherwise
%% keep the stub response so the auth-only tests stay green without
%% having to spin up a kernel process. Format threads through to
%% both stub and CID responses so the Content-Type matches what
%% the client asked for via Accept.
publish_if_kernel(Req, F) ->
%%
%% ActorRef is either an explicit ActorId atom (Step 4b token map
%% resolution: route through nx_kernel:publish_to/2) or the atom
%% `legacy` from a single :publish_token Cfg back-compat (route
%% through nx_kernel:publish/1, which fans out to bucket 0).
publish_if_kernel(Req, F, ActorRef) ->
case erlang:whereis(nx_kernel) of
undefined ->
post_activity_response_for(F);
_Pid ->
Body = field(body, Req),
Request = [{type, create}, {object, Body}],
case nx_kernel:publish(Request) of
{ok, Result} ->
case envelope:get_field(cid, Result) of
Result = case ActorRef of
legacy -> nx_kernel:publish(Request);
_ -> nx_kernel:publish_to(ActorRef, Request)
end,
case Result of
{ok, R} ->
case envelope:get_field(cid, R) of
{ok, Cid} -> cid_response_for(Cid, F);
_ -> post_activity_response_for(F)
end;
@@ -348,14 +357,36 @@ validation_failed_response() ->
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;
{ok, Got} -> resolve_token(Got, Cfg);
not_found -> {error, no_auth}
end.
%% resolve_token/2 — map a bearer token to either an explicit
%% ActorId (via Cfg's :tokens proplist) or the back-compat `legacy`
%% atom (via the M1 single-actor :publish_token). The :tokens map
%% takes precedence; if both are configured, :publish_token is only
%% consulted when the token isn't present in :tokens.
resolve_token(Got, Cfg) ->
case field(tokens, Cfg) of
nil -> resolve_legacy_token(Got, Cfg);
Tokens ->
case lookup_token(Got, Tokens) of
{ok, ActorId} -> {ok, ActorId};
not_found -> resolve_legacy_token(Got, Cfg)
end
end.
resolve_legacy_token(Got, Cfg) ->
case field(publish_token, Cfg) of
nil -> {error, no_token_match};
Want when Got =:= Want -> {ok, legacy};
_ -> {error, bad_token}
end.
lookup_token(_, []) -> not_found;
lookup_token(K, [{K, V} | _]) -> {ok, V};
lookup_token(K, [_ | Rest]) -> lookup_token(K, Rest).
%% Look up the Authorization header, strip "Bearer ", return token.
bearer_token(Req) ->
case field(headers, Req) of
@@ -383,11 +414,6 @@ strip_bearer(V) ->
_ -> not_found
end.
expected_token(Cfg) ->
case field(publish_token, Cfg) of
nil -> not_found;
T -> {ok, T}
end.
%% ── Step 8d: Accept-header parsing ──────────────────────────────
%%