standardwebhooks 0.1.0
Standard Webhooks (standardwebhooks.com) signing and verification for D
To use this package, run the following command in your project's root directory:
Manual usage
Put the following dependency into your project's dependences section:
This package provides sub packages which can be used individually:
standardwebhooks:vibe - vibe.d HTTP integration for standardwebhooks
standardwebhooks:ed25519 - Asymmetric ed25519 (v1a) signatures for standardwebhooks, via libsodium
standardwebhooks.d
A faithful Standard Webhooks implementation for the D programming language — sign outgoing webhooks and verify incoming ones with HMAC-SHA256, constant-time comparison, and timestamp replay protection.
The core is dependency-free (Phobos only). Two optional subpackages extend
it: standardwebhooks:vibe wires it directly into
vibe.d HTTP requests, and
standardwebhooks:ed25519 adds the spec's
asymmetric v1a signatures via libsodium.
Why Standard Webhooks?
Standard Webhooks is an open specification
for securing webhooks, backed by a set of reference libraries across languages.
A sender signs the message id, timestamp, and raw body; a receiver recomputes
the signature and rejects anything that does not match or is too old. This
library produces and accepts byte-for-byte the same webhook-signature values
as the official Go, Python, JavaScript, and Rust libraries (verified against
their published test vectors).
Install
dub add standardwebhooks
or in dub.json:
"dependencies": {
"standardwebhooks": "~>0.1"
}
Quickstart
import standardwebhooks;
void main()
{
// The secret you share with the other side. The `whsec_` prefix is
// optional; raw key bytes are also supported via Webhook.fromRaw.
auto wh = Webhook("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw");
string id = "msg_2b3c4d5e";
long timestamp = 1614265330; // unix seconds
string payload = `{"event":"ping"}`; // the raw request body
// --- Sending: build the three headers to attach to your HTTP request ---
string[string] headers = wh.signHeaders(id, timestamp, payload);
// headers["webhook-id"], ["webhook-timestamp"], ["webhook-signature"]
// --- Receiving: verify an incoming request (throws on any mismatch) ---
auto verified = wh.verify(payload, headers);
assert(verified == payload);
}
verify throws a
WebhookVerificationException
if a header is missing, the timestamp is outside the tolerance window (±5
minutes by default), or no signature matches. On success it returns the raw
payload so you can parse it in the same expression:
import std.json : parseJSON;
try
{
auto data = parseJSON(wh.verify(body_, request.headers));
handleEvent(data);
}
catch (WebhookVerificationException e)
{
// Reject with HTTP 400; e.error gives the machine-readable cause.
}
API
All of the following live on the Webhook value type.
| Member | Purpose |
|---|---|
Webhook(string secret) | Construct from a whsec_-prefixed or bare base64 secret. Padded or unpadded base64 both work. |
Webhook.fromRaw(ubyte[] key) | Construct directly from raw HMAC key bytes. |
sign(id, timestamp, payload) | Returns a single v1,<base64> signature. timestamp is unix seconds or a SysTime. |
signHeaders(id, timestamp, payload) | Returns the webhook-id / webhook-timestamp / webhook-signature headers as an associative array. |
verify(payload, headers) | Verifies signature and timestamp; returns the payload or throws. |
verifyIgnoringTimestamp(payload, headers) | Verifies the signature only (no replay-window check). |
toleranceSeconds | Field; the replay window in seconds (default 300). |
Header lookup is case-insensitive and also accepts the Svix-branded
svix-id / svix-timestamp / svix-signature aliases as a fallback, so
payloads from Svix-powered senders verify without remapping.
Signature scheme
The signed content is the literal string {id}.{timestamp}.{payload}, HMAC'd
with SHA-256 under the decoded secret, then standard-base64 encoded. The
webhook-signature header is a space-delimited list of v1,<sig> entries
(supporting zero-downtime secret rotation); verification succeeds if any
v1 entry matches in constant time. Entries with other versions are skipped.
The core implements the symmetric (HMAC, whsec_, v1) scheme, which is what
every official reference library ships, and treats unknown versions (including
v1a) as skip-not-error. The spec's asymmetric v1a (ed25519) scheme is
available as the optional
standardwebhooks:ed25519 subpackage.
vibe.d integration
The standardwebhooks:vibe subpackage adds one-call helpers for vibe.d HTTP:
dub add standardwebhooks:vibe
import standardwebhooks;
import standardwebhooks.vibe;
import vibe.http.server;
void handler(HTTPServerRequest req, HTTPServerResponse res)
{
auto wh = Webhook("whsec_...");
try
{
auto payload = wh.verifyRequest(req); // reads body + headers, verifies
res.writeBody("ok");
}
catch (WebhookVerificationException)
{
res.statusCode = HTTPStatus.badRequest;
res.writeBody("invalid signature");
}
}
For outgoing requests, signRequest(wh, clientReq, id, timestamp, payload) sets
the three headers; send payload as the body so the signed bytes match the sent
bytes.
Asymmetric (ed25519) signatures
The spec also defines an asymmetric scheme (v1a): the sender signs with an
ed25519 private key and receivers verify with the matching public key, so a
leaked verifier cannot forge messages. This is provided by the optional
standardwebhooks:ed25519 subpackage, which links the system libsodium for
the ed25519 primitives (so it is kept out of the dependency-free core).
# Install libsodium first: apt-get install libsodium-dev | brew install libsodium
dub add standardwebhooks:ed25519
import standardwebhooks;
import standardwebhooks.ed25519;
// Sender holds a whsk_ signing key (or derive one from a 32-byte seed via
// AsymmetricWebhook.fromSeed); it can both sign and verify.
auto signer = AsymmetricWebhook("whsk_...");
auto headers = signer.signHeaders("msg_2b3c", 1614265330, `{"event":"ping"}`);
// signer.publicKeyEncoded() yields the whpk_ string to hand to receivers.
// Receiver holds only the whpk_ public key; it can verify but never sign.
auto verifier = AsymmetricWebhook("whpk_...");
auto payload = verifier.verify(`{"event":"ping"}`, headers);
AsymmetricWebhook mirrors the Webhook API — sign, signHeaders, verify,
verifyIgnoringTimestamp, and the toleranceSeconds field all behave the same,
with the same case-insensitive/svix-* header handling and replay window. Keys
use the Standard Webhooks serialisation: whsk_ + base64 of the 64-byte
seed ‖ public_key, and whpk_ + base64 of the 32-byte public key. Calling
sign on a verify-only (whpk_) instance throws with
WebhookError.signingKeyRequired.
See examples/asymmetric for a runnable end-to-end demo.
Security
- Constant-time comparison. Signatures are compared without short-circuiting to avoid timing side channels.
- Replay protection. Payloads older or newer than
toleranceSeconds(±5 minutes by default) are rejected. - Verify the raw body. Always verify the exact bytes you received, before parsing. Re-serializing JSON first will change the bytes and break verification — that is by design.
See SECURITY.md to report a vulnerability.
Development
just build # dub build
just test # dub test (unit tests, incl. the reference vectors)
just fmt # dfmt --inplace
just lint # dscanner static analysis
(Or use dub directly; prefix with ulimit -n 65536 && if your terminal sets a
huge file-descriptor limit — see CONTRIBUTING.md.)
License
MIT. The Standard Webhooks specification is also MIT-licensed by its authors; see NOTICE.
- 0.1.0 released a day ago
- Poita/standardwebhooks.d
- github.com/Poita/standardwebhooks.d
- MIT
- Copyright © 2026 Peter Alexander
- Authors:
- Sub packages:
- standardwebhooks:vibe, standardwebhooks:ed25519
- Dependencies:
- none
- Versions:
-
Show all 8 versions0.2.0 2026-Jun-14 0.1.0 2026-Jun-14 ~main 2026-Jun-14 ~fix/99-zeroize-transient-secret-buffers 2026-Jun-14 ~fix/98-siglen-runtime-check 2026-Jun-14 - Download Stats:
-
-
0 downloads today
-
0 downloads this week
-
0 downloads this month
-
0 downloads total
-
- Score:
- 0.0
- Short URL:
- standardwebhooks.dub.pm