Outbound webhook delivery framework for MoonBit: signing, retry with backoff, outbox, dead letter, idempotency, audit, and real HTTP delivery over moonbitlang/async.
Dependencies
moon check --deny-warn # 三个目标均 0 warning
moon test --deny-warn # 75 个测试
moon check --target wasm-gc --deny-warn # 核心包保持可移植
moon check --target js --deny-warn
moon test --enable-coverage # 覆盖率:库代码 90% 以上(不含 CLI)
moon coverage report -f summary
moon run cmd/httpdemo -- e2e # 真实 HTTP 投递 + 验签,端到端moon test # 核心测试 + HTTP 集成测试
moon run cmd/main -- demo # 内存流水线演示
moon run cmd/main -- sign <secret> <payload>
moon run cmd/main -- verify <secret> <payload> <signature>
moon run cmd/main -- request <hook-id> <url> <secret> <payload>
moon run cmd/httpdemo -- e2e # 起接收端 + 真实投递 + 验签,一条命令跑通
moon run cmd/httpdemo -- serve 127.0.0.1:8080 <secret>
moon run cmd/httpdemo -- deliver <url> <secret> <payload>[receiver] listening on http://127.0.0.1:18080/hooks/orders
[sender] POST http://127.0.0.1:18080/hooks/orders
[sender] attempt 1: HTTP 200 in 2ms
[sender] status: success
[receiver] verified payloads: 1
[receiver] payload: {"order_id": "A-1001", "total": 99}///|
async fn main {
let registry = @MoonHook.HookRegistry::new()
let hook = @MoonHook.Hook::new("orders", "https://example.com/hooks/orders")
.with_secret("shared-secret")
.with_retry_policy(@MoonHook.RetryPolicy::Exponential(200, 2.0, 5_000))
.with_max_attempts(5)
registry.register(hook) |> ignore
let engine = @transport_http.AsyncDeliveryEngine::new(
registry,
@transport_http.http_transport(),
)
let outcome = engine.deliver("orders", event)
println(outcome.status.describe())
}///|
async fn main {
let server = @http.Server(@socket.Addr::parse("127.0.0.1:8080"))
server.run_forever(
@transport_http.webhook_receiver("shared-secret", 300_000, payload => {
println("verified webhook: \{payload}")
}),
)
}| 平台 | 方案 | 签名内容 |
|---|---|---|
| GitHub | X-Hub-Signature-256: sha256=<hex> | 原始 body |
| Stripe | Stripe-Signature: t=<unix_s>,v1=<hex> | "<t>.<body>",支持密钥轮换时的多个 v1 |
| Slack | X-Slack-Signature: v0=<hex> + X-Slack-Request-Timestamp | "v0:<t>:<body>" |
| 飞书自定义机器人 | body 内 timestamp + sign(base64) | HMAC-SHA256(key = "<t>\n<secret>", message = "") |
| Shopify | X-Shopify-Hmac-Sha256: <base64> | 原始 body |
///|
let provider = @MoonHook.SignatureProvider::Stripe
// 发送端:把平台签名头追加到已签名的请求上
///|
let signed = request.with_provider_signature(provider, secret, timestamp_s)
// 接收端:同一个 provider 负责验签,并校验时间窗
///|
let accepted = provider.verify(secret, headers, body, now_s, 300)moon run cmd/main -- providers
moon run cmd/main -- sign-provider stripe whsec_test_secret 1492774577 '{"id":"evt_1"}'///|
let request = @MoonHook.build_signed_request(hook, event)///|
let valid = @MoonHook.verify_webhook_request_with_timestamp(
secret, headers, body, now_ms, 300_000,
)///|
let valid = @MoonHook.verify_webhook_request(secret, headers, body)event.mbt # 事件模型与 EventBuilder
hook.mbt # Hook 模型与配置 Builder
registry.mbt # Hook 注册表
retry.mbt # 重试策略、退避计算与参数校验
sha256.mbt # 纯 MoonBit SHA-256
hmac.mbt # HMAC-SHA256 签名与验签
transport.mbt # 投递结果与 Transport 抽象
engine.mbt # DeliveryEngine 投递引擎
store.mbt # Outbox 与死信队列
idempotency.mbt # 幂等键存储
audit.mbt # 审计日志
request.mbt # 签名请求构建与接收端验签
json_codec.mbt # 领域模型 JSON 序列化
coordinator.mbt # MoonHook 高层门面
transport_http/ # 真实 HTTP 投递适配(native):传输、异步引擎、接收端处理器
cmd/main/ # 内存演示 CLI
cmd/httpdemo/ # HTTP 收发演示 CLI(native)///|
pub type Transport = (Hook, WebhookEvent) -> DeliveryResultpub(all) struct DeadLetterEntry {
hook_id : String
event : WebhookEvent
reason : String
failed_at_ms : Int
attempts : Int
} derive(Eq, Debug)pub struct DeliveryEngine {
registry : HookRegistry
transport : (Hook, WebhookEvent) -> DeliveryResult
}fn DeliveryEngine::deliver(self : DeliveryEngine, hook_id : String, event : WebhookEvent) -> DeliveryOutcomefn DeliveryEngine::new(registry : HookRegistry, transport : (Hook, WebhookEvent) -> DeliveryResult) -> DeliveryEnginepub(all) struct DeliveryOutcome {
hook_id : String
event_id : String
status : DeliveryStatus
attempts : Array[DeliveryAttempt]
} derive(Eq, Debug)fn DeliveryResult::failure(status_code : Int, error : String, duration_ms : Int, retryable : Bool) -> DeliveryResultfn EventBuilder::with_idempotency_key(self : EventBuilder, idempotency_key : String) -> EventBuilderpub(all) struct Hook {
id : String
url : String
secret : String
headers : Array[(String, String)]
active : Bool
max_attempts : Int
retry_policy : RetryPolicy
timeout_ms : Int
} derive(Eq, Debug)fn IdempotencyStore::check_and_mark(self : IdempotencyStore, key : String, event_id : String, now_ms : Int, ttl_ms : Int) -> Boolfn InMemoryOutbox::enqueue(self : InMemoryOutbox, hook_id : String, event : WebhookEvent, now_ms : Int) -> Unitpub struct MoonHook {
registry : HookRegistry
outbox : InMemoryOutbox
dead_letter : InMemoryDeadLetter
idempotency : IdempotencyStore
audit : AuditLog
engine : DeliveryEngine
idempotency_ttl_ms : Int
}fn MoonHook::deliver_now(self : MoonHook, hook_id : String, event : WebhookEvent, now_ms : Int) -> DeliveryOutcomefn MoonHook::enqueue(self : MoonHook, hook_id : String, event : WebhookEvent, now_ms : Int) -> Result[Unit, String]pub(all) struct PendingDelivery {
hook_id : String
event : WebhookEvent
queued_at_ms : Int
attempts_used : Int
} derive(Eq, Debug)fn SignatureProvider::sign_headers(self : SignatureProvider, secret : String, payload : String, timestamp_s : Int) -> Array[(String, String)]fn SignatureProvider::sign_value(self : SignatureProvider, secret : String, payload : String, timestamp_s : Int) -> Stringfn SignatureProvider::verify(self : SignatureProvider, secret : String, headers : Array[(String, String)], payload : String, now_s : Int, tolerance_s : Int) -> Boolfn SignedRequest::with_provider_signature(self : SignedRequest, provider : SignatureProvider, secret : String, timestamp_s : Int) -> SignedRequestfn WebhookEvent::new(id : String, event_type : String, subject : String, payload : String, timestamp_ms : Int, idempotency_key : String) -> WebhookEventlet HEADER_GITHUB_SIGNATURE : Stringlet HEADER_SHOPIFY_SIGNATURE : Stringlet HEADER_SIGNATURE : Stringlet HEADER_SLACK_TIMESTAMP : Stringlet HEADER_STRIPE_SIGNATURE : Stringfn constant_time_eq(left : String, right : String) -> Boolfn feishu_bot_text_body(secret : String, text : String, timestamp_s : Int) -> Stringfn sha256_hex(data : Bytes) -> Stringfn sign_hmac_sha256(secret : String, payload : String) -> Stringfn sign_webhook(secret : String, timestamp_ms : Int, payload : String) -> Stringfn verify_hmac_sha256(secret : String, payload : String, signature : String) -> Boolfn verify_signature_header(secret : String, body : String, header_value : String) -> Boolfn verify_webhook(secret : String, timestamp_ms : Int, payload : String, signature : String, now_ms : Int, tolerance_ms : Int) -> Boolfn verify_webhook_request(secret : String, headers : Array[(String, String)], body : String) -> Boolfn verify_webhook_request_with_timestamp(secret : String, headers : Array[(String, String)], body : String, now_ms : Int, tolerance_ms : Int) -> BoolInstall
Download zipOutbound webhook delivery framework for MoonBit: signing, retry with backoff, outbox, dead letter, idempotency, audit, and real HTTP delivery over moonbitlang/async.
Dependencies