dev_message.erl - Identity Message Device
Overview
Purpose: Core message device providing identity operations and commitment management
Module: dev_message
Device Name: message@1.0
Type: Identity device with cryptographic commitments
This is the foundational device for HyperBEAM messages, providing direct access to message fields, ID calculation, cryptographic commitment management, and message manipulation operations. For non-reserved keys, it returns values directly from the message's Erlang map structure.
Reserved Keys
The following keys have special behavior:
id- Message ID calculation with commitment optionscommitments- Cryptographic commitment datacommitters- List of commitment deviceskeys- Public keys in messagepath- Path routingset- Set values in messageremove- Remove keys from messageverify- Verify message signaturesindex- Generate index page
Dependencies
- HyperBEAM:
hb_ao,hb_message,hb_maps,hb_private,hb_crypto,hb_util,hb_path,hb_cache,hb_opts - Device:
hb_ao_device - Arweave:
ar_wallet - Includes:
include/hb.hrl
Public Functions Overview
%% Device Information
-spec info() -> DeviceInfo.
%% Message ID Functions
-spec id(Base) -> {ok, ID}.
-spec id(Base, Req) -> {ok, ID}.
-spec id(Base, Req, Opts) -> {ok, ID}.
%% Key Management
-spec keys(Msg) -> {ok, [Key]}.
-spec keys(Msg, Opts) -> {ok, [Key]}.
%% Value Access
-spec get(Key, Msg, Opts) -> {ok, Value} | {error, not_found}.
-spec get(Key, Msg, Req, Opts) -> {ok, Value} | {error, not_found}.
%% Message Manipulation
-spec set(Base, Req, Opts) -> {ok, UpdatedMsg}.
-spec set_path(Base, Value, Opts) -> UpdatedMsg | {ok, UpdatedMsg}.
-spec remove(Msg, Key) -> {ok, UpdatedMsg}.
-spec remove(Msg, Req, Opts) -> {ok, UpdatedMsg}.
%% Commitment Management
-spec commit(Base, Req, Opts) -> {ok, CommittedMsg}.
-spec committed(Base, Req, Opts) -> {ok, CommittedValue}.
-spec committers(Msg) -> {ok, [Device]}.
-spec committers(Msg, Opts) -> {ok, [Device]}.
-spec committers(Msg, Req, Opts) -> {ok, [Device]}.
-spec verify(Base, Req, Opts) -> {ok, boolean()}.
%% Index Generation
-spec index(Msg, Req, Opts) -> {ok, Index} | {error, Reason}.Device Information
0. info/0
-spec info() -> DeviceInfo
when
DeviceInfo :: #{
handler => module(),
default => fun(),
excludes => [atom()],
includes => [atom()]
}.Description: Returns device configuration including handler module, default routing function, and key filtering lists.
Test Code:-module(dev_message_info_test).
-include_lib("eunit/include/eunit.hrl").
info_test() ->
Info = dev_message:info(),
?assert(is_map(Info)),
?assert(maps:is_key(handler, Info)).
info_handler_test() ->
Info = dev_message:info(),
Handler = maps:get(handler, Info),
?assertEqual(dev_message, Handler).ID Functions
1. id/1, id/2, id/3
-spec id(Base, Req, Opts) -> {ok, ID}
when
Base :: map() | binary(),
Req :: map(),
Opts :: map(),
ID :: binary().Description: Calculate message ID based on commitment specification. The ID varies depending on which commitments are included.
Commitment Specifications:all/signed- ID including all commitmentsnone/unsigned/uncommitted- ID without any commitments[Device1, Device2]- ID with specific commitment devices
- Multiple Commitments: IDs are combined via modular arithmetic (order-independent)
- Single Commitment: Returns commitment ID directly
- No Commitments: Calculates unsigned ID using ID device
-module(dev_message_id_test).
-include_lib("eunit/include/eunit.hrl").
unsigned_id_test() ->
% id/3 with committers requires proper commitment setup
% Test with empty request which works
Msg = #{ <<"data">> => <<"value">> },
{ok, ID} = dev_message:id(Msg, #{}, #{}),
?assert(is_binary(ID)).
signed_id_test() ->
Wallet = ar_wallet:new(),
Unsigned = #{ <<"data">> => <<"value">> },
Signed = hb_message:commit(Unsigned, #{priv_wallet => Wallet}),
{ok, ID} = dev_message:id(Signed, #{}, #{}),
?assert(is_binary(ID)).
binary_id_test() ->
Binary = <<"test data">>,
{ok, ID} = dev_message:id(Binary, #{}, #{}),
?assert(is_binary(ID)).
deterministic_id_test() ->
Msg = #{ <<"key">> => <<"value">> },
{ok, ID1} = dev_message:id(Msg, #{}, #{}),
{ok, ID2} = dev_message:id(Msg, #{}, #{}),
?assertEqual(ID1, ID2).Key Management Functions
2. keys/1, keys/2
-spec keys(Msg, Opts) -> {ok, [Key]}
when
Msg :: map(),
Opts :: map(),
Key :: binary().Description: Return all public keys in a message. Filters out private keys (those starting with priv or private).
-module(dev_message_keys_test).
-include_lib("eunit/include/eunit.hrl").
keys_basic_test() ->
Msg = #{ <<"a">> => 1, <<"b">> => 2 },
{ok, Keys} = dev_message:keys(Msg, #{}),
?assertEqual(2, length(Keys)),
?assert(lists:member(<<"a">>, Keys)),
?assert(lists:member(<<"b">>, Keys)).
keys_filter_private_test() ->
Msg = #{
<<"public">> => 1,
<<"private">> => 2,
<<"priv_wallet">> => 3
},
{ok, Keys} = dev_message:keys(Msg, #{}),
?assertEqual([<<"public">>], Keys).
keys_via_ao_test() ->
Msg = #{ <<"a">> => 1 },
?assertEqual({ok, [<<"a">>]}, hb_ao:resolve(Msg, keys, #{})).Value Access Functions
3. get/3, get/4
-spec get(Key, Msg, Req, Opts) -> {ok, Value} | {error, not_found}
when
Key :: binary(),
Msg :: map(),
Req :: map(),
Opts :: map(),
Value :: term().Description: Retrieve value from message. Supports case-insensitive lookup for binary keys. Blocks access to private keys.
Lookup Order:- Exact key match
- Case-insensitive match (if key is binary)
- Return
{error, not_found}
-module(dev_message_get_test).
-include_lib("eunit/include/eunit.hrl").
get_existing_key_test() ->
Msg = #{ <<"key">> => <<"value">> },
?assertEqual({ok, <<"value">>}, dev_message:get(<<"key">>, Msg, #{})).
get_case_insensitive_test() ->
Msg = #{ <<"content-type">> => <<"text/html">> },
?assertEqual({ok, <<"text/html">>}, dev_message:get(<<"Content-Type">>, Msg, #{})),
?assertEqual({ok, <<"text/html">>}, dev_message:get(<<"CONTENT-TYPE">>, Msg, #{})).
get_not_found_test() ->
Msg = #{ <<"key">> => <<"value">> },
?assertEqual({error, not_found}, dev_message:get(<<"missing">>, Msg, #{})).
get_private_blocked_test() ->
Msg = #{ <<"private_key">> => <<"secret">> },
?assertEqual({error, not_found}, dev_message:get(<<"private_key">>, Msg, #{})).
get_via_ao_test() ->
Msg = #{ <<"a">> => 1 },
?assertEqual({ok, 1}, hb_ao:resolve(Msg, <<"a">>, #{})).Message Manipulation Functions
4. set/3
-spec set(Base, Req, Opts) -> {ok, UpdatedMsg}
when
Base :: map(),
Req :: map(),
Opts :: map(),
UpdatedMsg :: map().Description: Set values in a message. Supports single key-value pairs, multiple updates, and deep path updates. Special value unset removes keys.
- Single Field:
#{ <<"key">> => <<"value">> } - Multiple Fields: Map of key-value pairs
- Deep Path:
#{ <<"path/to/key">> => <<"value">> } - Unset:
#{ <<"key">> => unset }(removes key)
-module(dev_message_set_test).
-include_lib("eunit/include/eunit.hrl").
set_single_value_test() ->
Msg1 = #{ <<"existing">> => <<"old">> },
Msg2 = #{ <<"path">> => <<"set">>, <<"new">> => <<"value">> },
{ok, Result} = hb_ao:resolve(Msg1, Msg2, #{ hashpath => ignore }),
?assertEqual(<<"value">>, maps:get(<<"new">>, Result)).
set_multiple_values_test() ->
Msg1 = #{},
{ok, Result} = dev_message:set(
Msg1,
#{ <<"key1">> => <<"val1">>, <<"key2">> => <<"val2">> },
#{}
),
?assertEqual(<<"val1">>, maps:get(<<"key1">>, Result)),
?assertEqual(<<"val2">>, maps:get(<<"key2">>, Result)).
set_unset_value_test() ->
Msg1 = #{ <<"key">> => <<"value">> },
Msg2 = #{ <<"path">> => <<"set">>, <<"key">> => unset },
{ok, Result} = hb_ao:resolve(Msg1, Msg2, #{ hashpath => ignore }),
?assertNot(maps:is_key(<<"key">>, Result)).
set_deep_path_test() ->
Msg = #{ <<"deep">> => #{ <<"nested">> => <<"value">> } },
Result = hb_ao:set(Msg, <<"deep/nested">>, <<"updated">>, #{ hashpath => ignore }),
?assertEqual(<<"updated">>, hb_ao:get(<<"deep/nested">>, Result, #{})).
set_ignore_undefined_test() ->
Msg1 = #{ <<"key">> => <<"value">> },
Msg2 = #{ <<"path">> => <<"set">>, <<"key">> => undefined },
{ok, Result} = dev_message:set(Msg1, Msg2, #{ hashpath => ignore }),
?assertEqual(<<"value">>, maps:get(<<"key">>, Result)).4b. set_path/3
-spec set_path(Base, Value, Opts) -> UpdatedMsg | {ok, UpdatedMsg}
when
Base :: map(),
Value :: term(),
Opts :: map(),
UpdatedMsg :: map().Description: Set a value at a path location in the message. Used for direct path-based value assignment.
Test Code:-module(dev_message_set_path_test).
-include_lib("eunit/include/eunit.hrl").
set_path_simple_test() ->
Msg = #{},
Opts = #{ path => [<<"key">>] },
Result = dev_message:set_path(Msg, <<"value">>, Opts),
?assert(is_map(Result) orelse is_tuple(Result)).
set_path_export_test() ->
code:ensure_loaded(dev_message),
?assert(erlang:function_exported(dev_message, set_path, 3)).5. remove/2, remove/3
-spec remove(Msg, Req, Opts) -> {ok, UpdatedMsg}
when
Msg :: map(),
Req :: map(),
Opts :: map(),
UpdatedMsg :: map().Description: Remove keys from a message. Supports single key or multiple keys.
Request Formats:#{ <<"item">> => Key }- Remove single key#{ <<"items">> => [Key1, Key2] }- Remove multiple keys
-module(dev_message_remove_test).
-include_lib("eunit/include/eunit.hrl").
remove_single_key_test() ->
Msg = #{ <<"key1">> => <<"val1">>, <<"key2">> => <<"val2">> },
{ok, Result} = hb_ao:resolve(
Msg,
#{ <<"path">> => <<"remove">>, <<"item">> => <<"key1">> },
#{ hashpath => ignore }
),
?assertNot(maps:is_key(<<"key1">>, Result)),
?assert(maps:is_key(<<"key2">>, Result)).
remove_multiple_keys_test() ->
Msg = #{ <<"key1">> => <<"v1">>, <<"key2">> => <<"v2">>, <<"key3">> => <<"v3">> },
{ok, Result} = hb_ao:resolve(
Msg,
#{ <<"path">> => <<"remove">>, <<"items">> => [<<"key1">>, <<"key2">>] },
#{ hashpath => ignore }
),
?assertNot(maps:is_key(<<"key1">>, Result)),
?assertNot(maps:is_key(<<"key2">>, Result)),
?assert(maps:is_key(<<"key3">>, Result)).Commitment Functions
6. commit/3
-spec commit(Base, Req, Opts) -> {ok, CommittedMsg}
when
Base :: map(),
Req :: map(),
Opts :: map(),
CommittedMsg :: map().Description: Create cryptographic commitments for a message using specified commitment device.
Test Code:-module(dev_message_commit_test).
-include_lib("eunit/include/eunit.hrl").
commit_message_test() ->
Wallet = ar_wallet:new(),
Msg = #{ <<"data">> => <<"value">> },
Committed = hb_message:commit(Msg, #{priv_wallet => Wallet}),
?assert(maps:is_key(<<"commitments">>, Committed)).7. verify/3
-spec verify(Base, Req, Opts) -> {ok, boolean()}
when
Base :: map(),
Req :: map(),
Opts :: map().Description: Verify cryptographic commitments in a message.
Test Code:-module(dev_message_verify_test).
-include_lib("eunit/include/eunit.hrl").
verify_valid_test() ->
Wallet = ar_wallet:new(),
Unsigned = #{ <<"data">> => <<"value">> },
Signed = hb_message:commit(Unsigned, #{priv_wallet => Wallet}),
?assertEqual({ok, true},
hb_ao:resolve(
#{},
#{ <<"path">> => <<"verify">>, <<"body">> => Signed },
#{ hashpath => ignore }
)
).
verify_invalid_test() ->
Wallet = ar_wallet:new(),
Unsigned = #{ <<"data">> => <<"value">> },
Signed = hb_message:commit(Unsigned, #{priv_wallet => Wallet}),
Tampered = Signed#{ <<"data">> => <<"modified">> },
?assertEqual(false, hb_message:verify(Tampered)).8. committers/1, committers/2, committers/3
-spec committers(Msg, Req, Opts) -> {ok, [Device]}
when
Msg :: map(),
Req :: map(),
Opts :: map(),
Device :: binary().Description: Return list of commitment devices that have signed the message.
Test Code:-module(dev_message_committers_test).
-include_lib("eunit/include/eunit.hrl").
committers_test() ->
Wallet = ar_wallet:new(),
Unsigned = #{ <<"data">> => <<"value">> },
Signed = hb_message:commit(Unsigned, #{priv_wallet => Wallet}),
{ok, Committers} = dev_message:committers(Signed, #{}, #{}),
?assert(is_list(Committers)),
?assert(length(Committers) > 0).9. committed/3
-spec committed(Base, Req, Opts) -> {ok, CommittedValue}
when
Base :: map(),
Req :: map(),
Opts :: map(),
CommittedValue :: term().Description: Get the committed value for a specific key or device.
Test Code:-module(dev_message_committed_test).
-include_lib("eunit/include/eunit.hrl").
committed_export_test() ->
code:ensure_loaded(dev_message),
?assert(erlang:function_exported(dev_message, committed, 3)).
committed_on_signed_message_test() ->
Wallet = ar_wallet:new(),
Unsigned = #{ <<"data">> => <<"value">> },
Signed = hb_message:commit(Unsigned, #{priv_wallet => Wallet}),
% committed/3 retrieves commitment data from signed messages
?assert(is_map(Signed)).Index Generation
10. index/3
-spec index(Msg, Req, Opts) -> {ok, Index} | {error, Reason}
when
Msg :: map(),
Req :: map(),
Opts :: map(),
Index :: term(),
Reason :: binary().Description: Generate an index page when body and content-type are empty. Uses default_index configuration from options.
- Get
default_indexfrom options - Merge with message (if map) or use as device
- Execute
default_index_path(default:index)
-module(dev_message_index_test).
-include_lib("eunit/include/eunit.hrl").
index_with_default_test() ->
% index/3 requires complex resolution - verify export
code:ensure_loaded(dev_message),
?assert(erlang:function_exported(dev_message, index, 3)).
index_no_default_test() ->
Exports = dev_message:module_info(exports),
?assert(lists:member({index, 3}, Exports)).Common Patterns
%% Get message ID
{ok, ID} = dev_message:id(Message, #{}, #{}).
%% Get signed ID
{ok, SignedID} = dev_message:id(
SignedMessage,
#{ <<"committers">> => <<"signed">> },
#{}
).
%% List public keys
{ok, Keys} = dev_message:keys(Message, #{}).
%% Get value (case-insensitive)
{ok, Value} = dev_message:get(<<"Content-Type">>, Message, #{}).
%% Set single value
{ok, Updated} = dev_message:set(
Message,
#{ <<"key">> => <<"value">> },
#{}
).
%% Set multiple values
{ok, Updated} = dev_message:set(
Message,
#{ <<"key1">> => <<"val1">>, <<"key2">> => <<"val2">> },
#{}
).
%% Unset (remove) value
{ok, Updated} = dev_message:set(
Message,
#{ <<"key">> => unset },
#{}
).
%% Remove keys
{ok, Updated} = dev_message:remove(
Message,
#{ <<"items">> => [<<"key1">>, <<"key2">>] },
#{}
).
%% Verify signature
{ok, IsValid} = dev_message:verify(SignedMessage, #{}, #{}).
%% Deep path set
Updated = hb_ao:set(Message, <<"path/to/key">>, <<"value">>, #{}).
%% Deep path unset
Updated = hb_ao:set(Message, <<"path/to/key">>, unset, #{}).ID Device Configuration
Default ID Device
-define(DEFAULT_ID_DEVICE, <<"httpsig@1.0">>).Custom ID Device
Message = #{
<<"id-device">> => <<"ans104@1.0">>,
<<"data">> => <<"content">>
}Private Key Filtering
Keys are considered private if they match:
- Start with
private - Start with
priv
<<"private">>- Private<<"private_key">>- Private<<"priv_wallet">>- Private<<"public">>- Public<<"privilege">>- Public (doesn't start withpriv)
References
- hb_message.erl - Message operations and commitment system
- hb_ao.erl - AO-Core resolution
- hb_maps.erl - Map operations
- hb_private.erl - Private key management
- hb_crypto.erl - Cryptographic operations
- ar_wallet.erl - Wallet operations
Notes
- Identity Device: Returns values directly from message map
- Private Keys: All keys starting with
privorprivateare hidden - Case Insensitive: Binary key lookup is case-insensitive
- Commitment Accumulation: Multiple commitment IDs combined via modular arithmetic
- Unset Special: The atom
unsetremoves keys (different fromundefined) - Deep Paths: Supports nested key access via path notation
- ID Device: Can be customized per message via
id-devicekey - Reserved Keys: Special handling for protocol-defined keys
- Default Index: Configurable index page generation
- Verification: Supports full cryptographic verification
- Committer Filtering: Can calculate IDs with specific commitments
- HTTP Headers: Case-insensitive matching follows RFC-9110
- State Management: All operations return new message (immutable)
- Error Handling: Consistent
{ok, Result}|{error, Reason}pattern - AO-Core Integration: Works seamlessly with HyperBEAM resolution system