///|
fn timestamp_diagnostic(
request : WebhookRequest,
policy : VerificationPolicy,
) -> GuardDiagnostic? {
let value = provider_timestamp(request, policy.provider)
if policy.require_timestamp && value == 0 {
Some(GuardDiagnostic::new(MissingTimestamp, "timestamp header is missing"))
} else if value != 0 &&
abs_int(policy.now - value) > policy.provider.timestamp_tolerance_seconds {
Some(
GuardDiagnostic::new(
TimestampOutsideWindow,
"timestamp is outside allowed window",
),
)
} else {
None
}
}
///|
fn signature_diagnostic(
request : WebhookRequest,
policy : VerificationPolicy,
) -> GuardDiagnostic? {
let signature = request.signature(policy.provider.signature_header)
if policy.require_signature && signature == "" {
Some(GuardDiagnostic::new(MissingSignature, "signature header is missing"))
} else if signature != "" && !verify_signature(request, policy) {
Some(
GuardDiagnostic::new(
SignatureMismatch,
"signature does not match canonical payload",
),
)
} else {
None
}
}
///|
fn event_id_diagnostic(
request : WebhookRequest,
policy : VerificationPolicy,
) -> GuardDiagnostic? {
let event_id = provider_event_id(request, policy.provider)
if policy.require_event_id && event_id == "" {
Some(GuardDiagnostic::new(MissingEventId, "event id header is missing"))
} else {
None
}
}
///|
fn provider_diagnostic(
request : WebhookRequest,
policy : VerificationPolicy,
) -> GuardDiagnostic? {
if request.provider != "" && request.provider != policy.provider.name {
Some(
GuardDiagnostic::new(
ProviderMismatch,
"request provider does not match policy",
),
)
} else {
None
}
}
///|
fn collect_base_diagnostics(
request : WebhookRequest,
policy : VerificationPolicy,
) -> Array[GuardDiagnostic] {
let candidates = [
provider_diagnostic(request, policy),
timestamp_diagnostic(request, policy),
signature_diagnostic(request, policy),
event_id_diagnostic(request, policy),
]
let mut count = 0
for i in 0.. {
while offset < candidates.length() {
let value = candidates[offset]
offset = offset + 1
match value {
Some(diag) => return diag
None => ()
}
}
GuardDiagnostic::new(AcceptedDiagnostic, "accepted")
})
}
///|
pub fn verify_request(
request : WebhookRequest,
policy : VerificationPolicy,
) -> VerificationResult {
let diagnostics = collect_base_diagnostics(request, policy)
let event_type = provider_event_type(request, policy.provider)
let event_id = provider_event_id(request, policy.provider)
if diagnostics.length() == 0 {
VerificationResult::accepted(policy.provider.name, event_type, event_id)
} else {
VerificationResult::rejected(
policy.provider.name,
event_type,
event_id,
diagnostics,
)
}
}
///|
pub fn verify_request_with_state(
request : WebhookRequest,
policy : VerificationPolicy,
state : GuardState,
) -> (VerificationResult, GuardState) {
let base = verify_request(request, policy)
if !base.is_ok() {
(base, state)
} else {
let replay = replay_key(request, policy)
let idem = idempotency_key(request, policy)
if state.replay_store.contains(replay, policy.now) {
(
VerificationResult::rejected(
policy.provider.name,
base.event_type,
base.event_id,
[
GuardDiagnostic::new(
ReplayDetected,
"replay key has already been seen",
),
],
),
state,
)
} else if state.idempotency_store.contains(idem, policy.now) {
(
VerificationResult::rejected(
policy.provider.name,
base.event_type,
base.event_id,
[
GuardDiagnostic::new(
DuplicateIdempotencyKey,
"idempotency key has already been seen",
),
],
),
state,
)
} else {
(base, state.remember(request, policy))
}
}
}
///|
pub fn verify_batch(
requests : Array[WebhookRequest],
policy : VerificationPolicy,
) -> Array[VerificationResult] {
Array::makei(requests.length(), i => verify_request(requests[i], policy))
}
///|
pub fn accepted_count(results : Array[VerificationResult]) -> Int {
let mut count = 0
for i in 0.. Int {
results.length() - accepted_count(results)
}