Node.js helpers
The helper functions of @piedap/consent/server for webhook signatures, identity proofs, fingerprints, capture hashes, skipped imports, cookies and receipts.
@piedap/consent/server exports helper functions next to the client. They check webhook signatures, compute proofs, fingerprints and capture hashes, spot skipped imports, read the consent cookie and verify receipts.
Requirements#
- Node.js 18 or later.
- For
verifyWebhook(): the endpoint's signing secret,whsec_…. - For
identityProof(): the key's identity secret,idv_…. - For
subjectFingerprint()andcaptureReferenceHash(): the project's fingerprint key,sfk_….
Install#
The helpers ship in the same package as the client. See Install.
Import the helpers#
import {
verifyWebhook,
identityProof,
subjectFingerprint,
captureReferenceHash,
isSkippedImport,
parseConsentCookie,
consentFromCookieHeader,
verifyReceipt,
canonicalize,
} from '@piedap/consent/server';The helpers are plain functions. They make no network calls and need no client.
Functions#
verifyWebhook()#
Checks that a webhook delivery is genuine and recent. Pass one object with these fields:
| Parameter | Type | Required or default | Description |
|---|---|---|---|
payload |
string | Buffer |
Required | The raw request body, not parsed JSON. |
signature |
string | null | undefined |
Required | The x-piedap-signature header. |
timestamp |
string | null | undefined |
Required | The x-piedap-timestamp header. |
secret |
string |
Required | The endpoint's signing secret. |
toleranceSeconds |
number |
300 |
How far the timestamp may be from now, in either direction. |
now |
number |
Date.now() |
The current time in milliseconds. |
Returns boolean: true only when the signature matches the raw body and the timestamp is within the tolerance.
Errors A payload that is not a string or a Buffer, such as parsed JSON, can throw a TypeError. A missing header, an empty secret or a malformed value gives false.
import {
SIGNATURE_HEADER,
TIMESTAMP_HEADER,
verifyWebhook,
} from '@piedap/consent/server';
const ok = verifyWebhook({
payload: req.body, // the raw bytes
signature: req.get(SIGNATURE_HEADER),
timestamp: req.get(TIMESTAMP_HEADER),
secret: process.env.PIEDAP_WEBHOOK_SECRET,
});See Verify signatures.
identityProof()#
Computes the proof that lets a browser or an app record consent under a signed-in person.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
identitySecret |
string |
Required | The key's identity secret. |
subjectType |
string |
Required | EMAIL, USER_ID, PHONE or DEVICE_ID. |
subjectId |
string |
Required | The identifier, exactly as the page or app sends it. |
Returns string: the hex HMAC-SHA256 of <subjectType>:<subjectId>.
Errors Throws a TypeError when identitySecret is missing, such as an unset environment variable. The other arguments are not checked, so keep the order: type first, then the identifier.
const proof = identityProof(
process.env.PIEDAP_IDENTITY_SECRET,
'USER_ID',
user.id,
);See Signed-in visitors.
subjectFingerprint()#
Computes the fingerprint that receipts and webhooks carry for a person.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
fingerprintKey |
string |
Required | The project's fingerprint key. |
subjectId |
string |
Required | The identifier you recorded consent under. |
subjectType |
SubjectType |
None | The kind of identifier. Without it, a value that contains @ is treated as an email address. |
Returns string: the hex HMAC-SHA256 of the identifier. An email address is put in lower case first.
Errors Throws a TypeError when fingerprintKey is missing. Here the identifier comes before the type.
const fingerprint = subjectFingerprint(
process.env.PIEDAP_FINGERPRINT_KEY,
'usr_8f2K1xQz',
'USER_ID',
);captureReferenceHash()#
Computes the agentRefHash or locationRefHash that a capture context carries, so you can match it to your own staff id or site code.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
fingerprintKey |
string |
Required | The project's fingerprint key. |
kind |
'agent' | 'location' |
Required | agent for a member of staff, location for a branch or site. |
reference |
string |
Required | Your id, as sent in capture.agentRef or capture.locationRef. |
Returns string: the hex HMAC-SHA256 of <kind>:<reference>, with the reference trimmed and in lower case.
Errors Throws a TypeError when fingerprintKey or reference is missing, or kind is neither value.
const agentRefHash = captureReferenceHash(
process.env.PIEDAP_FINGERPRINT_KEY,
'agent',
'EMP-40213',
);See Record consent taken in a branch, by phone or on paper.
isSkippedImport()#
Tells a write with collectedAt that the records overrule from a real error.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
error |
unknown |
Required | What a recordConsent() call threw. |
Returns boolean: true for a ConsentApiError with status 409 and one of the codes in SKIPPED_IMPORT_CODES.
Errors None.
try {
await client.recordConsent(row);
} catch (error) {
if (!isSkippedImport(error)) throw error;
skipped.push({ row: row.idempotencyKey, code: error.code });
}See Bring in consent given earlier.
parseConsentCookie()#
Reads the two cookie values the browser runtime wrote.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
value |
string | null | undefined |
Required | The piedap_consent cookie value. |
partnersValue |
string | null |
None | The piedap_partners cookie value. |
Returns a ConsentCookie, or null.
Errors None. null comes back when the visitor has not answered or the cookie is malformed.
const consent = parseConsentCookie(
req.cookies.piedap_consent,
req.cookies.piedap_partners,
);consentFromCookieHeader()#
Finds both consent cookies in a whole Cookie request header and reads them.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
header |
string | null | undefined |
Required | The Cookie header. |
Returns a ConsentCookie, or null.
Errors None. null comes back when the visitor has not answered or the cookie is malformed.
app.get('/', (req, res) => {
const consent = consentFromCookieHeader(req.get('cookie'));
const analytics = consent?.granted('analytics') ?? false;
const matomo = consent?.granted('analytics', 'matomo') ?? false;
res.render('home', { analytics, matomo });
});verifyReceipt()#
Checks an Ed25519 receipt against the deployment's public keys without calling the API.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
signed |
SignedReceipt |
Required | A receipt from receipt() or exportSubject(). |
keys |
{ keys: Jwk[] } | Jwk[] |
Required | The keys from publicKeys(). |
Returns boolean: true when the signature matches.
Errors None. It returns false for anything it cannot vouch for, including older HMAC-SHA256 receipts. Only the issuing deployment can check those, through POST /sdk/v1/receipts/verify.
const keys = await client.publicKeys();
const pack = await client.exportSubject('usr_8f2K1xQz', 'USER_ID');
for (const signed of pack.receipts) {
console.log(signed.receipt.consentId, verifyReceipt(signed, keys));
}canonicalize()#
Returns the exact string a receipt signature covers.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
value |
unknown |
Required | The receipt document. |
Returns string: JSON with the keys sorted at every depth and undefined values dropped.
Errors None.
const signedText = canonicalize(signed.receipt);You need it only to check a signature with your own code.
Types#
ConsentCookie#
| Field | Type | Description |
|---|---|---|
consentId |
string | null |
The record's id. null for the short time between the visitor's answer and the API confirming it. |
formVersion |
number |
The version of the form that was answered. |
decisions |
Record<string, boolean> |
Purpose key to allowed. |
decidedAt |
Date |
When the visitor answered. |
partners |
Record<string, string[]> |
Purpose key to the partner keys the visitor refused. |
granted(purposeKey, partnerKey?) |
boolean |
Whether the purpose is allowed. With partnerKey, whether that partner is allowed too. |
The cookie is not evidence
A visitor controls their own cookies. Use the cookie to decide what to render. Where the answer must be right, read the record with getConsent().
The cookie formats are in Cookies and browser storage. SignedReceipt and Jwk are in Types.
Constants#
| Name | Value |
|---|---|
SIGNATURE_HEADER |
x-piedap-signature |
TIMESTAMP_HEADER |
x-piedap-timestamp |
USER_AGENT |
piedap-consent-node/1.0.7 |
CHECK_TIMEOUT_MS |
1000, how long checkConsent() waits unless told otherwise |
SKIPPED_IMPORT_CODES |
CONSENT_WITHDRAWN_ON_RECORD, CONSENT_ALREADY_ON_RECORD, CONSENT_NEWER_ON_RECORD, CONSENT_ERASED_ON_RECORD, CONSENT_HELD_UNDER_OTHER_IDENTIFIER |