herald-dingtalk

Multi-language Documentation
DingTalk notification adapter for Herald. Herald forwards verification codes over HTTP to this service; herald-dingtalk calls the DingTalk work notification API to deliver messages. All DingTalk credentials and business logic live in this project only—Herald does not hold any DingTalk credentials.
The HTTP server uses Fiber v3.5.0 and the matching v2 module lines of the Fiber-facing kit packages. Building from source requires Go 1.26.6 or later.
Core Features
- Herald HTTP Provider contract: Implements the same HTTP send contract as Herald's external provider; request/response align with provider-kit
HTTPSendRequest / HTTPSendResponse.
- Optional API Key auth: When
API_KEY is set, Herald must send X-API-Key; otherwise no auth required.
- Idempotency: Coalesces concurrent requests with the same key and caches successful results within the TTL. Reusing a key with different send content returns
409 idempotency_conflict.
- Graceful shutdown: On
SIGINT or SIGTERM, server stops accepting new requests and allows up to 35 seconds for in-flight requests to finish.
- Hardened service boundary: Constant-time API key verification, a bounded request body, HTTP timeouts, request IDs, security headers, and panic recovery.
Architecture
sequenceDiagram
participant User
participant Stargate
participant Herald
participant HeraldDingtalk as herald-dingtalk
participant DingTalk
User->>Stargate: Login (identifier)
Stargate->>Herald: Create challenge (channel=dingtalk, destination=userid)
Herald->>HeraldDingtalk: POST /v1/send (to=userid, body/code)
HeraldDingtalk->>DingTalk: Work notification API
DingTalk-->>User: DingTalk message
HeraldDingtalk-->>Herald: ok, message_id
Herald-->>Stargate: challenge_id, expires_in
- Stargate: ForwardAuth / login orchestration.
- Herald: OTP challenge creation and verification; calls herald-dingtalk for channel
dingtalk.
- herald-dingtalk: HTTP adapter; calls DingTalk work notification API; holds DingTalk credentials only here.
Protocol
- POST /v1/resolve (optional)
Exchange DingTalk OAuth2 auth_code for userid. Request: { "auth_code": "..." }. Response: { "ok": true, "userid": "..." } or error. See API.
- POST /v1/send
Request: channel (must be dingtalk when set), to (DingTalk userid, or 11-digit mobile when DINGTALK_LOOKUP_MODE=mobile), body (or params.code), idempotency_key, optional template/params/locale/subject/timeout_seconds (0–30).
Response: { "ok": true, "message_id": "...", "provider": "dingtalk" } or { "ok": false, "error_code": "...", "error_message": "..." }.
- GET /healthz:
{ "status": "healthy", "service": "herald-dingtalk" } (via health-kit).
- GET /readyz: Returns
200 only when credentials are complete, DINGTALK_AGENT_ID is a positive integer, and lookup mode is supported; otherwise returns 503.
Configuration
| Variable |
Description |
Default |
Required |
PORT |
Listen port (with or without leading colon) |
:8083 |
No |
API_KEY |
If set, Herald must send X-API-Key |
`` |
No |
DINGTALK_APP_KEY |
DingTalk app key |
`` |
Yes (for send) |
DINGTALK_APP_SECRET |
DingTalk app secret |
`` |
Yes (for send) |
DINGTALK_AGENT_ID |
Positive base-10 Agent ID for work notification |
`` |
Yes (for send) |
DINGTALK_LOOKUP_MODE |
Must be none (userid only) or mobile (userid or 11-digit mobile; requires Contact.User.mobile permission) |
none |
No |
LOG_LEVEL |
Log level: trace, debug, info, warn, error |
info |
No |
IDEMPOTENCY_TTL_SECONDS |
Idempotency cache TTL (seconds) |
300 |
No |
MAX_REQUEST_BODY_BYTES |
Maximum HTTP request body size; valid range is 1 byte–1 MiB |
65536 |
No |
MAX_CONCURRENT_REQUESTS |
Maximum in-flight /v1 requests per process; 0 disables the limit |
32 |
No |
Herald side
Configure Herald with HTTP provider for channel dingtalk:
HERALD_DINGTALK_API_URL = base URL of herald-dingtalk (e.g. http://herald-dingtalk:8083)
- Optional:
HERALD_DINGTALK_API_KEY = same as herald-dingtalk API_KEY
Herald does not hold any DingTalk credentials.
Quick Start
Build & run (binary)
Download a platform binary and checksums.txt from GitHub Releases. For example, after v1.0.0 is published:
curl -LO https://github.com/soulteary/herald-dingtalk/releases/download/v1.0.0/herald-dingtalk-linux-amd64
curl -LO https://github.com/soulteary/herald-dingtalk/releases/download/v1.0.0/checksums.txt
grep 'herald-dingtalk-linux-amd64$' checksums.txt | sha256sum -c -
chmod +x herald-dingtalk-linux-amd64
./herald-dingtalk-linux-amd64 --version
# 1.0.0
To build from source:
go build -o herald-dingtalk .
./herald-dingtalk
Print the embedded build version with ./herald-dingtalk --version.
With DingTalk credentials in env, POST /v1/send will send work notifications to the given userid.
Run with Docker
docker pull ghcr.io/soulteary/herald-dingtalk:v1.0.0
docker run -d --name herald-dingtalk -p 8083:8083 \
-e DINGTALK_APP_KEY=your_app_key \
-e DINGTALK_APP_SECRET=your_app_secret \
-e DINGTALK_AGENT_ID=your_agent_id \
ghcr.io/soulteary/herald-dingtalk:v1.0.0
Optional: add -e API_KEY=your_shared_secret and set HERALD_DINGTALK_API_KEY to the same value on Herald.
Documentation
Testing
go test ./...
Coverage:
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
go tool cover -html=coverage.out
Coverage includes configuration validation, idempotency and concurrency behavior, DingTalk success and failure responses, request handlers, authentication, routing, observability, and graceful shutdown. CI enforces a 90% total statement coverage floor. Lint: golangci-lint run.
Operation
- Graceful shutdown: On
SIGINT or SIGTERM, the server stops accepting new requests and allows up to 35 seconds for in-flight requests to finish. Logs "shutting down" and any shutdown error.
- Probes: Use
/healthz for liveness and /readyz for readiness. Readiness is 503 until DingTalk credentials are configured.
- HTTP boundary: Responses include an
X-Request-ID and security headers. Bodies default to 64 KiB, reads to 10s, writes to 35s, and idle connections to 60s.
- Logging: Structured JSON logs via logger-kit. Recipient identifiers, mobile numbers, user IDs, OAuth codes, API keys, and request bodies are not logged.
- Container: The runtime image includes a Docker health check and runs as the unprivileged
herald user.
- DingTalk client safeguards: Concurrent token refreshes are coalesced, an explicitly rejected token is refreshed once, non-2xx responses are rejected, and response bodies are limited to 1 MiB.
License
See LICENSE for details.