Skip to content

feat(core): add webhook signature verification helpers (resolves #5101) - #5106

Open
eslam-reda-div wants to merge 4 commits into
adonisjs:7.xfrom
eslam-reda-div:feat/webhook-signature-verification
Open

feat(core): add webhook signature verification helpers (resolves #5101)#5106
eslam-reda-div wants to merge 4 commits into
adonisjs:7.xfrom
eslam-reda-div:feat/webhook-signature-verification

Conversation

@eslam-reda-div

Copy link
Copy Markdown

Resolves #5101

What

  • Add webhook verification helpers, including Standard Webhooks support (v1 HMAC + v1a ed25519).
  • Provide a configurable HMAC verifier for bespoke/non-standard provider schemes.
  • Add test coverage for standard verification, missing headers, tolerance checks, and custom signatures.

Why

  • Gives AdonisJS a secure, reusable verification layer while still supporting provider-specific schemes.
  • Addresses Standard Webhooks interoperability without forcing a one-size-fits-all API.

How

  • createStandardWebhookVerifier implements the Standard Webhooks signature rules and tolerance checks.
  • createWebhookVerifier allows custom header parsing and payload signing for non-standard providers.
  • Structured results + WebhookVerificationError for ergonomic error handling.

Testing

  • npm test

Notes

  • Standard Webhooks keys accept whsec_ and whpk_ prefixes; format: 'raw' is supported for raw secrets.
Copilot AI review requested due to automatic review settings May 11, 2026 20:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class webhook signature verification utilities to @adonisjs/core/helpers, covering both the Standard Webhooks spec (v1 HMAC + v1a ed25519) and a configurable HMAC verifier for provider-specific schemes.

Changes:

  • Introduces createStandardWebhookVerifier (Standard Webhooks: required headers + tolerance checks + v1/v1a verification).
  • Introduces createWebhookVerifier for configurable/custom HMAC verification (custom header parsing + signed payload builder).
  • Adds tests for Standard Webhooks (v1 HMAC) + missing headers + tolerance, and a basic custom HMAC verifier test.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/helpers/webhooks.ts Implements Standard Webhooks + configurable webhook signature verification helpers and error type.
src/helpers/main.ts Re-exports the new webhook helpers from the public helpers entrypoint.
tests/webhooks.spec.ts Adds tests for Standard Webhooks HMAC flow, tolerance/missing-header failures, and a custom HMAC verifier.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/webhooks.spec.ts
Comment on lines +15 to +37
test('verify Standard Webhooks signatures', ({ assert }) => {
const rawSecret = 'super-secret'
const secret = `whsec_${Buffer.from(rawSecret).toString('base64')}`
const webhookId = 'msg_2KWPBgLlAfxdpx2AI54pPJ85f4W'
const timestamp = 1700000000
const payload = JSON.stringify({ type: 'user.created' })

const signedPayload = `${webhookId}.${timestamp}.${payload}`
const signature = createHmac('sha256', Buffer.from(rawSecret))
.update(signedPayload)
.digest('base64')

const verifier = createStandardWebhookVerifier(secret, { now: () => timestamp })
const result = verifier.verify(payload, {
'webhook-id': webhookId,
'webhook-timestamp': String(timestamp),
'webhook-signature': `v1,${signature}`,
})

assert.isTrue(result.isValid)
assert.equal(result.webhookId, webhookId)
assert.equal(result.timestamp, timestamp)
})
Comment thread src/helpers/webhooks.ts
Comment on lines +440 to +445
const rawPublicKey = Buffer.from(secretValue.slice(STANDARD_PUBLIC_PREFIX.length), 'base64')
const spkiKey = Buffer.concat([ED25519_SPKI_PREFIX, rawPublicKey])
return {
type: 'ed25519',
key: createPublicKey({ key: spkiKey, format: 'der', type: 'spki' }),
}
@eslam-reda-div
eslam-reda-div requested a review from Copilot May 12, 2026 15:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread src/helpers/webhooks.ts Outdated
Comment on lines +439 to +452
if (format !== 'raw' && secretValue.startsWith(STANDARD_PUBLIC_PREFIX)) {
const rawPublicKey = Buffer.from(secretValue.slice(STANDARD_PUBLIC_PREFIX.length), 'base64')
try {
const spkiKey = Buffer.concat([ED25519_SPKI_PREFIX, rawPublicKey])
return {
type: 'ed25519',
key: createPublicKey({ key: spkiKey, format: 'der', type: 'spki' }),
}
} catch {
throw new Error(
'Invalid Standard Webhooks public key. Expected whpk_ base64 encoded ed25519 key.'
)
}
}
Comment thread src/helpers/webhooks.ts Outdated
Comment on lines +228 to +238
const verifyOrThrow = (
payload: WebhookPayload,
headers: WebhookHeaders
): WebhookVerificationSuccess => {
const result = verify(payload, headers)
if (!result.isValid) {
throw new WebhookVerificationError(result.reason!, WEBHOOK_ERROR_MESSAGES[result.reason!])
}

return result as WebhookVerificationSuccess
}
Comment thread src/helpers/webhooks.ts Outdated
): WebhookVerificationSuccess => {
const result = verify(payload, headers)
if (!result.isValid) {
throw new WebhookVerificationError(result.reason!, WEBHOOK_ERROR_MESSAGES[result.reason!])

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread src/helpers/webhooks.ts Outdated
Comment on lines +212 to +217
const signedPayload = options.buildSignedPayload({
payload,
headers: normalizedHeaders,
webhookId,
timestamp,
})
Comment thread src/helpers/webhooks.ts
Comment on lines +445 to +449
const stripped = secretValue.startsWith(STANDARD_SECRET_PREFIX)
? secretValue.slice(STANDARD_SECRET_PREFIX.length)
: secretValue
return { type: 'hmac', key: Buffer.from(stripped, 'base64') }
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants