///|
pub struct Header {
name : String
value : String
} derive(Eq, Debug)
///|
pub fn Header::new(name : String, value : String) -> Header {
{ name, value }
}
///|
pub fn Header::normalized_name(self : Header) -> String {
ascii_lower(self.name)
}
///|
pub(all) enum HttpMethod {
Get
Post
Put
Patch
Delete
Unknown(String)
} derive(Eq, Debug)
///|
pub fn method_name(http_method : HttpMethod) -> String {
match http_method {
Get => "GET"
Post => "POST"
Put => "PUT"
Patch => "PATCH"
Delete => "DELETE"
Unknown(value) => value
}
}
///|
pub struct WebhookRequest {
provider : String
http_method : HttpMethod
path : String
body : String
headers : Array[Header]
received_at : Int
remote_ip : String
} derive(Eq, Debug)
///|
pub fn WebhookRequest::new(
provider : String,
http_method : HttpMethod,
path : String,
body : String,
received_at : Int,
headers? : Array[Header] = [],
remote_ip? : String = "",
) -> WebhookRequest {
{ provider, http_method, path, body, headers, received_at, remote_ip }
}
///|
pub fn WebhookRequest::header(self : WebhookRequest, name : String) -> String? {
let wanted = ascii_lower(name)
for i in 0.. WebhookRequest {
let old = self.headers
let header = Header::new(name, value)
let headers = Array::makei(old.length() + 1, i => {
if i < old.length() {
old[i]
} else {
header
}
})
{ ..self, headers, }
}
///|
pub fn WebhookRequest::without_header(
self : WebhookRequest,
name : String,
) -> WebhookRequest {
let wanted = ascii_lower(name)
let mut count = 0
for header in self.headers {
if header.normalized_name() != wanted {
count = count + 1
}
}
let mut offset = 0
let headers = Array::makei(count, _ => {
while self.headers[offset].normalized_name() == wanted {
offset = offset + 1
}
let header = self.headers[offset]
offset = offset + 1
header
})
{ ..self, headers, }
}
///|
pub fn WebhookRequest::with_replaced_header(
self : WebhookRequest,
name : String,
value : String,
) -> WebhookRequest {
self.without_header(name).with_header(name, value)
}
///|
pub fn WebhookRequest::signature(
self : WebhookRequest,
header_name : String,
) -> String {
match self.header(header_name) {
Some(value) => value
None => ""
}
}
///|
pub fn WebhookRequest::timestamp(
self : WebhookRequest,
header_name : String,
) -> Int {
match self.header(header_name) {
Some(value) => parse_positive_int(value)
None => 0
}
}
///|
pub fn WebhookRequest::event_type(
self : WebhookRequest,
header_name : String,
) -> String {
match self.header(header_name) {
Some(value) => value
None => ""
}
}
///|
pub fn WebhookRequest::event_id(
self : WebhookRequest,
header_name : String,
) -> String {
match self.header(header_name) {
Some(value) => value
None => ""
}
}
///|
pub struct ProviderProfile {
name : String
signature_header : String
timestamp_header : String
event_type_header : String
event_id_header : String
signature_prefix : String
timestamp_tolerance_seconds : Int
} derive(Eq, Debug)
///|
pub fn ProviderProfile::new(
name : String,
signature_header : String,
timestamp_header : String,
event_type_header : String,
event_id_header : String,
signature_prefix? : String = "",
timestamp_tolerance_seconds? : Int = 300,
) -> ProviderProfile {
{
name,
signature_header,
timestamp_header,
event_type_header,
event_id_header,
signature_prefix,
timestamp_tolerance_seconds,
}
}
///|
pub fn ProviderProfile::github() -> ProviderProfile {
ProviderProfile::new(
"github",
"x-hub-signature-256",
"",
"x-github-event",
"x-github-delivery",
signature_prefix="sha256=",
)
}
///|
pub fn ProviderProfile::feishu() -> ProviderProfile {
ProviderProfile::new(
"feishu", "x-lark-signature", "x-lark-request-timestamp", "x-lark-event-type",
"x-lark-request-nonce",
)
}
///|
pub fn ProviderProfile::stripe() -> ProviderProfile {
ProviderProfile::new("stripe", "stripe-signature", "", "", "")
}
///|
pub fn ProviderProfile::generic() -> ProviderProfile {
ProviderProfile::new(
"generic", "x-webhook-signature", "x-webhook-timestamp", "x-webhook-event", "x-webhook-id",
)
}
///|
pub fn provider_catalog() -> Array[ProviderProfile] {
[
ProviderProfile::github(),
ProviderProfile::feishu(),
ProviderProfile::stripe(),
ProviderProfile::generic(),
]
}
///|
pub fn find_provider(name : String) -> ProviderProfile {
let providers = provider_catalog()
for i in 0.. VerificationPolicy {
{
provider: ProviderProfile::github(),
secret: "demo-secret",
now: 1700000000,
require_signature: true,
require_timestamp: false,
require_event_id: true,
allow_unknown_event: true,
}
}
///|
pub fn VerificationPolicy::for_provider(
provider : ProviderProfile,
) -> VerificationPolicy {
{
..VerificationPolicy::default(),
provider,
require_timestamp: provider.name != "github",
}
}
///|
pub fn VerificationPolicy::with_secret(
self : VerificationPolicy,
secret : String,
) -> VerificationPolicy {
{ ..self, secret, }
}
///|
pub fn VerificationPolicy::with_clock(
self : VerificationPolicy,
now : Int,
) -> VerificationPolicy {
{ ..self, now, }
}
///|
pub fn VerificationPolicy::with_unknown_events(
self : VerificationPolicy,
allowed : Bool,
) -> VerificationPolicy {
{ ..self, allow_unknown_event: allowed }
}
///|
pub(all) enum VerificationStatus {
Accepted
Rejected
Skipped
} derive(Eq, Debug)
///|
pub(all) enum DiagnosticCode {
MissingSignature
SignatureMismatch
MissingTimestamp
TimestampOutsideWindow
MissingEventId
ReplayDetected
DuplicateIdempotencyKey
UnknownEvent
ProviderMismatch
AcceptedDiagnostic
} derive(Eq, Debug)
///|
pub struct GuardDiagnostic {
code : DiagnosticCode
message : String
} derive(Eq, Debug)
///|
pub fn GuardDiagnostic::new(
code : DiagnosticCode,
message : String,
) -> GuardDiagnostic {
{ code, message }
}
///|
pub fn diagnostic_name(code : DiagnosticCode) -> String {
match code {
MissingSignature => "missing_signature"
SignatureMismatch => "signature_mismatch"
MissingTimestamp => "missing_timestamp"
TimestampOutsideWindow => "timestamp_outside_window"
MissingEventId => "missing_event_id"
ReplayDetected => "replay_detected"
DuplicateIdempotencyKey => "duplicate_idempotency_key"
UnknownEvent => "unknown_event"
ProviderMismatch => "provider_mismatch"
AcceptedDiagnostic => "accepted"
}
}
///|
pub struct VerificationResult {
status : VerificationStatus
provider : String
event_type : String
event_id : String
diagnostics : Array[GuardDiagnostic]
} derive(Eq, Debug)
///|
pub fn VerificationResult::new(
status : VerificationStatus,
provider : String,
event_type : String,
event_id : String,
diagnostics? : Array[GuardDiagnostic] = [],
) -> VerificationResult {
{ status, provider, event_type, event_id, diagnostics }
}
///|
pub fn VerificationResult::accepted(
provider : String,
event_type : String,
event_id : String,
) -> VerificationResult {
VerificationResult::new(Accepted, provider, event_type, event_id, diagnostics=[
GuardDiagnostic::new(AcceptedDiagnostic, "accepted"),
])
}
///|
pub fn VerificationResult::rejected(
provider : String,
event_type : String,
event_id : String,
diagnostics : Array[GuardDiagnostic],
) -> VerificationResult {
VerificationResult::new(
Rejected,
provider,
event_type,
event_id,
diagnostics~,
)
}
///|
pub fn VerificationResult::is_ok(self : VerificationResult) -> Bool {
self.status == Accepted
}
///|
pub fn VerificationResult::to_text(self : VerificationResult) -> String {
let builder = StringBuilder::new()
builder.write_string("status=")
match self.status {
Accepted => builder.write_string("accepted")
Rejected => builder.write_string("rejected")
Skipped => builder.write_string("skipped")
}
builder.write_string(" provider=")
builder.write_string(self.provider)
builder.write_string(" event=")
builder.write_string(self.event_type)
builder.write_string(" id=")
builder.write_string(self.event_id)
for i in 0..