Request signing
Sign exact request bytes, preserve empty GET bodies, and rotate connector secrets.
@askdepth/audience-contractv0.1.2On this page
Audience Connector requests use HMAC-SHA256 with a connector-specific secret. This secret stays on servers; it is unrelated to the publishable browser write key.
Signature format
payload = timestamp + "." + rawBody
signature = "v1=" + hex(HMAC-SHA256(secret, payload))| Header | Value |
|---|---|
x-askdepth-timestamp | Epoch seconds, normally an integer string. |
x-askdepth-signature | The full v1=<hex digest> result, including the prefix exactly once. |
content-type | application/json for POST bodies; not required for bodyless GET. |
GET and HEAD sign the empty string. Their payload ends immediately after the dot. Signing {}, undefined, or an invented GET body fails verification.
Send a signed request
import {
sign,
signedBodyFor,
SIGNATURE_HEADER,
TIMESTAMP_HEADER,
} from '@askdepth/audience-contract';
const value = process.env.ASKDEPTH_SECRET;
if (!value) throw new Error('ASKDEPTH_SECRET must be configured');
const secret = Buffer.from(value, 'utf8');
const method = 'POST';
const rawBody = JSON.stringify({
criteria: { all: [] },
mapping: { id: 'externalId', email_address: 'email' },
});
const timestamp = Math.floor(Date.now() / 1000);
const response = await fetch(
'https://audience.example.com/askdepth/v1/candidates/count',
{
method,
redirect: 'manual',
headers: {
'content-type': 'application/json',
[TIMESTAMP_HEADER]: String(timestamp),
[SIGNATURE_HEADER]: sign(signedBodyFor(method, rawBody), timestamp, secret),
},
body: rawBody,
},
);Serialize once, sign that string, and send the same string. Do not parse and reserialize between signing and transmission. Whitespace, escaping, and number formatting can change bytes even when the parsed JSON value is equivalent. Do not forward signed headers across redirects to another host.
Verify the received bytes
import { signedBodyFor, verify } from '@askdepth/audience-contract';
function checkSignature(
method: string,
rawBody: string,
timestampHeader: string,
signatureHeader: string,
secret: Buffer,
) {
return verify(
signedBodyFor(method, rawBody),
timestampHeader,
signatureHeader,
secret,
);
}verify returns { valid: true } or { valid: false, reason }, where the reason is malformed, expired, or mismatch. It checks timestamp distance before HMAC comparison and uses a length check followed by constant-time comparison.
The published createConnector handler already performs verification. A custom verifier must preserve raw bytes, reject missing/invalid headers, and map failures to the contract's documented error envelopes.
Clock and rotation
REPLAY_WINDOW_SECONDS is 300: timestamps more than five minutes in the past or future are rejected. Use synchronized server clocks and seconds, not milliseconds. A retry must serialize/sign its request with a fresh timestamp.
This timestamp window is not a one-time nonce store. The signing helper does not deduplicate every previously seen request inside the window, and the signature does not bind a URL or HTTP method beyond the body convention. Keep the connector on HTTPS and expose only the specified read-only endpoints.
For rotation, configure previousSecret alongside the new secret during the agreed overlap, then remove it. The handler verifies against the active secret first and the previous secret second. Do not permanently retain old secrets or expose either value through browser configuration or logs.
