fed-sx-m1: Step 6d-publish — outbox:publish/2 orchestration (construct+sign+validate+append) + 13 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s

This commit is contained in:
2026-05-28 05:14:11 +00:00
parent 6e12f539fd
commit c5481d06aa
3 changed files with 182 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
-module(outbox).
-export([construct/4, sign/2, cid_of/1]).
-export([construct/4, sign/2, cid_of/1, publish/2]).
%% Outbox envelope construction + signing per design §3.1.
%%
@@ -53,3 +53,53 @@ sign(Envelope, KeySpec) ->
cid_of(Envelope) ->
{ok, Id} = envelope:get_field(id, Envelope),
Id.
%% publish/2 — the outbound activity pipeline orchestrator.
%%
%% Request shape: [{type, T}, {object, O}]
%% Context shape: [{actor_id, A}, {published, P}, {key_spec, KS},
%% {actor_state, AS}, {log, L}]
%%
%% Returns:
%% {ok, [{cid, Cid}, {activity, Signed}], NewLog} — happy path
%% {error, Reason, LogState} — validation halted
%%
%% Stages run in order: envelope shape, signature, replay. The
%% replay check uses the log state pre-append, so if the caller
%% publishes the same Request twice with the same Published
%% timestamp the second call halts with {error, replay, _}.
%%
%% Projection-scheduler dispatch (the async fold the design calls
%% for) is deferred to Step 7 — once the projection gen_server
%% exists, this function will broadcast `Signed` to it.
publish(Request, Context) ->
Type = envelope_field(type, Request),
Object = envelope_field(object, Request),
ActorId = envelope_field(actor_id, Context),
Published = envelope_field(published, Context),
KeySpec = envelope_field(key_spec, Context),
ActorState = envelope_field(actor_state, Context),
LogState = envelope_field(log, Context),
Unsigned = construct(Type, ActorId, Published, Object),
Signed = sign(Unsigned, KeySpec),
Stages = [
fun (A) -> pipeline:stage_envelope(A) end,
pipeline:stage_signature(ActorState),
pipeline:stage_replay(LogState)
],
case pipeline:run_stages(Signed, Stages) of
ok ->
{ok, NewLog, _Seq} = log:append(LogState, Signed),
Result = [{cid, cid_of(Signed)}, {activity, Signed}],
{ok, Result, NewLog};
{error, Reason} ->
{error, Reason, LogState}
end.
envelope_field(K, PL) ->
case envelope:get_field(K, PL) of
{ok, V} -> V;
not_found -> nil
end.