From 49fa5aa2a127bdf8924d02bf77e5086b39c7a447 Mon Sep 17 00:00:00 2001 From: Calvin Morrison Date: Wed, 3 Sep 2025 21:15:36 -0400 Subject: i vibe coded it --- server/src/jchat_utils.erl | 163 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 server/src/jchat_utils.erl (limited to 'server/src/jchat_utils.erl') diff --git a/server/src/jchat_utils.erl b/server/src/jchat_utils.erl new file mode 100644 index 0000000..3f94cfb --- /dev/null +++ b/server/src/jchat_utils.erl @@ -0,0 +1,163 @@ +-module(jchat_utils). + +-export([now_iso8601/0, + generate_id/0, + validate_id/1, + json_encode/1, + json_decode/1, + format_error/1, + validate_account_id/1, + extract_auth_token/1, + validate_method_call/1, + format_method_response/3, + format_error_response/3]). + +%% Generate ISO8601 timestamp +now_iso8601() -> + {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:universal_time(), + iolist_to_binary(io_lib:format("~4..0w-~2..0w-~2..0wT~2..0w:~2..0w:~2..0wZ", + [Year, Month, Day, Hour, Minute, Second])). + +%% Generate a new ID (UUID v4) +generate_id() -> + <> = crypto:strong_rand_bytes(16), + iolist_to_binary(io_lib:format("~8.16.0b-~4.16.0b-4~3.16.0b-~4.16.0b-~12.16.0b", + [U0, U1, U2, ((U3 bsr 60) band 16#03) bor 16#08, U3 band 16#0fffffffffff])). + +%% Validate ID format (URL-safe base64 characters) +validate_id(Id) when is_binary(Id) -> + Size = byte_size(Id), + case Size >= 1 andalso Size =< 255 of + true -> + validate_id_chars(Id); + false -> + false + end; +validate_id(_) -> + false. + +validate_id_chars(<<>>) -> + true; +validate_id_chars(<>) -> + case is_valid_id_char(C) of + true -> validate_id_chars(Rest); + false -> false + end. + +is_valid_id_char(C) -> + (C >= $A andalso C =< $Z) orelse + (C >= $a andalso C =< $z) orelse + (C >= $0 andalso C =< $9) orelse + C =:= $- orelse C =:= $_. + +%% JSON encoding/decoding +json_encode(Term) -> + jsx:encode(Term). + +json_decode(JSON) -> + try + {ok, jsx:decode(JSON, [return_maps])} + catch + _:_ -> {error, invalid_json} + end. + +%% Format errors for JMAP responses (RFC 8620 compliant) +format_error(not_found) -> + #{type => <<"notFound">>}; +format_error(invalid_arguments) -> + #{type => <<"invalidArguments">>}; +format_error(account_not_found) -> + #{type => <<"accountNotFound">>}; +format_error(forbidden) -> + #{type => <<"forbidden">>}; +format_error(invalid_result_reference) -> + #{type => <<"invalidResultReference">>}; +format_error(anchor_not_found) -> + #{type => <<"anchorNotFound">>}; +format_error(unsupported_sort) -> + #{type => <<"unsupportedSort">>}; +format_error(unsupported_filter) -> + #{type => <<"unsupportedFilter">>}; +format_error(cannot_calculate_changes) -> + #{type => <<"cannotCalculateChanges">>}; +format_error(too_large) -> + #{type => <<"tooLarge">>}; +format_error(too_many_changes) -> + #{type => <<"tooManyChanges">>}; +format_error(rate_limited) -> + #{type => <<"rateLimited">>}; +format_error(request_too_large) -> + #{type => <<"requestTooLarge">>}; +format_error(limit_exceeded) -> + #{type => <<"limitExceeded">>}; +format_error(state_mismatch) -> + #{type => <<"stateMismatch">>}; +format_error(will_destroy) -> + #{type => <<"willDestroy">>}; +format_error({invalid_arguments, Description}) -> + #{type => <<"invalidArguments">>, description => Description}; +format_error({not_found, Description}) -> + #{type => <<"notFound">>, description => Description}; +format_error({forbidden, Description}) -> + #{type => <<"forbidden">>, description => Description}; +format_error({server_fail, Description}) -> + #{type => <<"serverFail">>, description => Description}; +format_error(Error) -> + #{type => <<"serverFail">>, description => iolist_to_binary(io_lib:format("~p", [Error]))}. + +%% JMAP-specific utility functions + +%% Validate account ID format (as per JMAP spec) +validate_account_id(AccountId) when is_binary(AccountId) -> + validate_id(AccountId); +validate_account_id(_) -> + false. + +%% Extract Bearer token from Authorization header +extract_auth_token(undefined) -> + {error, no_auth_header}; +extract_auth_token(<<"Bearer ", Token/binary>>) -> + {ok, Token}; +extract_auth_token(_) -> + {error, invalid_auth_format}. + +%% Validate JMAP method call structure +validate_method_call([Method, Args, CallId]) + when is_binary(Method), is_map(Args), is_binary(CallId) -> + case validate_method_name(Method) of + true -> ok; + false -> {error, invalid_method_name} + end; +validate_method_call(_) -> + {error, invalid_method_call_structure}. + +validate_method_name(Method) -> + case binary:split(Method, <<"/">>) of + [Type, Operation] when byte_size(Type) > 0, byte_size(Operation) > 0 -> + validate_method_chars(Method); + _ -> + false + end. + +validate_method_chars(<<>>) -> + true; +validate_method_chars(<>) -> + case is_valid_method_char(C) of + true -> validate_method_chars(Rest); + false -> false + end. + +is_valid_method_char(C) -> + (C >= $A andalso C =< $Z) orelse + (C >= $a andalso C =< $z) orelse + (C >= $0 andalso C =< $9) orelse + C =:= $/ orelse C =:= $_. + +%% Format successful method response +format_method_response(Method, Result, CallId) -> + [Method, Result, CallId]. + +%% Format error method response +format_error_response(Error, CallId, Method) -> + ErrorMap = format_error(Error), + [<<"error">>, ErrorMap#{<<"description">> => <<"Error in method: ", Method/binary>>}, CallId]. -- cgit v1.2.3