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
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:
@@ -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 ──────────────────────────────
|
||||
%%
|
||||
|
||||
@@ -40,6 +40,18 @@ cat > "$TMPFILE" <<'EPOCHS'
|
||||
(load "lib/erlang/vm/dispatcher.sx")
|
||||
(epoch 2)
|
||||
(eval "(get (erlang-load-module (file-read \"next/kernel/http_server.erl\")) :name)")
|
||||
(epoch 3)
|
||||
(eval "(er-load-gen-server!)")
|
||||
(epoch 4)
|
||||
(eval "(get (erlang-load-module (file-read \"next/kernel/envelope.erl\")) :name)")
|
||||
(epoch 5)
|
||||
(eval "(get (erlang-load-module (file-read \"next/kernel/log.erl\")) :name)")
|
||||
(epoch 6)
|
||||
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
|
||||
(epoch 7)
|
||||
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
|
||||
(epoch 8)
|
||||
(eval "(get (erlang-load-module (file-read \"next/kernel/nx_kernel.erl\")) :name)")
|
||||
|
||||
;; split_first_slash sanity
|
||||
(epoch 10)
|
||||
@@ -100,6 +112,43 @@ cat > "$TMPFILE" <<'EPOCHS'
|
||||
;; Accept: application/json on /actors/alice/followers -> JSON stub
|
||||
(epoch 32)
|
||||
(eval "(get (erlang-eval-ast \"AcceptKey = <<97,99,99,101,112,116>>, AcceptVal = <<97,112,112,108,105,99,97,116,105,111,110,47,106,115,111,110>>, Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,102,111,108,108,111,119,101,114,115>>}, {headers, [{AcceptKey, AcceptVal}]}, {body, <<>>}], R = http_server:route(Req), [_, _, {body, B}] = R, http_server:match_prefix(<<123,34,102,111,108,108,111,119,101,114,115,34>>, B) =/= nomatch\") :name)")
|
||||
|
||||
;; ── Step 4b: token -> ActorId map ──────────────────────────────
|
||||
;; Each test inlines start_link + add_actor + Cfg with :tokens
|
||||
;; proplist mapping per-actor bearer tokens. Tokens look like
|
||||
;; "alice-token" = <<97,108,105,99,101,45,116,111,107,101,110>>
|
||||
;; (bytes spelled) and "bob-token" = <<98,111,98,45,116,111,107,101,110>>.
|
||||
|
||||
(epoch 40)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], BK = <<5,6,7,8>>, BKS = [{key_id,k1},{algorithm,ed25519},{value,BK}], BAS = [{public_keys,[[{id,k1},{created,0},{value,BK}]]}], nx_kernel:start_link(alice, AKS, AAS), nx_kernel:add_actor(bob, BKS, BAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, BobTok = <<98,111,98,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}, {BobTok, bob}]}], Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], R = http_server:route(Req, Cfg), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<99,105,100,58,32>>, B) =/= nomatch; _ -> false end\") :name)")
|
||||
|
||||
;; Alice token publishes to alice's bucket (log_tip alice = 1, bob = 0)
|
||||
(epoch 41)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], BK = <<5,6,7,8>>, BKS = [{key_id,k1},{algorithm,ed25519},{value,BK}], BAS = [{public_keys,[[{id,k1},{created,0},{value,BK}]]}], nx_kernel:start_link(alice, AKS, AAS), nx_kernel:add_actor(bob, BKS, BAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, BobTok = <<98,111,98,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}, {BobTok, bob}]}], Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], http_server:route(Req, Cfg), {nx_kernel:log_tip_for(alice), nx_kernel:log_tip_for(bob)} =:= {1, 0}\") :name)")
|
||||
|
||||
;; Bob token publishes to bob's bucket
|
||||
(epoch 42)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], BK = <<5,6,7,8>>, BKS = [{key_id,k1},{algorithm,ed25519},{value,BK}], BAS = [{public_keys,[[{id,k1},{created,0},{value,BK}]]}], nx_kernel:start_link(alice, AKS, AAS), nx_kernel:add_actor(bob, BKS, BAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, BobTok = <<98,111,98,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, BobAuth = <<66,101,97,114,101,114,32,98,111,98,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}, {BobTok, bob}]}], Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, BobAuth}]}, {body, <<104,105>>}], http_server:route(Req, Cfg), {nx_kernel:log_tip_for(alice), nx_kernel:log_tip_for(bob)} =:= {0, 1}\") :name)")
|
||||
|
||||
;; Mixed token stream -> independent logs
|
||||
(epoch 43)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], BK = <<5,6,7,8>>, BKS = [{key_id,k1},{algorithm,ed25519},{value,BK}], BAS = [{public_keys,[[{id,k1},{created,0},{value,BK}]]}], nx_kernel:start_link(alice, AKS, AAS), nx_kernel:add_actor(bob, BKS, BAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, BobTok = <<98,111,98,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, BobAuth = <<66,101,97,114,101,114,32,98,111,98,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}, {BobTok, bob}]}], AliceReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], BobReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, BobAuth}]}, {body, <<104,105>>}], http_server:route(AliceReq, Cfg), http_server:route(BobReq, Cfg), http_server:route(AliceReq, Cfg), {nx_kernel:log_tip_for(alice), nx_kernel:log_tip_for(bob)} =:= {2, 1}\") :name)")
|
||||
|
||||
;; Token not in :tokens map and no :publish_token -> 401
|
||||
(epoch 44)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], nx_kernel:start_link(alice, AKS, AAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, GhostAuth = <<66,101,97,114,101,114,32,103,104,111,115,116>>, Cfg = [{tokens, [{AliceTok, alice}]}], Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, GhostAuth}]}, {body, <<104,105>>}], case http_server:route(Req, Cfg) of [{status, 401}, _, _] -> true; _ -> false end\") :name)")
|
||||
|
||||
;; Legacy :publish_token still works (M1 back-compat)
|
||||
(epoch 45)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], nx_kernel:start_link(alice, AKS, AAS), Tok = <<102,111,111>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,102,111,111>>, Cfg = [{publish_token, Tok}], Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}]}, {body, <<104,105>>}], R = http_server:route(Req, Cfg), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<99,105,100,58,32>>, B) =/= nomatch; _ -> false end\") :name)")
|
||||
|
||||
;; :tokens takes precedence; legacy :publish_token still resolved on miss
|
||||
(epoch 46)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], BK = <<5,6,7,8>>, BKS = [{key_id,k1},{algorithm,ed25519},{value,BK}], BAS = [{public_keys,[[{id,k1},{created,0},{value,BK}]]}], nx_kernel:start_link(alice, AKS, AAS), nx_kernel:add_actor(bob, BKS, BAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, LegacyTok = <<102,111,111>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, LegacyAuth = <<66,101,97,114,101,114,32,102,111,111>>, Cfg = [{tokens, [{AliceTok, alice}]}, {publish_token, LegacyTok}], Req1 = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], Req2 = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, LegacyAuth}]}, {body, <<104,105>>}], http_server:route(Req1, Cfg), http_server:route(Req2, Cfg), {nx_kernel:log_tip_for(alice), nx_kernel:log_tip_for(bob)} =:= {2, 0}\") :name)")
|
||||
|
||||
;; Token resolution before kernel is registered -> auth-stub published response
|
||||
(epoch 47)
|
||||
(eval "(get (erlang-eval-ast \"AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}]}], Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<>>}], R = http_server:route(Req, Cfg), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<112,117,98,108,105,115,104,101,100>>, B) =/= nomatch; _ -> false end\") :name)")
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 240 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
||||
@@ -140,6 +189,14 @@ check 29 "outbox body carries actor id" "true"
|
||||
check 30 "outbox JSON content negotiation" "true"
|
||||
check 31 "inbox SX content negotiation" "true"
|
||||
check 32 "followers JSON content negotiation" "true"
|
||||
check 40 "two-token Cfg + Alice POST -> 200" "true"
|
||||
check 41 "Alice token publishes to alice" "true"
|
||||
check 42 "Bob token publishes to bob" "true"
|
||||
check 43 "interleaved tokens isolate logs" "true"
|
||||
check 44 "unknown token -> 401" "true"
|
||||
check 45 "legacy :publish_token still works" "true"
|
||||
check 46 "tokens map + legacy back-compat" "true"
|
||||
check 47 "no kernel + token map -> stub 200" "true"
|
||||
|
||||
TOTAL=$((PASS+FAIL))
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
|
||||
@@ -272,12 +272,19 @@ token; the token now maps to an `:actor_id` rather than a fixed `alice`.
|
||||
`accepted_response/1`). Unknown sub-paths under `/actors/<id>/`
|
||||
return 404. Bare `/actors/<id>` keeps the M1 `actor_doc_response_for`
|
||||
arm. 17 cases in `http_multi_actor.sh`.
|
||||
- [ ] **4b** — Token → ActorId map. Cfg's `:publish_token` becomes
|
||||
`:tokens` (proplist `[{Token, ActorId}, ...]`). POST /activity
|
||||
resolves the bearer token to an actor id and routes through
|
||||
`nx_kernel:publish_to/2` instead of `publish/1`. Multi-token
|
||||
Cfg keeps the M1 `:publish_token` single-token field as a
|
||||
back-compat alias for one token mapping to `alice`.
|
||||
- [x] **4b** — Token → ActorId map. New `resolve_token/2` reads
|
||||
`:tokens` from Cfg (proplist `[{Token, ActorId}, ...]`) and
|
||||
returns `{ok, ActorId}` on match. Falls back to the M1
|
||||
`:publish_token` single-token field on miss (returns
|
||||
`{ok, legacy}`, route through `nx_kernel:publish/1` to bucket 0
|
||||
unchanged). Cfg with both fields: `:tokens` wins for matched
|
||||
tokens; `:publish_token` only consulted on `:tokens` miss.
|
||||
`handle_post_activity` now 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.
|
||||
No-kernel auth-only path unchanged. The dead M1
|
||||
`expected_token/1` helper is gone. 8 new cases in
|
||||
`http_multi_actor.sh` (25/25 total).
|
||||
- [ ] **4c** — `http_server:route/3(Req, Cfg, Kernel)` — the Cfg
|
||||
carries opaque `:kernel` reference (or accepts the registered
|
||||
`nx_kernel` atom) so per-actor handlers can call
|
||||
@@ -731,6 +738,21 @@ proceed.
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-06-06** — Step 4b: token -> ActorId map. Cfg's `:tokens`
|
||||
proplist (`[{Token, ActorId}, ...]`) maps bearer tokens to
|
||||
per-actor publishers. `handle_post_activity` threads the
|
||||
resolved `ActorRef` to `publish_if_kernel/3` which calls
|
||||
`nx_kernel:publish_to/2` for explicit actor ids and `publish/1`
|
||||
for the back-compat `legacy` atom (M1's `:publish_token`
|
||||
single-token field still works as-is). When both fields are
|
||||
present, `:tokens` takes precedence; `:publish_token` is the
|
||||
fallback on miss. Dead `expected_token/1` helper removed. 8
|
||||
new cases in `http_multi_actor.sh` (25/25 total) covering
|
||||
two-actor token routing, log-tip isolation, interleaved
|
||||
publishes, bad-token 401, back-compat coexistence, no-kernel
|
||||
stub path. Conformance 761/761 preserved. 116/116 across 10
|
||||
Step-4-adjacent suites.
|
||||
|
||||
- **2026-06-06** — Step 4a: per-actor HTTP sub-paths. New
|
||||
`split_first_slash/1` helper lets GET / POST `/actors/<id>/...`
|
||||
paths route on the sub-segment (`outbox`, `inbox`, `followers`,
|
||||
|
||||
Reference in New Issue
Block a user