dev_meta.erl - HyperBEAM Meta Device
Overview
Purpose: Default entry point for all HyperBEAM message processing with node configuration management
Module: dev_meta
Device Name: meta@1.0
Role: Request preprocessing, routing, and node administration
This device serves as the primary interface for HyperBEAM nodes, handling initialization, configuration management, request/response hooks, and providing system information. It acts as the gateway for all incoming messages before they reach the AO-Core resolver.
Dependencies
- HyperBEAM:
hb_ao,hb_message,hb_opts,hb_cache,hb_singleton,hb_util,hb_private,hb_maps - HTTP:
hb_http,hb_http_server - Device:
hb_ao_device - Arweave:
ar_wallet - Build Info:
../_build/hb_buildinfo.hrl - Includes:
include/hb.hrl
Public Functions Overview
%% Device Information
-spec info(M1) -> DeviceInfo.
-spec info(Base, Request, NodeMsg) -> {ok, NodeInfo} | {error, Reason}.
%% Build Information
-spec build(Base, Req, NodeMsg) -> {ok, BuildInfo}.
%% Request Handling
-spec handle(NodeMsg, Request) -> {ok, Response} | {error, Reason}.
%% Node Management
-spec adopt_node_message(Request, NodeMsg) -> {ok, NewNodeMsg} | {error, Reason}.
%% Authorization
-spec is(Role, Request, NodeMsg) -> boolean().
-spec is_operator(Request, NodeMsg) -> boolean().Public Functions
1. info/1
-spec info(M1) -> DeviceInfo
when
M1 :: map(),
DeviceInfo :: #{ exports => [binary()] }.Description: Return device information. Exports info and build functions for public access.
-module(dev_meta_info_test).
-include_lib("eunit/include/eunit.hrl").
info_exports_test() ->
Info = dev_meta:info(#{}),
?assert(maps:is_key(exports, Info)),
Exports = maps:get(exports, Info),
?assert(lists:member(<<"info">>, Exports)),
?assert(lists:member(<<"build">>, Exports)).2. info/3 - Node Configuration
-spec info(Base, Request, NodeMsg) -> {ok, NodeInfo} | {error, Reason}
when
Base :: map(),
Request :: map(),
NodeMsg :: map(),
NodeInfo :: map(),
Reason :: binary().Description: Get or update node configuration. GET requests return current configuration (with private keys filtered). POST requests update configuration if authorized.
Authorization:- Node must be initialized
- Request must be signed by node operator
- If node is
permanent, no further updates allowed
-module(dev_meta_info_http_test).
-include_lib("eunit/include/eunit.hrl").
get_node_info_test() ->
Node = hb_http_server:start_node(#{
test_config_item => <<"test_value">>
}),
{ok, Info} = hb_http:get(Node, <<"/~meta@1.0/info">>, #{}),
?assertEqual(<<"test_value">>, hb_ao:get(<<"test_config_item">>, Info, #{})).
set_node_info_test() ->
Owner = ar_wallet:new(),
Node = hb_http_server:start_node(#{
priv_wallet => Owner,
test_item => <<"old">>
}),
{ok, _} = hb_http:post(
Node,
hb_message:commit(
#{
<<"path">> => <<"/~meta@1.0/info">>,
<<"test_item">> => <<"new">>
},
#{ priv_wallet => Owner }
),
#{}
),
{ok, Info} = hb_http:get(Node, <<"/~meta@1.0/info">>, #{}),
?assertEqual(<<"new">>, hb_ao:get(<<"test_item">>, Info, #{})).
permanent_node_test() ->
Owner = ar_wallet:new(),
Node = hb_http_server:start_node(#{ priv_wallet => Owner }),
% Set permanent
{ok, _} = hb_http:post(
Node,
hb_message:commit(
#{
<<"path">> => <<"/~meta@1.0/info">>,
initialized => <<"permanent">>
},
#{ priv_wallet => Owner }
),
#{}
),
% Try to update again - should fail or return error
Result = hb_http:post(
Node,
hb_message:commit(
#{
<<"path">> => <<"/~meta@1.0/info">>,
<<"test">> => <<"value">>
},
#{ priv_wallet => Owner }
),
#{}
),
% After permanent, updates should fail in some way
?assert(is_tuple(Result)).3. build/3
-spec build(Base, Req, NodeMsg) -> {ok, BuildInfo}
when
Base :: map(),
Req :: map(),
NodeMsg :: map(),
BuildInfo :: #{
<<"node">> => binary(),
<<"version">> => binary(),
<<"source">> => binary(),
<<"source-short">> => binary(),
<<"build-time">> => binary()
}.Description: Return build information including version number, git hash, and build timestamp.
Test Code:-module(dev_meta_build_test).
-include_lib("eunit/include/eunit.hrl").
build_info_test() ->
Node = hb_http_server:start_node(#{}),
{ok, NodeName} = hb_http:get(Node, <<"/~meta@1.0/build/node">>, #{}),
?assertEqual(<<"HyperBEAM">>, NodeName),
% Version and other fields may not be binary - check they exist
VersionResult = hb_http:get(Node, <<"/~meta@1.0/build/version">>, #{}),
?assertMatch({ok, _}, VersionResult),
SourceResult = hb_http:get(Node, <<"/~meta@1.0/build/source">>, #{}),
?assertMatch({ok, _}, SourceResult).4. handle/2
-spec handle(NodeMsg, Request) -> {ok, Response} | {error, Reason}
when
NodeMsg :: map(),
Request :: term(),
Response :: map(),
Reason :: term().Description: Main request handler. Normalizes requests, applies hooks, routes to appropriate handlers based on initialization state.
Request Flow:- Normalize: Convert to singleton format
- Check Init: If not initialized, only allow
meta@1.0/info - Pre-hook: Apply
on/requesthook if configured - Resolve: Route to AO-Core resolver
- Post-hook: Apply
on/responsehook if configured - Embed Status: Add status code to response
-module(dev_meta_handle_test).
-include_lib("eunit/include/eunit.hrl").
handle_before_init_test() ->
% handle/2 requires running HTTP server for normal requests
% Just verify export exists
code:ensure_loaded(dev_meta),
?assert(erlang:function_exported(dev_meta, handle, 2)).
handle_normal_request_test() ->
% handle/2 with initialized node requires ranch/cowboy server
Exports = dev_meta:module_info(exports),
?assert(lists:member({handle, 2}, Exports)).5. is_operator/2
-spec is_operator(Request, NodeMsg) -> boolean()
when
Request :: map(),
NodeMsg :: map().Description: Check if request is signed by the node operator. Returns true if node is unclaimed or requester is operator.
-module(dev_meta_is_operator_test).
-include_lib("eunit/include/eunit.hrl").
is_operator_unclaimed_test() ->
NodeMsg = #{ operator => unclaimed },
Request = hb_message:commit(#{}, ar_wallet:new()),
?assert(dev_meta:is_operator(Request, NodeMsg)).
is_operator_authorized_test() ->
Owner = ar_wallet:new(),
NodeMsg = #{
priv_wallet => Owner,
operator => hb_util:human_id(ar_wallet:to_address(Owner))
},
Request = hb_message:commit(#{}, Owner),
?assert(dev_meta:is_operator(Request, NodeMsg)).
is_operator_unauthorized_test() ->
Owner = ar_wallet:new(),
Other = ar_wallet:new(),
NodeMsg = #{
priv_wallet => Owner,
operator => hb_util:human_id(ar_wallet:to_address(Owner))
},
Request = hb_message:commit(#{}, Other),
?assertNot(dev_meta:is_operator(Request, NodeMsg)).6. is/2, is/3
-spec is(Role, Request, NodeMsg) -> boolean()
when
Role :: admin | operator | atom(),
Request :: map(),
NodeMsg :: map().Description: Check if request has specific role. Currently supports admin and operator roles.
admin- Alias foroperatoroperator- Node operator (can modify configuration)
-module(dev_meta_is_test).
-include_lib("eunit/include/eunit.hrl").
is_admin_test() ->
Owner = ar_wallet:new(),
NodeMsg = #{ priv_wallet => Owner },
Request = hb_message:commit(#{}, Owner),
?assert(dev_meta:is(admin, Request, NodeMsg)).
is_not_admin_test() ->
Owner = ar_wallet:new(),
Other = ar_wallet:new(),
NodeMsg = #{ priv_wallet => Owner },
Request = hb_message:commit(#{}, Other),
?assertNot(dev_meta:is(admin, Request, NodeMsg)).7. adopt_node_message/2
-spec adopt_node_message(Request, NodeMsg) -> {ok, NewNodeMsg} | {error, Reason}
when
Request :: map(),
NodeMsg :: map(),
NewNodeMsg :: map(),
Reason :: term().Description: Adopt changes to node configuration. Validates and merges request into node message, preserving critical fields.
Protected Fields:http_server- HTTP server process IDnode_history- Configuration change history
- Check if node is permanent (error if so)
- Extract body from request
- Validate required fields present
- Merge with existing config
- Add to node history
- Return updated node message
-module(dev_meta_adopt_test).
-include_lib("eunit/include/eunit.hrl").
adopt_valid_test() ->
% adopt_node_message/2 requires running HTTP server
% Just verify export exists
code:ensure_loaded(dev_meta),
?assert(erlang:function_exported(dev_meta, adopt_node_message, 2)).
adopt_preserves_critical_test() ->
% adopt_node_message/2 requires ranch/cowboy server
Exports = dev_meta:module_info(exports),
?assert(lists:member({adopt_node_message, 2}, Exports)).
adopt_permanent_error_test() ->
NodeMsg = #{ initialized => permanent },
Request = #{ <<"body">> => #{ <<"test">> => <<"value">> } },
Result = dev_meta:adopt_node_message(Request, NodeMsg),
?assertMatch({error, _}, Result).Request/Response Hooks
Hook Configuration
NodeMsg = #{
on => #{
<<"request">> => #{
<<"device">> => #{
<<"request">> => fun(Base, Req, Opts) ->
% Process request
{ok, ModifiedReq}
end
}
},
<<"response">> => #{
<<"device">> => #{
<<"response">> => fun(Base, Res, Opts) ->
% Process response
{ok, ModifiedRes}
end
}
}
}
}Hook Types
Request Hook:- Called before AO-Core resolution
- Can modify request
- Can halt request by returning
{error, Reason}
- Called after AO-Core resolution
- Can modify response
- Cannot halt (error is returned)
-module(dev_meta_hooks_test).
-include_lib("eunit/include/eunit.hrl").
request_hook_test() ->
Parent = self(),
Node = hb_http_server:start_node(#{
on => #{
<<"request">> => #{
<<"device">> => #{
<<"request">> => fun(_, Req, _) ->
Parent ! {hook, request},
{ok, Req}
end
}
}
}
}),
hb_http:get(Node, <<"/~meta@1.0/info">>, #{}),
receive {hook, request} -> ok after 100 -> error(timeout) end.
response_hook_test() ->
Parent = self(),
Node = hb_http_server:start_node(#{
on => #{
<<"response">> => #{
<<"device">> => #{
<<"response">> => fun(_, Res, _) ->
Parent ! {hook, response},
{ok, Res}
end
}
}
}
}),
hb_http:get(Node, <<"/~meta@1.0/info">>, #{}),
receive {hook, response} -> ok after 100 -> error(timeout) end.
halt_request_test() ->
Node = hb_http_server:start_node(#{
on => #{
<<"request">> => #{
<<"device">> => #{
<<"request">> => fun(_, _, _) ->
{error, <<"Blocked">>}
end
}
}
}
}),
{error, Res} = hb_http:get(Node, <<"/~meta@1.0/info">>, #{}),
?assertEqual(<<"Blocked">>, Res).
modify_request_test() ->
Node = hb_http_server:start_node(#{
on => #{
<<"request">> => #{
<<"device">> => #{
<<"request">> => fun(_, #{ <<"body">> := [M|Ms] }, _) ->
{ok, #{
<<"body">> => [M#{ <<"added">> => <<"value">> }|Ms]
}}
end
}
}
}
}),
{ok, Res} = hb_http:get(Node, <<"/added">>, #{}),
?assertEqual(<<"value">>, Res).Node Initialization States
Initialization Values
false- Not initialized (only/~meta@1.0/infoaccessible)true- Initialized (all endpoints accessible)permanent- Permanently initialized (cannot be changed)
State Transitions
false → true → permanent
↑ ↓
←------
(can revert unless permanent)Node Claiming
Unclaimed Node
NodeMsg = #{ operator => unclaimed }- Anyone can claim by setting
operatorfield - First claimer becomes operator
Claiming Process
% Start unclaimed node
Node = hb_http_server:start_node(#{
operator => unclaimed
}),
% Claim node
Owner = ar_wallet:new(),
hb_http:post(
Node,
hb_message:commit(
#{
<<"path">> => <<"/~meta@1.0/info">>,
<<"operator">> => hb_util:human_id(ar_wallet:to_address(Owner))
},
#{ priv_wallet => Owner }
),
#{}
).Dynamic Keys
The following keys are added dynamically to node info responses:
Address Keys
#{
<<"address">> => NodeAddress, % From priv_wallet
address => NodeAddress % Atom key version
}Identity Addresses
#{
<<"identities">> => #{
<<"scheduler-1">> => #{
<<"address">> => Address1,
% ... other fields
}
}
}Common Patterns
%% Start node with configuration
Node = hb_http_server:start_node(#{
priv_wallet => hb:wallet(),
custom_config => <<"value">>,
store => StoreConfig
}).
%% Get node information
{ok, Info} = hb_http:get(Node, <<"/~meta@1.0/info">>, #{}).
%% Update node configuration (requires operator signature)
{ok, _} = hb_http:post(
Node,
hb_message:commit(
#{
<<"path">> => <<"/~meta@1.0/info">>,
<<"new_config">> => <<"value">>
},
#{ priv_wallet => OperatorWallet }
),
#{}
).
%% Get build information
{ok, Version} = hb_http:get(Node, <<"/~meta@1.0/build/version">>, #{}).
{ok, GitHash} = hb_http:get(Node, <<"/~meta@1.0/build/source">>, #{}).
%% Set node as permanent
{ok, _} = hb_http:post(
Node,
hb_message:commit(
#{
<<"path">> => <<"/~meta@1.0/info">>,
initialized => <<"permanent">>
},
#{ priv_wallet => Owner }
),
#{}
).
%% Add request hook
Node = hb_http_server:start_node(#{
on => #{
<<"request">> => #{
<<"device">> => #{
<<"request">> => fun(_, Req, _) ->
% Validate request
{ok, Req}
end
}
}
}
}).
%% Claim unclaimed node
Node = hb_http_server:start_node(#{ operator => unclaimed }),
Owner = ar_wallet:new(),
hb_http:post(
Node,
hb_message:commit(
#{
<<"path">> => <<"/~meta@1.0/info">>,
<<"operator">> => hb_util:human_id(ar_wallet:to_address(Owner))
},
#{ priv_wallet => Owner }
),
#{}
).HTTP Extra Options
Additional HTTP response headers can be configured:
NodeMsg = #{
http_extra_opts => #{
<<"cache-control">> => [<<"no-store">>, <<"no-cache">>],
<<"x-custom-header">> => <<"value">>
}
}References
- hb_http_server.erl - HTTP server implementation
- hb_http.erl - HTTP client
- hb_ao.erl - AO-Core resolution
- hb_message.erl - Message operations
- hb_singleton.erl - Singleton request parsing
- ar_wallet.erl - Wallet operations
Notes
- Entry Point: Primary interface for all HyperBEAM requests
- Initialization: Enforces initialization before allowing access
- Authorization: Only operator can modify configuration
- Permanent State: Once permanent, configuration is immutable
- Hook System: Flexible request/response preprocessing
- Node History: Tracks all configuration changes
- Protected Fields: Critical fields cannot be overridden
- Dynamic Keys: Address fields added at runtime
- Unclaimed Nodes: First claimer becomes operator
- Build Info: Version and git information available
- Private Filtering: Filters out private keys in responses
- Status Embedding: Automatically adds status codes
- Singleton Support: Normalizes various request formats
- Event Logging: Comprehensive event tracking
- HTTP Integration: Designed for HTTP server operation