-module(glupbit@auth). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/glupbit/auth.gleam"). -export([access_key/1, secret_key/1, credentials/2, credentials_from_env/0, sha512_hex/1, generate_token/1, generate_token_with_query/2, generate_token_with_body/2]). -export_type([access_key/0, secret_key/0, credentials/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " JWT HS512 authentication for Upbit Exchange API.\n" "\n" " Generates signed tokens with `access_key`, `nonce`, and optional\n" " `query_hash` / `query_hash_alg` claims using the **gose** JOSE library.\n" ). -opaque access_key() :: {access_key, binary()}. -opaque secret_key() :: {secret_key, binary()}. -opaque credentials() :: {credentials, access_key(), secret_key()}. -file("src/glupbit/auth.gleam", 41). ?DOC(" Wrap a raw string as an AccessKey.\n"). -spec access_key(binary()) -> access_key(). access_key(Key) -> {access_key, Key}. -file("src/glupbit/auth.gleam", 46). ?DOC(" Wrap a raw string as a SecretKey.\n"). -spec secret_key(binary()) -> secret_key(). secret_key(Key) -> {secret_key, Key}. -file("src/glupbit/auth.gleam", 51). ?DOC(" Bundle an access key and secret key into Credentials.\n"). -spec credentials(access_key(), secret_key()) -> credentials(). credentials(Ak, Sk) -> {credentials, Ak, Sk}. -file("src/glupbit/auth.gleam", 57). ?DOC( " Load credentials from `UPBIT_OPEN_API_ACCESS_KEY` and\n" " `UPBIT_OPEN_API_SECRET_KEY` environment variables.\n" ). -spec credentials_from_env() -> {ok, credentials()} | {error, binary()}. credentials_from_env() -> gleam@result:'try'( begin _pipe = envoy_ffi:get(<<"UPBIT_OPEN_API_ACCESS_KEY"/utf8>>), gleam@result:replace_error( _pipe, <<"UPBIT_OPEN_API_ACCESS_KEY not set"/utf8>> ) end, fun(Ak) -> gleam@result:'try'( begin _pipe@1 = envoy_ffi:get( <<"UPBIT_OPEN_API_SECRET_KEY"/utf8>> ), gleam@result:replace_error( _pipe@1, <<"UPBIT_OPEN_API_SECRET_KEY not set"/utf8>> ) end, fun(Sk) -> {ok, credentials({access_key, Ak}, {secret_key, Sk})} end ) end ). -file("src/glupbit/auth.gleam", 101). ?DOC(" Compute the lowercase-hex SHA-512 digest of a string.\n"). -spec sha512_hex(binary()) -> binary(). sha512_hex(Input) -> _pipe = Input, _pipe@1 = gleam_stdlib:identity(_pipe), _pipe@2 = gleam@crypto:hash(sha512, _pipe@1), _pipe@3 = gleam_stdlib:base16_encode(_pipe@2), string:lowercase(_pipe@3). -file("src/glupbit/auth.gleam", 109). -spec query_hash_claims(binary()) -> list({binary(), gleam@json:json()}). query_hash_claims(Query_string) -> [{<<"query_hash"/utf8>>, gleam@json:string(sha512_hex(Query_string))}, {<<"query_hash_alg"/utf8>>, gleam@json:string(<<"SHA512"/utf8>>)}]. -file("src/glupbit/auth.gleam", 153). -spec jwt_error_to_api_error(gose@jwt:jwt_error()) -> glupbit@types:api_error(). jwt_error_to_api_error(Err) -> {auth_error, <<"JWT: "/utf8, (gleam@string:inspect(Err))/binary>>}. -file("src/glupbit/auth.gleam", 117). ?DOC(" Build, sign and serialize a JWT with HS512.\n"). -spec sign_jwt(credentials(), list({binary(), gleam@json:json()})) -> {ok, binary()} | {error, glupbit@types:api_error()}. sign_jwt(Creds, Extra_claims) -> {credentials, {access_key, Ak}, {secret_key, Sk}} = Creds, gleam@result:'try'( begin _pipe = Sk, _pipe@1 = gleam_stdlib:identity(_pipe), _pipe@2 = gose@jwk:from_octet_bits(_pipe@1), gleam@result:map_error( _pipe@2, fun(E) -> {auth_error, <<"JWK: "/utf8, (gose:error_message(E))/binary>>} end ) end, fun(Key) -> Base = [{<<"access_key"/utf8>>, gleam@json:string(Ak)}, {<<"nonce"/utf8>>, gleam@json:string(youid@uuid:v4_string())}], gleam@result:'try'( begin _pipe@3 = lists:append(Base, Extra_claims), gleam@list:try_fold( _pipe@3, gose@jwt:claims(), fun(Claims, Pair) -> _pipe@4 = gose@jwt:with_claim( Claims, erlang:element(1, Pair), erlang:element(2, Pair) ), gleam@result:map_error( _pipe@4, fun jwt_error_to_api_error/1 ) end ) end, fun(Claims@1) -> gleam@result:'try'( begin _pipe@5 = gose@jwt:sign( {jws_hmac, hmac_sha256}, Claims@1, Key ), gleam@result:map_error( _pipe@5, fun jwt_error_to_api_error/1 ) end, fun(Signed) -> {ok, gose@jwt:serialize(Signed)} end ) end ) end ). -file("src/glupbit/auth.gleam", 72). ?DOC(" Generate a JWT for requests **without** parameters.\n"). -spec generate_token(credentials()) -> {ok, binary()} | {error, glupbit@types:api_error()}. generate_token(Creds) -> sign_jwt(Creds, []). -file("src/glupbit/auth.gleam", 78). ?DOC( " Generate a JWT for GET/DELETE requests **with** query parameters.\n" " The raw `query_string` is SHA-512 hashed and included as `query_hash`.\n" ). -spec generate_token_with_query(credentials(), binary()) -> {ok, binary()} | {error, glupbit@types:api_error()}. generate_token_with_query(Creds, Query_string) -> sign_jwt(Creds, query_hash_claims(Query_string)). -file("src/glupbit/auth.gleam", 87). ?DOC( " Generate a JWT for POST requests with a JSON body.\n" " Body params are converted to `key=value&...` format before hashing.\n" ). -spec generate_token_with_body(credentials(), list({binary(), binary()})) -> {ok, binary()} | {error, glupbit@types:api_error()}. generate_token_with_body(Creds, Params) -> Query_string = begin _pipe = Params, _pipe@1 = gleam@list:map( _pipe, fun(Pair) -> <<<<(erlang:element(1, Pair))/binary, "="/utf8>>/binary, (erlang:element(2, Pair))/binary>> end ), gleam@string:join(_pipe@1, <<"&"/utf8>>) end, sign_jwt(Creds, query_hash_claims(Query_string)).