Releases: macula-io/macula
Releases · macula-io/macula
Release list
v10.25.0
chore: release v10.25.0 Moves the unreleased changes under 10.25.0 and sets the app version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011RWqkFb5xWX63dCJttFouW
v10.24.0
chore: release v10.24.0 Moves the unreleased changes under 10.24.0 and sets the app version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pNC2B1XzrjGoZtRygZRCp
v10.23.0
chore: release v10.23.0 Moves the unreleased changes under 10.23.0 and sets the app version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pNC2B1XzrjGoZtRygZRCp
v10.22.0
chore: release v10.22.0 Covers the six commits since v10.21.0 (529d288, 69fd639, ca15537, 76747bc, deaf01b, 4a0b5ce). CHANGELOG documents the two previously undocumented feature/fix commits (76747bc, deaf01b); 69fd639 + its revert ca15537 verified to net to a true no-op (git diff v10.21.0..ca15537 touches only the unrelated 529d288 script, confirmed by inspection) so neither gets its own entry. Minor bump (10.21.0 -> 10.22.0) per this repo's own version table: deaf01b adds new backward-compatible functionality (the draining notification + dedicated_streams_idle cast), which is MINOR regardless of the two accompanying Fixed entries. Checklist: - rebar3 eunit --module=macula_station_link_tests: 58/58 pass. - rebar3 eunit (full suite), twice: both runs hit the documented pre-existing flake (macula_full_eunit_suite_flaky_under_load) on macula_station_link_tests:disconnect_notifies_subscribers_test_ (a setelement/tuple-index race, unrelated to this change) -- passes cleanly in isolation both times, confirmed not a regression. - rebar3 dialyzer: clean, no warnings. - rebar3 ex_doc: builds clean; only pre-existing CHANGELOG cross-reference warnings from older, unrelated entries remain (none introduced by this release). Not run: rebar3 hex publish -- Raf's call to fire, per standing policy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhMehfurzfWzTW4bTLzrPs
v10.21.0
chore: release v10.21.0 Covers everything currently on main: the realm_member_required auth policy (4a4a928, a6907c3) and the ucan_nif never-expiring-token fix (6844d8f). Minor bump -- realm_member_required is a genuine new feature. CHANGELOG leads with the ucan_nif security fix (silent exp-omission on any opts JSON encoding failure, affecting every UCAN this library has ever issued) stated plainly and prominently rather than buried under the new feature, then realm_member_required itself with the device- tier-bypass finding folded into its own history (found and fixed within the same work, not shipped as a separate line), then the advertise/5 fail-fast policy-shape guard as its own bullet. Also synced docs/guides/shared/AUTHORIZATION_GUIDE.md, which still listed only open/ucan_required in both its overview callout and its worked-example section -- added realm_member_required to both, plus a worked example matching the existing ucan_required one. Pre-publish checklist run: rebar3 eunit (2131 passed, 1 pre-existing flaky failure -- disconnect_notifies_subscribers_test_, confirmed present on the unmodified pre-realm_member_required baseline earlier today, unrelated to anything in this release), rebar3 dialyzer (clean), rebar3 ex_doc (exit 0; confirmed zero new cross-reference warnings introduced by this changeset specifically -- the ~50 existing warnings are pre-existing stale backtick references inside old CHANGELOG entries, unchanged by this commit). Not published to hex.pm -- that's Raf's own call per this repo's release process. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFHPxb2H9GANFJFMw1yB6F
v10.20.3
Fix silent stale-NIF caching in build-nifs.sh and fetch-nif.sh Both scripts skipped rebuilding purely on [ -f "$NIF_FILE" ] -- whether the artifact existed at all, never whether it was still current. This is exactly what masked several of today's earlier verification runs: editing deterministic.rs and running `rebar3 compile`/`eunit` kept silently loading an Aug-27 .so through a full day of "clean" results, until an unrelated regression test's crash (from decoding against genuinely stale code) surfaced it. Fixed by also checking, via `find -newer` (POSIX, portable across GNU and BSD find), whether any .rs/Cargo.toml/Cargo.lock file under the crate is newer than the built artifact -- rebuild if so. fetch-nif.sh had a second, related bug fixed in the same pass: MACULA_FORCE_SOURCE_BUILD=1 did nothing at all whenever a cached artifact already existed, because the bare existence check ran BEFORE that env var was ever consulted -- the one thing that flag exists for was silently defeated by a cache hit. Verified RED then GREEN, not just read: - Reproduced the original silent-staleness bug in isolation (an aged .so next to a newer source file, old check says skip). - Confirmed the new check catches it (says rebuild). - Confirmed it doesn't over-trigger once genuinely up to date, and that a Cargo.toml-only change (no .rs edit) is caught too. - Confirmed the MACULA_FORCE_SOURCE_BUILD fix specifically: old logic exits before ever checking the env var, new logic proceeds past the skip as intended. - Confirmed end-to-end against this repo's own build: touching deterministic.rs and running `rebar3 compile` now visibly logs "[macula_cbor_nif] Building NIF from source..." and produces a new .so, where before it silently did neither. Reran immediately after to confirm no rebuild loop (correctly skips once genuinely current). Full rebar3 eunit (2099+, same 1 documented pre-existing flake) and dialyzer (0 warnings) clean afterward. Same defect (priv/build-nifs.sh only -- no fetch-nif.sh equivalent there) found and fixed the same way in reckon-db-org/reckon-db. Surveyed every other native-NIF-having repo in the workspace (hecate-graph, hecate-embed, hecate-vector, hecate-rag): all four already run `cargo build` unconditionally on every invocation, with no existence-only skip -- not affected, left untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WcgZMqfUYLdsS4JfKQzLSA
v10.20.2
Document a known sha256 per-chunk-verification gap in chunk_mcid/3 No behavior change. Found while auditing content-transfer code adjacent to today's CBOR decoder work: macula_content_transfer: verify_block_hash/2 verifies every fetched chunk with hardcoded blake3, regardless of a manifest's hash_algorithm field, while create/2 bakes whichever algorithm was requested into each chunk's hash (and therefore its MCID). A manifest built with hash_algorithm => sha256 produces chunk MCIDs that can never actually verify on fetch as a result -- hash_algorithm today only affects the manifest's own root-hash Merkle combination step in verify/2, not per-chunk integrity. chunk_mcid/3's own Algorithm argument is already unused (the chunk's hash was already computed at create time), which is what led to tracing this. Same shape confirmed independently in macula-io/macula-rust's content.rs (block_mcid is likewise hardcoded to blake3). Checked macula-io/macula-py's equivalent call too -- it already matches this reference behavior correctly, not a bug there. Recorded rather than fixed: this is an open design question (was per-chunk sha256 verification ever meant to work, or is per-chunk hashing deliberately always-blake3 with hash_algorithm scoped to root-hash only) rather than a bug with an obvious fix, and nothing exercises sha256 in practice today. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WcgZMqfUYLdsS4JfKQzLSA
v10.20.1
Fix 3 pre-auth vulnerabilities in the deterministic CBOR decoder deterministic.rs's decode_one/decode_array/decode_map is the codec every network-received frame decodes through before any signature check (macula_frame.erl, macula_record.erl), running as a NIF directly on a BEAM scheduler thread. Found across four rounds of adversarial review of the same function: 1. Uncatchable process crash via stack overflow: plain recursive descent had no nesting-depth limit. A list-of-one-list-of-one- list... encodes extreme nesting in one byte per level; a ~10KB frame reliably segfaulted the whole BEAM VM, not just one connection. Fixed with MAX_NESTING_DEPTH = 128, adapted from a sibling Rust reimplementation of this codec already through 3 rounds of its own adversarial review plus 1M fuzzed payloads. 2. O(depth x key size) blowup reintroduced by 10.20.0's O(n^2) dedup fix: re-encoding a key fresh via encode_value at every ancestor level, for a map whose key is itself a large nested structure, turned back into real seconds of pre-auth CPU on a frame still under the size cap. Fixed by threading a need_canon flag alongside depth and building each decoded value's own canonical bytes bottom-up (computed once, only ever concatenated/sorted by ancestors, never re-derived) -- bounding the cost to the same MAX_NESTING_DEPTH bound already enforced elsewhere. A 128-deep chain of single-entry maps wrapping a 4MB blob key now decodes in ~400ms; it took 8+ seconds and 400MB+ transient at just 100 levels before this fix. 3. Uncatchable VM abort on a NaN/Infinity map key: decode_major7's float32/float64 arms built an Erlang float term directly from the wire bits with no finiteness check, unlike the adjacent half-float arm which already rejects it. BEAM has no NaN/Infinity float representation; the resulting invalid term worked fine right up until this fix's own encode_value call inspected it as a map key, aborting the whole VM with an ERTS assertion failure (tag_val_def(), SIGABRT) -- not a catchable Erlang exception. Fixed by rejecting non-finite floats in both arms, matching the half-float arm's existing behavior. All three verified empirically -- RED then GREEN against isolated pre-fix builds, not reasoned about -- and adversarially reviewed (4 rounds total across this function today, the final round finding nothing blocking). Depth limit: accepts exactly at 128, rejects at 129, rejects 100,000 without crashing. NaN/Infinity: 6 variants (f32/ f64, NaN/+Inf/-Inf, nested-in-a-key, bare top-level) all now return clean catchable errors; finite floats unaffected. O(n^2): 20,000 keys in 48ms. O(depth x size): 128-deep/4MB-blob-key in ~400ms. 6 new regression tests: nesting-depth boundary (accept 128/reject 129/reject 100,000-without-crashing), duplicate-key-slot preservation (flat and, per the final review round, nested-map-key cases -- the branch the differential fuzz suite structurally can't reach, since it only ever feeds already-canonical bytes), both quadratic-complexity guards, and 6 NaN/Infinity malformed-input vectors. Full differential fuzz suite (3000 randomized terms against the pure-Erlang reference) and full rebar3 eunit/dialyzer clean throughout. Also exposed and worked around (not fixed here, tracked separately): priv/*.so under native/*/priv/build-nifs.sh's "skip if already built" caching can go stale relative to source changes with zero warning -- this silently invalidated several "clean rebar3 eunit" verification runs earlier today until caught by comparing against isolated scratch builds. Deleting priv/macula_cbor_nif.so before each real verification pass is what made today's later results trustworthy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WcgZMqfUYLdsS4JfKQzLSA
v10.20.0
Bump gproc/telemetry, fix phantom msgpack dep, unblock on macula-mdns gproc 0.9.1 -> 1.3.0 (exact pin -> ~> 1.3): audited commit-by-commit across all 4 tag jumps via the GitHub compare API. Purely additive (new reg_remote/unreg_remote/set_value_remote/update_counter_remote remote-registration API) plus internal non-exported refactors (gproc_pool's setup_wait/do_wait/clear_wait timer-to-monotonic-time rewrite) plus an OTP 29 warnings fix in the final jump. No exported function removed or changed signature. This repo only calls gproc:lookup_pids/1 (standalone script + test helper), untouched by any of it. telemetry constraint loosened 1.3.0 -> ~> 1.3 (was exact). Two-segment ~> still resolves to the real latest (1.4.2) in an isolated build. Originally drafted as ~> 1.4, but adversarial review caught that this would be unsatisfiable for any consumer resolving macula alongside evoq/reckon_db/reckon_gater, which all pin telemetry exactly to 1.3.0 -- ~> 1.3 is compatible with both. macula_mdns floor raised 0.1.0 -> ~> 0.1.1. 0.1.0 pinned gproc exactly to 0.9.1, which made this repo's own gproc ~> 1.3 unsatisfiable for any consumer resolving both packages together -- rebar3 masked this locally by letting the root project's constraint win with just a warning, but a real solver has no solution for gproc == 0.9.1 AND gproc >= 1.3.0. macula_mdns 0.1.1 fixes that pin and, caught by the same review, a real bug: mdns:vsn/0 referenced the pre-rename OTP application name and crash-looped the built-in mDNS advertiser on every distributed node. See macula-mdns's own CHANGELOG/commit for the full trace. Release order: macula_mdns 0.1.1 must be hex-published before or together with this release, or this package's own build breaks for every fresh consumer in between. Also fixes a phantom msgpack runtime dependency present on every hex release since v3.0.0: rebar.config's deps list has had zero msgpack entries since the CBOR migration, but a stale local (untracked) rebar.lock kept resolving and republishing it anyway, because `rebar3 hex publish` derives its declared requirements from the resolved/locked graph rather than purely from rebar.config. The only thing still requiring the real msgpack library was test/dht_address_serialization_test.erl, a debugging-era test that doesn't call any macula function -- it only exercises the third-party msgpack library's own tuple-vs-binary packing behavior, disconnected from the current CBOR wire path (the `msgpack` atom used elsewhere as a macula_stream encoding tag is unrelated and needs no library). Deleted that test; a clean rebuild now resolves zero msgpack anywhere. Verified via a from-scratch _build wipe against the real macula_mdns 0.1.1 source (checkouts override, since it isn't hex-published yet): compile clean, eunit 2084/2085 (1 failure is the documented pre-existing macula_station_link_tests flake, confirmed clean 3/3 in isolation), dialyzer 0 warnings, ex_doc 48 warnings (byte-identical to the pre-change baseline via git stash). Both dependency-bump diffs went through adversarial review before landing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WcgZMqfUYLdsS4JfKQzLSA
v10.19.2
Stop the prod profile from stripping consumers' debug_info rebar3 merges a fetched dependency's erl_opts across every profile that dependency defines, not just whichever one the consumer actually activated. The prod profile here (no_debug_info, deterministic) was silently stripping debug_info from every consumer's copy of macula, whether or not they ever selected prod themselves -- breaking `rebar3 dialyzer` for anyone with macula in plt_extra_apps. Confirmed with a controlled A/B rebuild: a bare git dependency fetch of this exact repo, only variable changed was this profile's erl_opts. With it: consumer's compile options for macula came back missing debug_info, deterministic present despite nobody selecting prod. Without it: debug_info present, deterministic gone, full abstract_code confirmed via beam_lib:chunks. Nothing in this repo's own CI/scripts invokes `rebar3 as prod` or a relx release build (grepped, zero hits) -- the profile's relx block is a local-dev-only convenience, left untouched. Only the erl_opts override is removed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WcgZMqfUYLdsS4JfKQzLSA