Packages
macula
4.5.0
7.1.0
7.0.0
6.0.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.8.0
4.7.1
4.7.0
4.6.0
4.5.0
4.4.10
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.1
4.3.0
4.2.9
4.2.8
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.1
4.1.0
4.0.0
3.16.0
3.15.3
3.15.2
3.15.1
3.14.0
3.13.0
3.12.1
3.12.0
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.9.0
3.8.0
3.7.0
3.5.0
3.4.0
3.3.0
3.2.0
3.1.0
3.0.0
2.1.1
2.1.0
2.0.0
1.5.2
1.5.1
1.4.30
1.4.29
1.4.28
1.4.27
1.4.26
1.4.25
1.4.24
1.4.23
1.4.22
1.4.21
1.4.20
1.4.19
1.4.18
1.4.17
1.4.16
1.4.15
1.4.14
1.4.13
1.4.11
1.4.10
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.1
1.3.0
1.2.0
1.1.0
1.0.10
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.48.6
0.48.5
0.48.4
0.48.3
0.48.2
0.48.1
0.48.0
0.47.1
0.47.0
0.46.3
0.46.1
0.46.0
0.45.3
0.45.2
0.45.1
0.45.0
0.44.2
0.44.1
0.44.0
0.43.3
0.43.2
0.43.1
0.43.0
0.42.9
0.42.8
0.42.7
0.42.6
0.42.5
0.42.4
0.42.3
0.42.2
0.42.1
0.42.0
0.41.1
0.41.0
0.40.1
0.40.0
0.39.9
0.39.8
0.39.7
0.39.6
0.39.5
0.39.4
0.39.3
0.39.2
0.39.1
0.39.0
0.38.8
0.38.7
0.38.6
0.38.5
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.7
0.37.6
0.37.5
0.37.4
0.37.3
0.37.2
0.37.1
0.37.0
0.36.6
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.4
0.35.3
0.35.2
0.35.1
0.35.0
0.34.1
0.34.0
0.33.1
0.33.0
0.32.5
0.32.4
0.32.3
0.32.2
0.32.1
0.32.0
0.31.9
0.31.8
0.31.7
0.31.6
0.31.5
0.31.4
0.31.3
0.31.2
0.31.1
0.31.0
0.30.10
0.30.9
0.30.8
0.30.7
0.30.6
0.30.5
0.30.4
0.30.3
0.30.2
0.30.1
0.30.0
0.29.0
0.28.3
0.28.2
0.28.1
0.28.0
0.27.1
0.27.0
0.26.1
0.26.0
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.6
0.24.5
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.7
0.21.6
0.21.5
0.21.4
0.21.2
0.21.1
0.21.0
0.20.25
0.20.24
0.20.23
0.20.22
0.20.21
0.20.20
0.20.19
0.20.18
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.3
0.20.2
0.20.1
0.20.0
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.12.6
0.12.5
0.12.3
0.11.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.30
0.7.29
0.7.28
0.7.27
0.7.26
0.7.25
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.4
0.3.3
0.3.2
0.3.1
Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise
Current section
Files
Jump to
Current section
Files
src/auth/macula_ucan_nif.erl
%% @doc UCAN (User Controlled Authorization Networks) token operations.
%%
%% This module provides creation, verification, and manipulation of UCAN tokens
%% for decentralized authorization in the Macula mesh. UCANs are self-contained
%% capability tokens that support delegation.
%%
%% == Token Structure ==
%%
%% UCAN tokens follow JWT format: header.payload.signature
%%
%% Header:
%% - alg: "EdDSA" (Ed25519)
%% - typ: "JWT"
%% - ucv: "0.10.0" (UCAN version)
%%
%% Payload:
%% - iss: Issuer DID (did:macula:io.macula.org)
%% - aud: Audience DID
%% - cap: Capabilities [{with, can}, ...]
%% - exp: Expiration (optional, unix timestamp)
%% - nbf: Not before (optional, unix timestamp)
%% - nnc: Nonce (optional, for uniqueness)
%% - fct: Facts (optional, metadata)
%% - prf: Proof chain (CIDs of parent tokens)
%%
%% @author rgfaber
-module(macula_ucan_nif).
%% API
-export([
create/4,
create/5,
verify/2,
decode/1,
compute_cid/1,
get_issuer/1,
get_audience/1,
get_capabilities/1,
get_expiration/1,
get_proofs/1,
is_expired/1,
is_nif_loaded/0
]).
%% NIF stubs
-export([
nif_create/5,
nif_verify/2,
nif_decode/1,
nif_compute_cid/1,
nif_get_issuer/1,
nif_get_audience/1
]).
-on_load(init/0).
-define(NIF_LOADED_KEY, macula_ucan_nif_loaded).
%%====================================================================
%% Types
%%====================================================================
-type did() :: binary().
-type capability() :: #{with := binary(), can := binary()}.
-type ucan_opts() :: #{
exp => non_neg_integer(),
nbf => non_neg_integer(),
nnc => binary(),
fct => map(),
prf => [binary()]
}.
-export_type([did/0, capability/0, ucan_opts/0]).
%%====================================================================
%% Init
%%====================================================================
init() ->
PrivDir = case code:priv_dir(macula) of
{error, _} ->
case code:which(?MODULE) of
Filename when is_list(Filename) ->
filename:join(filename:dirname(filename:dirname(Filename)), "priv");
_ ->
"priv"
end;
Dir ->
Dir
end,
Path = filename:join(PrivDir, "macula_ucan_nif"),
case erlang:load_nif(Path, 0) of
ok ->
persistent_term:put(?NIF_LOADED_KEY, true),
ok;
{error, {reload, _}} ->
persistent_term:put(?NIF_LOADED_KEY, true),
ok;
{error, _Reason} ->
ok
end.
%%====================================================================
%% API
%%====================================================================
%% @doc Check if the NIF is loaded.
-spec is_nif_loaded() -> boolean().
is_nif_loaded() ->
persistent_term:get(?NIF_LOADED_KEY, false).
%% @doc Create a new UCAN token.
%% @equiv create(Issuer, Audience, Capabilities, PrivateKey, #{})
-spec create(Issuer :: did(), Audience :: did(), Capabilities :: [capability()],
PrivateKey :: binary()) ->
{ok, Token :: binary()} | {error, term()}.
create(Issuer, Audience, Capabilities, PrivateKey) ->
create(Issuer, Audience, Capabilities, PrivateKey, #{}).
%% @doc Create a new UCAN token with options.
%%
%% Options:
%% - exp: Expiration timestamp (unix seconds)
%% - nbf: Not before timestamp (unix seconds)
%% - nnc: Nonce (for uniqueness)
%% - fct: Facts map (metadata)
%% - prf: Proof chain (list of CIDs of parent tokens)
-spec create(Issuer :: did(), Audience :: did(), Capabilities :: [capability()],
PrivateKey :: binary(), Opts :: ucan_opts()) ->
{ok, Token :: binary()} | {error, term()}.
create(Issuer, Audience, Capabilities, PrivateKey, Opts) ->
case is_nif_loaded() of
true ->
CapsJson = encode_json(Capabilities),
OptsJson = encode_json(Opts),
case nif_create(Issuer, Audience, CapsJson, PrivateKey, OptsJson) of
{ok, Token} -> {ok, Token};
{invalid_private_key, _} -> {error, invalid_private_key};
{malformed_json, _} -> {error, malformed_json}
end;
false ->
erlang_create(Issuer, Audience, Capabilities, PrivateKey, Opts)
end.
%% @doc Verify a UCAN token.
%% Checks signature, expiration, and not-before.
%% Returns the decoded payload on success.
-spec verify(Token :: binary(), PublicKey :: binary()) ->
{ok, Payload :: map()} | {error, term()}.
verify(Token, PublicKey) ->
case is_nif_loaded() of
true ->
case nif_verify(Token, PublicKey) of
{ok, PayloadJson} -> {ok, decode_json(PayloadJson)};
{invalid_token, _} -> {error, invalid_token};
{invalid_signature, _} -> {error, invalid_signature};
{invalid_public_key, _} -> {error, invalid_public_key};
{expired, _} -> {error, expired};
{not_yet_valid, _} -> {error, not_yet_valid}
end;
false ->
erlang_verify(Token, PublicKey)
end.
%% @doc Decode a UCAN token without verification.
%% WARNING: This does NOT verify the signature!
-spec decode(Token :: binary()) -> {ok, Payload :: map()} | {error, term()}.
decode(Token) ->
case is_nif_loaded() of
true ->
case nif_decode(Token) of
{ok, PayloadJson} -> {ok, decode_json(PayloadJson)};
{invalid_token, _} -> {error, invalid_token}
end;
false ->
erlang_decode(Token)
end.
%% @doc Compute the CID (Content ID) of a UCAN token.
%% Used for proof chains.
-spec compute_cid(Token :: binary()) -> binary().
compute_cid(Token) ->
case is_nif_loaded() of
true -> nif_compute_cid(Token);
false -> erlang_compute_cid(Token)
end.
%% @doc Get the issuer DID from a UCAN token.
-spec get_issuer(Token :: binary()) -> {ok, did()} | {error, term()}.
get_issuer(Token) ->
case is_nif_loaded() of
true ->
case nif_get_issuer(Token) of
{ok, Issuer} -> {ok, Issuer};
{invalid_token, _} -> {error, invalid_token}
end;
false -> erlang_get_field(Token, <<"iss">>)
end.
%% @doc Get the audience DID from a UCAN token.
-spec get_audience(Token :: binary()) -> {ok, did()} | {error, term()}.
get_audience(Token) ->
case is_nif_loaded() of
true ->
case nif_get_audience(Token) of
{ok, Audience} -> {ok, Audience};
{invalid_token, _} -> {error, invalid_token}
end;
false -> erlang_get_field(Token, <<"aud">>)
end.
%% @doc Get capabilities from a UCAN token.
-spec get_capabilities(Token :: binary()) -> {ok, [capability()]} | {error, term()}.
get_capabilities(Token) ->
erlang_get_field(Token, <<"cap">>).
%% @doc Get expiration timestamp from a UCAN token.
-spec get_expiration(Token :: binary()) -> {ok, non_neg_integer() | null} | {error, term()}.
get_expiration(Token) ->
erlang_get_field(Token, <<"exp">>).
%% @doc Get proof chain from a UCAN token.
-spec get_proofs(Token :: binary()) -> {ok, [binary()]} | {error, term()}.
get_proofs(Token) ->
erlang_get_field(Token, <<"prf">>).
%% @doc Check if a UCAN token is expired.
-spec is_expired(Token :: binary()) -> boolean() | {error, term()}.
is_expired(Token) ->
case get_expiration(Token) of
{ok, null} -> false;
{ok, Exp} ->
Now = erlang:system_time(second),
Now > Exp;
Error -> Error
end.
%%====================================================================
%% NIF Stubs
%%====================================================================
nif_create(_Issuer, _Audience, _CapsJson, _PrivateKey, _OptsJson) ->
erlang:nif_error(nif_not_loaded).
nif_verify(_Token, _PublicKey) ->
erlang:nif_error(nif_not_loaded).
nif_decode(_Token) ->
erlang:nif_error(nif_not_loaded).
nif_compute_cid(_Token) ->
erlang:nif_error(nif_not_loaded).
nif_get_issuer(_Token) ->
erlang:nif_error(nif_not_loaded).
nif_get_audience(_Token) ->
erlang:nif_error(nif_not_loaded).
%%====================================================================
%% Pure Erlang Fallbacks
%%====================================================================
%% @private
erlang_create(Issuer, Audience, Capabilities, PrivateKey, Opts) when byte_size(PrivateKey) =:= 32 ->
%% Build header
Header = #{
<<"alg">> => <<"EdDSA">>,
<<"typ">> => <<"JWT">>,
<<"ucv">> => <<"0.10.0">>
},
%% Build payload
Payload0 = #{
<<"iss">> => Issuer,
<<"aud">> => Audience,
<<"cap">> => Capabilities,
<<"prf">> => maps:get(prf, Opts, [])
},
%% Add optional fields
Payload1 = maybe_add(<<"exp">>, exp, Opts, Payload0),
Payload2 = maybe_add(<<"nbf">>, nbf, Opts, Payload1),
Payload3 = maybe_add(<<"nnc">>, nnc, Opts, Payload2),
Payload = maybe_add(<<"fct">>, fct, Opts, Payload3),
%% Encode
HeaderB64 = base64_url_encode(encode_json(Header)),
PayloadB64 = base64_url_encode(encode_json(Payload)),
%% Sign
SigningInput = <<HeaderB64/binary, ".", PayloadB64/binary>>,
{ok, Signature} = macula_crypto_nif:sign(SigningInput, PrivateKey),
SignatureB64 = base64_url_encode(Signature),
%% Combine
Token = <<HeaderB64/binary, ".", PayloadB64/binary, ".", SignatureB64/binary>>,
{ok, Token};
erlang_create(_Issuer, _Audience, _Capabilities, _PrivateKey, _Opts) ->
{error, invalid_private_key}.
%% @private
erlang_verify(Token, PublicKey) when byte_size(PublicKey) =:= 32 ->
case split_token(Token) of
{ok, HeaderB64, PayloadB64, SignatureB64} ->
%% Decode payload for checks
case base64_url_decode(PayloadB64) of
{ok, PayloadJson} ->
Payload = decode_json(PayloadJson),
Now = erlang:system_time(second),
%% Check expiration
case check_expiration(Payload, Now) of
ok ->
%% Check not-before
case check_not_before(Payload, Now) of
ok ->
%% Verify signature
verify_signature(HeaderB64, PayloadB64, SignatureB64, PublicKey, Payload);
Error -> Error
end;
Error -> Error
end;
_ -> {error, invalid_token}
end;
_ -> {error, invalid_token}
end;
erlang_verify(_Token, _PublicKey) ->
{error, invalid_public_key}.
%% @private Check token expiration
check_expiration(Payload, Now) ->
case maps:get(<<"exp">>, Payload, null) of
null -> ok;
Exp when is_integer(Exp), Exp >= Now -> ok;
_ -> {error, expired}
end.
%% @private Check not-before
check_not_before(Payload, Now) ->
case maps:get(<<"nbf">>, Payload, null) of
null -> ok;
Nbf when is_integer(Nbf), Nbf =< Now -> ok;
_ -> {error, not_yet_valid}
end.
%% @private Verify signature
verify_signature(HeaderB64, PayloadB64, SignatureB64, PublicKey, Payload) ->
SigningInput = <<HeaderB64/binary, ".", PayloadB64/binary>>,
case base64_url_decode(SignatureB64) of
{ok, Signature} ->
case macula_crypto_nif:verify(SigningInput, Signature, PublicKey) of
true -> {ok, Payload};
false -> {error, invalid_signature}
end;
_ -> {error, invalid_signature}
end.
%% @private
erlang_decode(Token) ->
case split_token(Token) of
{ok, _HeaderB64, PayloadB64, _SignatureB64} ->
case base64_url_decode(PayloadB64) of
{ok, PayloadJson} -> {ok, decode_json(PayloadJson)};
_ -> {error, invalid_token}
end;
_ -> {error, invalid_token}
end.
%% @private
erlang_compute_cid(Token) ->
Hash = crypto:hash(sha256, Token),
base64_url_encode(Hash).
%% @private
erlang_get_field(Token, Field) ->
case erlang_decode(Token) of
{ok, Payload} ->
case maps:find(Field, Payload) of
{ok, Value} -> {ok, Value};
error -> {ok, null}
end;
Error -> Error
end.
%%====================================================================
%% Helpers
%%====================================================================
%% @private
split_token(Token) ->
case binary:split(Token, <<".">>, [global]) of
[Header, Payload, Signature] -> {ok, Header, Payload, Signature};
_ -> {error, invalid_token}
end.
%% @private
maybe_add(JsonKey, OptKey, Opts, Map) ->
case maps:find(OptKey, Opts) of
{ok, Value} -> maps:put(JsonKey, Value, Map);
error -> Map
end.
%% @private URL-safe base64 encode (no padding)
base64_url_encode(Data) when is_binary(Data) ->
B64 = base64:encode(Data),
B64_Url = binary:replace(binary:replace(B64, <<"+">>, <<"-">>, [global]), <<"/">>, <<"_">>, [global]),
binary:replace(B64_Url, <<"=">>, <<>>, [global]).
%% @private URL-safe base64 decode
base64_url_decode(Encoded) ->
try
B64_Std = binary:replace(binary:replace(Encoded, <<"-">>, <<"+">>, [global]), <<"_">>, <<"/">>, [global]),
Padded = case byte_size(B64_Std) rem 4 of
0 -> B64_Std;
2 -> <<B64_Std/binary, "==">>;
3 -> <<B64_Std/binary, "=">>
end,
{ok, base64:decode(Padded)}
catch
_:_ -> {error, invalid_base64}
end.
%% @private JSON encoding using term_to_binary for maps (simple implementation)
%% In production, use jsx or jiffy
encode_json(Term) ->
encode_json_term(Term).
encode_json_term(Map) when is_map(Map) ->
Pairs = maps:fold(fun(K, V, Acc) ->
Key = if is_atom(K) -> atom_to_binary(K); true -> K end,
[encode_json_pair(Key, V) | Acc]
end, [], Map),
<<"{", (iolist_to_binary(lists:join(<<",">>, Pairs)))/binary, "}">>;
encode_json_term(List) when is_list(List) ->
Items = [encode_json_term(I) || I <- List],
<<"[", (iolist_to_binary(lists:join(<<",">>, Items)))/binary, "]">>;
encode_json_term(Bin) when is_binary(Bin) ->
%% Simple JSON string encoding (escape quotes)
Escaped = binary:replace(Bin, <<"\"">>, <<"\\\"">>, [global]),
<<"\"", Escaped/binary, "\"">>;
encode_json_term(Num) when is_integer(Num) ->
integer_to_binary(Num);
encode_json_term(true) -> <<"true">>;
encode_json_term(false) -> <<"false">>;
encode_json_term(null) -> <<"null">>;
encode_json_term(Atom) when is_atom(Atom) ->
encode_json_term(atom_to_binary(Atom)).
encode_json_pair(Key, Value) ->
<<(encode_json_term(Key))/binary, ":", (encode_json_term(Value))/binary>>.
%% @private Simple JSON decoding
decode_json(Bin) ->
%% Very basic JSON parsing - use jsx or jiffy in production
try
{ok, Tokens, _} = json_tokenize(Bin),
{Value, []} = json_parse(Tokens),
Value
catch
_:_ -> #{}
end.
%% Minimal JSON tokenizer
json_tokenize(Bin) ->
json_tokenize(Bin, []).
json_tokenize(<<>>, Acc) ->
{ok, lists:reverse(Acc), <<>>};
json_tokenize(<<C, Rest/binary>>, Acc) when C =:= $\s; C =:= $\t; C =:= $\n; C =:= $\r ->
json_tokenize(Rest, Acc);
json_tokenize(<<"{", Rest/binary>>, Acc) ->
json_tokenize(Rest, ['{' | Acc]);
json_tokenize(<<"}", Rest/binary>>, Acc) ->
json_tokenize(Rest, ['}' | Acc]);
json_tokenize(<<"[", Rest/binary>>, Acc) ->
json_tokenize(Rest, ['[' | Acc]);
json_tokenize(<<"]", Rest/binary>>, Acc) ->
json_tokenize(Rest, [']' | Acc]);
json_tokenize(<<",", Rest/binary>>, Acc) ->
json_tokenize(Rest, [',' | Acc]);
json_tokenize(<<":", Rest/binary>>, Acc) ->
json_tokenize(Rest, [':' | Acc]);
json_tokenize(<<"\"", Rest/binary>>, Acc) ->
{Str, Rest2} = json_string(Rest, <<>>),
json_tokenize(Rest2, [{string, Str} | Acc]);
json_tokenize(<<"true", Rest/binary>>, Acc) ->
json_tokenize(Rest, [true | Acc]);
json_tokenize(<<"false", Rest/binary>>, Acc) ->
json_tokenize(Rest, [false | Acc]);
json_tokenize(<<"null", Rest/binary>>, Acc) ->
json_tokenize(Rest, [null | Acc]);
json_tokenize(<<C, _/binary>> = Bin, Acc) when C >= $0, C =< $9; C =:= $- ->
{Num, Rest} = json_number(Bin, <<>>),
json_tokenize(Rest, [{number, Num} | Acc]).
json_string(<<"\\\"", Rest/binary>>, Acc) ->
json_string(Rest, <<Acc/binary, "\"">>);
json_string(<<"\\\\", Rest/binary>>, Acc) ->
json_string(Rest, <<Acc/binary, "\\">>);
json_string(<<"\"", Rest/binary>>, Acc) ->
{Acc, Rest};
json_string(<<C, Rest/binary>>, Acc) ->
json_string(Rest, <<Acc/binary, C>>).
json_number(<<C, Rest/binary>>, Acc) when C >= $0, C =< $9; C =:= $-; C =:= $.; C =:= $e; C =:= $E; C =:= $+ ->
json_number(Rest, <<Acc/binary, C>>);
json_number(Rest, Acc) ->
Num = case binary:match(Acc, <<".">>) of
nomatch -> binary_to_integer(Acc);
_ -> binary_to_float(Acc)
end,
{Num, Rest}.
json_parse(['{' | Rest]) ->
json_parse_object(Rest, #{});
json_parse(['[' | Rest]) ->
json_parse_array(Rest, []);
json_parse([{string, S} | Rest]) ->
{S, Rest};
json_parse([{number, N} | Rest]) ->
{N, Rest};
json_parse([true | Rest]) ->
{true, Rest};
json_parse([false | Rest]) ->
{false, Rest};
json_parse([null | Rest]) ->
{null, Rest}.
json_parse_object(['}' | Rest], Acc) ->
{Acc, Rest};
json_parse_object([{string, Key}, ':' | Rest], Acc) ->
{Value, Rest2} = json_parse(Rest),
case Rest2 of
[',' | Rest3] -> json_parse_object(Rest3, maps:put(Key, Value, Acc));
['}' | Rest3] -> {maps:put(Key, Value, Acc), Rest3}
end.
json_parse_array([']' | Rest], Acc) ->
{lists:reverse(Acc), Rest};
json_parse_array(Tokens, Acc) ->
{Value, Rest} = json_parse(Tokens),
case Rest of
[',' | Rest2] -> json_parse_array(Rest2, [Value | Acc]);
[']' | Rest2] -> {lists:reverse([Value | Acc]), Rest2}
end.