Files
rose-ash/lib/identity/audit.sx
giles a5c22c5a01
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 42s
identity: grant audit ledger — issue/refresh/revoke events, queryable per subject (10 tests)
audit.sx is an append-only ledger process. token.sx gains start/1(Audit)
and emits an event on every grant transition (issue, refresh, revoke —
including reuse-triggered revoke); start/0 stays unaudited so existing use
is unchanged (token.sx has no compile-time dep on the audit module, it just
sends to a pid). The ledger answers (identity/audit subject) via
audit/actions/count/all, chronological. In-memory event stream; persist
backing is a later Erlang<->persist bridge, out of scope. 111/111.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 01:16:18 +00:00

28 lines
3.1 KiB
Plaintext

;; identity/audit.sx — the grant audit ledger.
;;
;; Every transition that changes a grant — issue, refresh, revoke (and,
;; wired from oauth, consent) — appends an immutable event to this
;; append-only process. The ledger is queryable by subject, which is what
;; `(identity/audit subject)` answers. This is the in-memory realisation
;; of the event stream; a persist-backed stream is a later substrate
;; concern (Erlang↔persist bridge), kept out of scope here per the loop's
;; \"in-memory log until persist lands\" allowance — the queryable
;; semantics are identical.
;;
;; Events are {Seq, Subject, Action}; Seq is a monotonic sequence number.
;; Reads return chronological (oldest-first) order:
;;
;; record(A, Subject, Action) -> ok (one-way; FIFO-ordered)
;; audit(A, Subject) -> [{Seq, Subject, Action}, ...]
;; actions(A, Subject) -> [Action, ...]
;; count(A, Subject) -> N
;; all(A) -> [{Seq, Subject, Action}, ...]
(define
identity-audit-source
"-module(identity_audit).\n\n start() ->\n spawn(fun () -> loop([], 0) end).\n\n record(A, Subject, Action) ->\n A ! {event, Subject, Action},\n ok.\n\n audit(A, Subject) ->\n A ! {audit, Subject, self()},\n receive {audit_reply, R} -> R end.\n\n actions(A, Subject) ->\n A ! {actions, Subject, self()},\n receive {audit_reply, R} -> R end.\n\n count(A, Subject) ->\n A ! {count, Subject, self()},\n receive {audit_reply, R} -> R end.\n\n all(A) ->\n A ! {all, self()},\n receive {audit_reply, R} -> R end.\n\n loop(Events, Seq) ->\n receive\n {event, Subject, Action} ->\n loop([{Seq, Subject, Action} | Events], Seq + 1);\n {audit, Subject, From} ->\n From ! {audit_reply, collect(Subject, Events, [])},\n loop(Events, Seq);\n {actions, Subject, From} ->\n From ! {audit_reply, action_list(Subject, Events, [])},\n loop(Events, Seq);\n {count, Subject, From} ->\n From ! {audit_reply, count_subj(Subject, Events, 0)},\n loop(Events, Seq);\n {all, From} ->\n From ! {audit_reply, reverse(Events, [])},\n loop(Events, Seq);\n {stop, From} ->\n From ! {audit_reply, ok}\n end.\n\n collect(_, [], Acc) -> Acc;\n collect(Subject, [{Seq, S, A} | Rest], Acc) ->\n case S =:= Subject of\n true -> collect(Subject, Rest, [{Seq, S, A} | Acc]);\n false -> collect(Subject, Rest, Acc)\n end.\n\n action_list(_, [], Acc) -> Acc;\n action_list(Subject, [{_, S, A} | Rest], Acc) ->\n case S =:= Subject of\n true -> action_list(Subject, Rest, [A | Acc]);\n false -> action_list(Subject, Rest, Acc)\n end.\n\n count_subj(_, [], N) -> N;\n count_subj(Subject, [{_, S, _} | Rest], N) ->\n case S =:= Subject of\n true -> count_subj(Subject, Rest, N + 1);\n false -> count_subj(Subject, Rest, N)\n end.\n\n reverse([], Acc) -> Acc;\n reverse([H | T], Acc) -> reverse(T, [H | Acc]).")
(define
identity-load-audit!
(fn () (erlang-load-module identity-audit-source)))