Packages

Sans-IO OpenAI API client for Gleam, ported from async-openai

Current section

Files

Jump to
glopenai src glopenai@webhook.erl
Raw

src/glopenai@webhook.erl

-module(glopenai@webhook).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/glopenai/webhook.gleam").
-export([webhook_event_decoder/0, created_at/1, event_type/1, parse_event/1, verify_signature_with_tolerance/7, build_event_with_tolerance/7, verify_signature/6, build_event/6]).
-export_type([webhook_error/0, webhook_batch_data/0, webhook_eval_run_data/0, webhook_fine_tuning_job_data/0, webhook_response_data/0, sip_header/0, webhook_realtime_call_data/0, webhook_event/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.
-type webhook_error() :: invalid_signature |
{invalid, binary()} |
{deserialization, binary(), gleam@json:decode_error()}.
-type webhook_batch_data() :: {webhook_batch_data, binary()}.
-type webhook_eval_run_data() :: {webhook_eval_run_data, binary()}.
-type webhook_fine_tuning_job_data() :: {webhook_fine_tuning_job_data, binary()}.
-type webhook_response_data() :: {webhook_response_data, binary()}.
-type sip_header() :: {sip_header, binary(), binary()}.
-type webhook_realtime_call_data() :: {webhook_realtime_call_data,
binary(),
list(sip_header())}.
-type webhook_event() :: {batch_cancelled,
integer(),
binary(),
gleam@option:option(binary()),
webhook_batch_data()} |
{batch_completed,
integer(),
binary(),
gleam@option:option(binary()),
webhook_batch_data()} |
{batch_expired,
integer(),
binary(),
gleam@option:option(binary()),
webhook_batch_data()} |
{batch_failed,
integer(),
binary(),
gleam@option:option(binary()),
webhook_batch_data()} |
{eval_run_canceled,
integer(),
binary(),
gleam@option:option(binary()),
webhook_eval_run_data()} |
{eval_run_failed,
integer(),
binary(),
gleam@option:option(binary()),
webhook_eval_run_data()} |
{eval_run_succeeded,
integer(),
binary(),
gleam@option:option(binary()),
webhook_eval_run_data()} |
{fine_tuning_job_cancelled,
integer(),
binary(),
gleam@option:option(binary()),
webhook_fine_tuning_job_data()} |
{fine_tuning_job_failed,
integer(),
binary(),
gleam@option:option(binary()),
webhook_fine_tuning_job_data()} |
{fine_tuning_job_succeeded,
integer(),
binary(),
gleam@option:option(binary()),
webhook_fine_tuning_job_data()} |
{realtime_call_incoming,
integer(),
binary(),
gleam@option:option(binary()),
webhook_realtime_call_data()} |
{response_cancelled,
integer(),
binary(),
gleam@option:option(binary()),
webhook_response_data()} |
{response_completed,
integer(),
binary(),
gleam@option:option(binary()),
webhook_response_data()} |
{response_failed,
integer(),
binary(),
gleam@option:option(binary()),
webhook_response_data()} |
{response_incomplete,
integer(),
binary(),
gleam@option:option(binary()),
webhook_response_data()}.
-file("src/glopenai/webhook.gleam", 172).
-spec batch_data_decoder() -> gleam@dynamic@decode:decoder(webhook_batch_data()).
batch_data_decoder() ->
gleam@dynamic@decode:field(
<<"id"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Id) -> gleam@dynamic@decode:success({webhook_batch_data, Id}) end
).
-file("src/glopenai/webhook.gleam", 177).
-spec eval_run_data_decoder() -> gleam@dynamic@decode:decoder(webhook_eval_run_data()).
eval_run_data_decoder() ->
gleam@dynamic@decode:field(
<<"id"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Id) -> gleam@dynamic@decode:success({webhook_eval_run_data, Id}) end
).
-file("src/glopenai/webhook.gleam", 182).
-spec fine_tuning_job_data_decoder() -> gleam@dynamic@decode:decoder(webhook_fine_tuning_job_data()).
fine_tuning_job_data_decoder() ->
gleam@dynamic@decode:field(
<<"id"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Id) ->
gleam@dynamic@decode:success({webhook_fine_tuning_job_data, Id})
end
).
-file("src/glopenai/webhook.gleam", 187).
-spec response_data_decoder() -> gleam@dynamic@decode:decoder(webhook_response_data()).
response_data_decoder() ->
gleam@dynamic@decode:field(
<<"id"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Id) -> gleam@dynamic@decode:success({webhook_response_data, Id}) end
).
-file("src/glopenai/webhook.gleam", 192).
-spec sip_header_decoder() -> gleam@dynamic@decode:decoder(sip_header()).
sip_header_decoder() ->
gleam@dynamic@decode:field(
<<"name"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Name) ->
gleam@dynamic@decode:field(
<<"value"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Value) ->
gleam@dynamic@decode:success({sip_header, Name, Value})
end
)
end
).
-file("src/glopenai/webhook.gleam", 198).
-spec realtime_call_data_decoder() -> gleam@dynamic@decode:decoder(webhook_realtime_call_data()).
realtime_call_data_decoder() ->
gleam@dynamic@decode:field(
<<"call_id"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Call_id) ->
gleam@dynamic@decode:field(
<<"sip_headers"/utf8>>,
gleam@dynamic@decode:list(sip_header_decoder()),
fun(Sip_headers) ->
gleam@dynamic@decode:success(
{webhook_realtime_call_data, Call_id, Sip_headers}
)
end
)
end
).
-file("src/glopenai/webhook.gleam", 212).
?DOC(
" Decode a `WebhookEvent` from JSON. The variant is chosen by the `type`\n"
" field on the envelope.\n"
).
-spec webhook_event_decoder() -> gleam@dynamic@decode:decoder(webhook_event()).
webhook_event_decoder() ->
gleam@dynamic@decode:field(
<<"type"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Event_type) ->
gleam@dynamic@decode:field(
<<"created_at"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Created_at) ->
gleam@dynamic@decode:field(
<<"id"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Id) ->
gleam@dynamic@decode:optional_field(
<<"object"/utf8>>,
none,
gleam@dynamic@decode:optional(
{decoder,
fun gleam@dynamic@decode:decode_string/1}
),
fun(Object) -> case Event_type of
<<"batch.cancelled"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
batch_data_decoder(),
fun(Data) ->
gleam@dynamic@decode:success(
{batch_cancelled,
Created_at,
Id,
Object,
Data}
)
end
);
<<"batch.completed"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
batch_data_decoder(),
fun(Data@1) ->
gleam@dynamic@decode:success(
{batch_completed,
Created_at,
Id,
Object,
Data@1}
)
end
);
<<"batch.expired"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
batch_data_decoder(),
fun(Data@2) ->
gleam@dynamic@decode:success(
{batch_expired,
Created_at,
Id,
Object,
Data@2}
)
end
);
<<"batch.failed"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
batch_data_decoder(),
fun(Data@3) ->
gleam@dynamic@decode:success(
{batch_failed,
Created_at,
Id,
Object,
Data@3}
)
end
);
<<"eval.run.canceled"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
eval_run_data_decoder(),
fun(Data@4) ->
gleam@dynamic@decode:success(
{eval_run_canceled,
Created_at,
Id,
Object,
Data@4}
)
end
);
<<"eval.run.failed"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
eval_run_data_decoder(),
fun(Data@5) ->
gleam@dynamic@decode:success(
{eval_run_failed,
Created_at,
Id,
Object,
Data@5}
)
end
);
<<"eval.run.succeeded"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
eval_run_data_decoder(),
fun(Data@6) ->
gleam@dynamic@decode:success(
{eval_run_succeeded,
Created_at,
Id,
Object,
Data@6}
)
end
);
<<"fine_tuning.job.cancelled"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
fine_tuning_job_data_decoder(),
fun(Data@7) ->
gleam@dynamic@decode:success(
{fine_tuning_job_cancelled,
Created_at,
Id,
Object,
Data@7}
)
end
);
<<"fine_tuning.job.failed"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
fine_tuning_job_data_decoder(),
fun(Data@8) ->
gleam@dynamic@decode:success(
{fine_tuning_job_failed,
Created_at,
Id,
Object,
Data@8}
)
end
);
<<"fine_tuning.job.succeeded"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
fine_tuning_job_data_decoder(),
fun(Data@9) ->
gleam@dynamic@decode:success(
{fine_tuning_job_succeeded,
Created_at,
Id,
Object,
Data@9}
)
end
);
<<"realtime.call.incoming"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
realtime_call_data_decoder(),
fun(Data@10) ->
gleam@dynamic@decode:success(
{realtime_call_incoming,
Created_at,
Id,
Object,
Data@10}
)
end
);
<<"response.cancelled"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
response_data_decoder(),
fun(Data@11) ->
gleam@dynamic@decode:success(
{response_cancelled,
Created_at,
Id,
Object,
Data@11}
)
end
);
<<"response.completed"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
response_data_decoder(),
fun(Data@12) ->
gleam@dynamic@decode:success(
{response_completed,
Created_at,
Id,
Object,
Data@12}
)
end
);
<<"response.failed"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
response_data_decoder(),
fun(Data@13) ->
gleam@dynamic@decode:success(
{response_failed,
Created_at,
Id,
Object,
Data@13}
)
end
);
<<"response.incomplete"/utf8>> ->
gleam@dynamic@decode:field(
<<"data"/utf8>>,
response_data_decoder(),
fun(Data@14) ->
gleam@dynamic@decode:success(
{response_incomplete,
Created_at,
Id,
Object,
Data@14}
)
end
);
Other ->
gleam@dynamic@decode:failure(
{batch_completed,
0,
<<""/utf8>>,
none,
{webhook_batch_data,
<<""/utf8>>}},
<<"WebhookEvent: unknown type "/utf8,
Other/binary>>
)
end end
)
end
)
end
)
end
).
-file("src/glopenai/webhook.gleam", 293).
?DOC(" Return the `created_at` timestamp of any webhook event variant.\n").
-spec created_at(webhook_event()) -> integer().
created_at(Event) ->
case Event of
{batch_cancelled, T, _, _, _} ->
T;
{batch_completed, T@1, _, _, _} ->
T@1;
{batch_expired, T@2, _, _, _} ->
T@2;
{batch_failed, T@3, _, _, _} ->
T@3;
{eval_run_canceled, T@4, _, _, _} ->
T@4;
{eval_run_failed, T@5, _, _, _} ->
T@5;
{eval_run_succeeded, T@6, _, _, _} ->
T@6;
{fine_tuning_job_cancelled, T@7, _, _, _} ->
T@7;
{fine_tuning_job_failed, T@8, _, _, _} ->
T@8;
{fine_tuning_job_succeeded, T@9, _, _, _} ->
T@9;
{realtime_call_incoming, T@10, _, _, _} ->
T@10;
{response_cancelled, T@11, _, _, _} ->
T@11;
{response_completed, T@12, _, _, _} ->
T@12;
{response_failed, T@13, _, _, _} ->
T@13;
{response_incomplete, T@14, _, _, _} ->
T@14
end.
-file("src/glopenai/webhook.gleam", 314).
?DOC(" Return the wire string for the event's `type` discriminator.\n").
-spec event_type(webhook_event()) -> binary().
event_type(Event) ->
case Event of
{batch_cancelled, _, _, _, _} ->
<<"batch.cancelled"/utf8>>;
{batch_completed, _, _, _, _} ->
<<"batch.completed"/utf8>>;
{batch_expired, _, _, _, _} ->
<<"batch.expired"/utf8>>;
{batch_failed, _, _, _, _} ->
<<"batch.failed"/utf8>>;
{eval_run_canceled, _, _, _, _} ->
<<"eval.run.canceled"/utf8>>;
{eval_run_failed, _, _, _, _} ->
<<"eval.run.failed"/utf8>>;
{eval_run_succeeded, _, _, _, _} ->
<<"eval.run.succeeded"/utf8>>;
{fine_tuning_job_cancelled, _, _, _, _} ->
<<"fine_tuning.job.cancelled"/utf8>>;
{fine_tuning_job_failed, _, _, _, _} ->
<<"fine_tuning.job.failed"/utf8>>;
{fine_tuning_job_succeeded, _, _, _, _} ->
<<"fine_tuning.job.succeeded"/utf8>>;
{realtime_call_incoming, _, _, _, _} ->
<<"realtime.call.incoming"/utf8>>;
{response_cancelled, _, _, _, _} ->
<<"response.cancelled"/utf8>>;
{response_completed, _, _, _, _} ->
<<"response.completed"/utf8>>;
{response_failed, _, _, _, _} ->
<<"response.failed"/utf8>>;
{response_incomplete, _, _, _, _} ->
<<"response.incomplete"/utf8>>
end.
-file("src/glopenai/webhook.gleam", 338).
?DOC(
" Decode a webhook payload string into a `WebhookEvent`. Does NOT verify the\n"
" signature; use `verify_signature` first.\n"
).
-spec parse_event(binary()) -> {ok, webhook_event()} | {error, webhook_error()}.
parse_event(Body) ->
case gleam@json:parse(Body, webhook_event_decoder()) of
{ok, Event} ->
{ok, Event};
{error, Error} ->
{error, {deserialization, Body, Error}}
end.
-file("src/glopenai/webhook.gleam", 454).
-spec parse_timestamp(binary()) -> {ok, integer()} | {error, webhook_error()}.
parse_timestamp(Timestamp) ->
case gleam_stdlib:parse_int(Timestamp) of
{ok, Value} ->
{ok, Value};
{error, _} ->
{error, {invalid, <<"invalid timestamp format"/utf8>>}}
end.
-file("src/glopenai/webhook.gleam", 479).
?DOC(
" Parse the value of the `webhook-signature` header. Two formats are\n"
" accepted:\n"
"\n"
" - `\"v1,base64sig v1,base64sig2\"` — Standard Webhooks list of versioned\n"
" signatures separated by whitespace; we only honour `v1`.\n"
" - `\"base64sig\"` — a bare signature with no version tag.\n"
).
-spec parse_signature_header(binary()) -> list(binary()).
parse_signature_header(Signature) ->
case gleam_stdlib:contains_string(Signature, <<","/utf8>>) of
true ->
_pipe = Signature,
_pipe@1 = gleam@string:split(_pipe, <<" "/utf8>>),
gleam@list:filter_map(
_pipe@1,
fun(Part) -> case gleam@string:split(Part, <<","/utf8>>) of
[<<"v1"/utf8>>, Sig] ->
{ok, Sig};
_ ->
{error, nil}
end end
);
false ->
[Signature]
end.
-file("src/glopenai/webhook.gleam", 505).
-spec constant_time_eq_bits(bitstring(), bitstring(), integer()) -> boolean().
constant_time_eq_bits(A, B, Acc) ->
case {A, B} of
{<<X, Rest_a/bitstring>>, <<Y, Rest_b/bitstring>>} ->
constant_time_eq_bits(
Rest_a,
Rest_b,
erlang:'bor'(Acc, erlang:'bxor'(X, Y))
);
{_, _} ->
Acc =:= 0
end.
-file("src/glopenai/webhook.gleam", 496).
?DOC(
" Constant-time string comparison via byte-wise XOR. Avoids early exits that\n"
" could leak length information through timing.\n"
).
-spec constant_time_eq_string(binary(), binary()) -> boolean().
constant_time_eq_string(A, B) ->
A_bits = gleam_stdlib:identity(A),
B_bits = gleam_stdlib:identity(B),
case erlang:byte_size(A_bits) =:= erlang:byte_size(B_bits) of
false ->
false;
true ->
constant_time_eq_bits(A_bits, B_bits, 0)
end.
-file("src/glopenai/webhook.gleam", 519).
-spec hmac_sha256(bitstring(), bitstring()) -> bitstring().
hmac_sha256(Key, Data) ->
glopenai_webhook_ffi:hmac_sha256(Key, Data).
-file("src/glopenai/webhook.gleam", 523).
-spec base64_encode_binary(bitstring()) -> binary().
base64_encode_binary(Bytes) ->
case gleam@bit_array:to_string(glopenai_webhook_ffi:base64_encode(Bytes)) of
{ok, S} ->
S;
{error, _} ->
<<""/utf8>>
end.
-file("src/glopenai/webhook.gleam", 531).
-spec base64_decode(bitstring()) -> {ok, bitstring()} | {error, nil}.
base64_decode(Input) ->
glopenai_webhook_ffi:base64_decode(Input).
-file("src/glopenai/webhook.gleam", 461).
-spec decode_secret(binary()) -> {ok, bitstring()} | {error, webhook_error()}.
decode_secret(Secret) ->
Stripped = case gleam_stdlib:string_starts_with(Secret, <<"whsec_"/utf8>>) of
true ->
gleam@string:drop_start(Secret, 6);
false ->
Secret
end,
case base64_decode(gleam_stdlib:identity(Stripped)) of
{ok, Bytes} ->
{ok, Bytes};
{error, _} ->
{error, {invalid, <<"failed to decode secret from base64"/utf8>>}}
end.
-file("src/glopenai/webhook.gleam", 374).
?DOC(" Verify a webhook signature with an explicit tolerance window in seconds.\n").
-spec verify_signature_with_tolerance(
binary(),
binary(),
binary(),
binary(),
binary(),
integer(),
integer()
) -> {ok, nil} | {error, webhook_error()}.
verify_signature_with_tolerance(
Body,
Signature,
Timestamp,
Webhook_id,
Secret,
Now,
Tolerance_seconds
) ->
gleam@result:'try'(
parse_timestamp(Timestamp),
fun(Timestamp_seconds) ->
case (Now - Timestamp_seconds) > Tolerance_seconds of
true ->
{error, {invalid, <<"webhook timestamp is too old"/utf8>>}};
false ->
case Timestamp_seconds > (Now + Tolerance_seconds) of
true ->
{error,
{invalid,
<<"webhook timestamp is too new"/utf8>>}};
false ->
gleam@result:'try'(
decode_secret(Secret),
fun(Secret_bytes) ->
Signed_payload = <<<<<<<<Webhook_id/binary,
"."/utf8>>/binary,
Timestamp/binary>>/binary,
"."/utf8>>/binary,
Body/binary>>,
Mac = hmac_sha256(
Secret_bytes,
gleam_stdlib:identity(Signed_payload)
),
Expected = base64_encode_binary(Mac),
Candidates = parse_signature_header(
Signature
),
case gleam@list:any(
Candidates,
fun(Sig) ->
constant_time_eq_string(
Sig,
Expected
)
end
) of
true ->
{ok, nil};
false ->
{error, invalid_signature}
end
end
)
end
end
end
).
-file("src/glopenai/webhook.gleam", 431).
?DOC(" Verify with explicit tolerance, then decode the body.\n").
-spec build_event_with_tolerance(
binary(),
binary(),
binary(),
binary(),
binary(),
integer(),
integer()
) -> {ok, webhook_event()} | {error, webhook_error()}.
build_event_with_tolerance(
Body,
Signature,
Timestamp,
Webhook_id,
Secret,
Now,
Tolerance_seconds
) ->
gleam@result:'try'(
verify_signature_with_tolerance(
Body,
Signature,
Timestamp,
Webhook_id,
Secret,
Now,
Tolerance_seconds
),
fun(_) -> parse_event(Body) end
).
-file("src/glopenai/webhook.gleam", 354).
?DOC(
" Verify a webhook signature using the default 300-second tolerance.\n"
"\n"
" `now` is the current Unix timestamp in seconds. Pass a clock you trust;\n"
" this keeps verification sans-IO.\n"
).
-spec verify_signature(
binary(),
binary(),
binary(),
binary(),
binary(),
integer()
) -> {ok, nil} | {error, webhook_error()}.
verify_signature(Body, Signature, Timestamp, Webhook_id, Secret, Now) ->
verify_signature_with_tolerance(
Body,
Signature,
Timestamp,
Webhook_id,
Secret,
Now,
300
).
-file("src/glopenai/webhook.gleam", 411).
?DOC(" Verify a signature and decode the body in one step.\n").
-spec build_event(binary(), binary(), binary(), binary(), binary(), integer()) -> {ok,
webhook_event()} |
{error, webhook_error()}.
build_event(Body, Signature, Timestamp, Webhook_id, Secret, Now) ->
build_event_with_tolerance(
Body,
Signature,
Timestamp,
Webhook_id,
Secret,
Now,
300
).