Appearance
For an engineer porting @onehazel/connect-adapter to another language, or checking their own implementation of the envelope.
Envelope spec and test vectors
This is what an implementation in front of a PAM must do with each request OneHazel sends. The Node adapter is the reference; the contract is the authority on routes and bodies. Every check below runs on the RAW request bytes, before the body is parsed.
Wallet and player calls
Applies to every wallet route (session/start, session/exchange, session/balance, wallet/bet, wallet/win, wallet/rollback, freerounds/issue, freerounds/cancel, freerounds/settle, freerounds/get) and to player/lookup. All are POST.
- Bearer.
Authorizationmust be exactlyBearer <callback key>. Compare in constant time. Failure:401, codeunauthorized. - Signature.
X-OneHazel-Signatureis a comma-separated list ofkey=valueparts. Readt(unix seconds, digits only) and everyv1(exactly 64 lowercase hex characters); ignore other parts. Reject whentis missing, whenabs(now - t) > 300, or when nov1is present. The expected value is lowercase hex ofHMAC-SHA256(key = secret, message = "<t>." + raw body), where the secret is the wholewhsec_...string as UTF-8 bytes. Accept when ANYv1equals the expected value for ANY configured secret (two secrets only during a rotation hour). Compare every candidate in constant time, with no early exit. Failure:401, codesignature_invalid. - Environment.
X-OneHazel-Envmust be exactlytestorlive. Anything else:400, codeinvalid_request. - Test players. When the body names a player (
session/start,freerounds/issue,player/lookup), atestcall must name a configured test player and alivecall must not. Failure:404, codeplayer_unknown. - Forward. Send the raw body bytes unchanged to the PAM, with only
Content-Type,Accept,Idempotency-Keyand the PAM's own auth header. DropAuthorizationand everyX-OneHazel-*header. Never follow a redirect.
Webhook deliveries
webhook.receive uses the webhook engine's format, not the one above. There is no bearer, no timestamp and no X-OneHazel-Env.
- Signature. After trimming surrounding whitespace,
X-OneHazel-Signaturemust be exactlysha256=followed by 64 lowercase hex characters. The expected value is lowercase hex ofHMAC-SHA256(key = secret, message = raw body). Accept when it matches ANY configured secret, compared in constant time. Failure:401, codesignature_invalid. - Dedup. With no timestamp, a replayed delivery verifies, so deduplicate on
deliveryIdbefore forwarding. Release the claim when forwarding fails so the retry is not swallowed. - Answer within 5 s.
200once forwarded;503when the sink fails, so OneHazel retries.
Errors
Every rejection and every mapped PAM error is the flat envelope and nothing else:
json
{ "code": "signature_invalid", "message": "signature rejected", "retryable": false }retryable is true only for internal_error and unavailable. Messages are written by the implementation; a PAM's error body is never echoed.
Test vectors
Each vector is one signature check. kind: "call" uses the wallet and player rule with now as the current time and a 300 s tolerance; kind: "webhook" uses the webhook rule. secrets is the configured list, header the X-OneHazel-Signature value, and body the exact raw bytes as a UTF-8 string. An implementation passes when its verdict equals expect for every vector. The secrets are fixed test values, never real ones.
json
[
{
"id": "call-valid",
"kind": "call",
"body": "{\"session_token\":\"sess_test_1\",\"round_id\":\"r-1\",\"amount_minor\":250,\"currency\":\"EUR\",\"idempotency_key\":\"ik_vec_1\"}",
"secrets": [
"whsec_testvector_current"
],
"header": "t=1760000000,v1=d8411bce6961d2f77b348f346926de0bfe2d80a65dc0a862b75c0acfabf6a0d7",
"now": 1760000010,
"expect": "accept"
},
{
"id": "call-future-within-tolerance",
"kind": "call",
"body": "{\"session_token\":\"sess_test_1\",\"round_id\":\"r-1\",\"amount_minor\":250,\"currency\":\"EUR\",\"idempotency_key\":\"ik_vec_1\"}",
"secrets": [
"whsec_testvector_current"
],
"header": "t=1760000000,v1=d8411bce6961d2f77b348f346926de0bfe2d80a65dc0a862b75c0acfabf6a0d7",
"now": 1759999700,
"expect": "accept"
},
{
"id": "call-stale",
"kind": "call",
"body": "{\"session_token\":\"sess_test_1\",\"round_id\":\"r-1\",\"amount_minor\":250,\"currency\":\"EUR\",\"idempotency_key\":\"ik_vec_1\"}",
"secrets": [
"whsec_testvector_current"
],
"header": "t=1760000000,v1=d8411bce6961d2f77b348f346926de0bfe2d80a65dc0a862b75c0acfabf6a0d7",
"now": 1760000301,
"expect": "reject"
},
{
"id": "call-body-changed",
"kind": "call",
"body": "{\"session_token\":\"sess_test_1\",\"round_id\":\"r-1\",\"amount_minor\":251,\"currency\":\"EUR\",\"idempotency_key\":\"ik_vec_1\"}",
"secrets": [
"whsec_testvector_current"
],
"header": "t=1760000000,v1=d8411bce6961d2f77b348f346926de0bfe2d80a65dc0a862b75c0acfabf6a0d7",
"now": 1760000010,
"expect": "reject"
},
{
"id": "call-reserialised-body",
"kind": "call",
"body": "{\n \"session_token\": \"sess_test_1\",\n \"round_id\": \"r-1\",\n \"amount_minor\": 250,\n \"currency\": \"EUR\",\n \"idempotency_key\": \"ik_vec_1\"\n}",
"secrets": [
"whsec_testvector_current"
],
"header": "t=1760000000,v1=d8411bce6961d2f77b348f346926de0bfe2d80a65dc0a862b75c0acfabf6a0d7",
"now": 1760000010,
"expect": "reject"
},
{
"id": "call-wrong-secret",
"kind": "call",
"body": "{\"session_token\":\"sess_test_1\",\"round_id\":\"r-1\",\"amount_minor\":250,\"currency\":\"EUR\",\"idempotency_key\":\"ik_vec_1\"}",
"secrets": [
"whsec_testvector_previous"
],
"header": "t=1760000000,v1=d8411bce6961d2f77b348f346926de0bfe2d80a65dc0a862b75c0acfabf6a0d7",
"now": 1760000010,
"expect": "reject"
},
{
"id": "call-rotation-previous-secret",
"kind": "call",
"body": "{\"session_token\":\"sess_test_1\",\"round_id\":\"r-1\",\"amount_minor\":250,\"currency\":\"EUR\",\"idempotency_key\":\"ik_vec_1\"}",
"secrets": [
"whsec_testvector_current",
"whsec_testvector_previous"
],
"header": "t=1760000000,v1=973285fb0841d56ee493e3c53a78cafdf00bbc42ba50a3775ac870485926788b",
"now": 1760000010,
"expect": "accept"
},
{
"id": "call-uppercase-hex",
"kind": "call",
"body": "{\"session_token\":\"sess_test_1\",\"round_id\":\"r-1\",\"amount_minor\":250,\"currency\":\"EUR\",\"idempotency_key\":\"ik_vec_1\"}",
"secrets": [
"whsec_testvector_current"
],
"header": "t=1760000000,v1=D8411BCE6961D2F77B348F346926DE0BFE2D80A65DC0A862B75C0ACFABF6A0D7",
"now": 1760000010,
"expect": "reject"
},
{
"id": "call-two-v1-one-valid",
"kind": "call",
"body": "{\"session_token\":\"sess_test_1\",\"round_id\":\"r-1\",\"amount_minor\":250,\"currency\":\"EUR\",\"idempotency_key\":\"ik_vec_1\"}",
"secrets": [
"whsec_testvector_current"
],
"header": "t=1760000000,v1=0000000000000000000000000000000000000000000000000000000000000000,v1=d8411bce6961d2f77b348f346926de0bfe2d80a65dc0a862b75c0acfabf6a0d7",
"now": 1760000010,
"expect": "accept"
},
{
"id": "call-missing-t",
"kind": "call",
"body": "{\"session_token\":\"sess_test_1\",\"round_id\":\"r-1\",\"amount_minor\":250,\"currency\":\"EUR\",\"idempotency_key\":\"ik_vec_1\"}",
"secrets": [
"whsec_testvector_current"
],
"header": "v1=d8411bce6961d2f77b348f346926de0bfe2d80a65dc0a862b75c0acfabf6a0d7",
"now": 1760000010,
"expect": "reject"
},
{
"id": "call-webhook-format",
"kind": "call",
"body": "{\"session_token\":\"sess_test_1\",\"round_id\":\"r-1\",\"amount_minor\":250,\"currency\":\"EUR\",\"idempotency_key\":\"ik_vec_1\"}",
"secrets": [
"whsec_testvector_current"
],
"header": "sha256=14533d630f6ae57fd76cfa834d392dd0a432ad234d72748a7ca4e8d273dff8b3",
"now": 1760000010,
"expect": "reject"
},
{
"id": "webhook-valid",
"kind": "webhook",
"body": "{\"deliveryId\":\"dlv_vec_1\",\"event\":\"player.updated\",\"data\":{\"player_external_id\":\"oh-test-1\"}}",
"secrets": [
"whsec_testvector_current"
],
"header": "sha256=0ce6f2aa4171e1319123393b3cbb240dd8be7cc8a7eb575f4aba7e8d3c84b65f",
"expect": "accept"
},
{
"id": "webhook-rotation-previous-secret",
"kind": "webhook",
"body": "{\"deliveryId\":\"dlv_vec_1\",\"event\":\"player.updated\",\"data\":{\"player_external_id\":\"oh-test-1\"}}",
"secrets": [
"whsec_testvector_current",
"whsec_testvector_previous"
],
"header": "sha256=e1282a87ea22ec7babcb13ea0f32fdc5c885fe55078ebb40d56aeecf93586117",
"expect": "accept"
},
{
"id": "webhook-body-changed",
"kind": "webhook",
"body": "{\"deliveryId\":\"dlv_vec_2\",\"event\":\"player.updated\",\"data\":{\"player_external_id\":\"oh-test-1\"}}",
"secrets": [
"whsec_testvector_current"
],
"header": "sha256=0ce6f2aa4171e1319123393b3cbb240dd8be7cc8a7eb575f4aba7e8d3c84b65f",
"expect": "reject"
},
{
"id": "webhook-uppercase-prefix",
"kind": "webhook",
"body": "{\"deliveryId\":\"dlv_vec_1\",\"event\":\"player.updated\",\"data\":{\"player_external_id\":\"oh-test-1\"}}",
"secrets": [
"whsec_testvector_current"
],
"header": "SHA256=0ce6f2aa4171e1319123393b3cbb240dd8be7cc8a7eb575f4aba7e8d3c84b65f",
"expect": "reject"
},
{
"id": "webhook-timestamped-format",
"kind": "webhook",
"body": "{\"deliveryId\":\"dlv_vec_1\",\"event\":\"player.updated\",\"data\":{\"player_external_id\":\"oh-test-1\"}}",
"secrets": [
"whsec_testvector_current"
],
"header": "t=1760000000,v1=5c25a1bcdea72d909d59a3665c418c4f06ee0693189bcf49931a6cf6726cad80",
"expect": "reject"
}
]