vhook-js
v1.0.4
Published
Verifyable Webhooks
Downloads
14
Maintainers
Readme
VHook-JS
Introducing VHook-JS
VHook-JS is a Node.js module designed to make working with VHooks (Verifiable Webhooks) super-easy. VHooks build upon traditional webhooks by adding strong verification through the use of JSON Web Tokens (JWT), allowing you to securely create, send, and verify webhook events with minimal effort.
What a VHook looks like in your code
{
"issuer": "https://yourservice.com",
"audience": "https://receiving-service.com/webhook-endpoint",
"expiration": 1695858000, // UNIX timestamp
"issued_at": 1695854400, // UNIX timestamp
"origin": "order_processing",
"event": "order.created",
"message_id": "b7a2e6c4-7df6-4e4b-9bda-093d346f3024",
"data": {
"order_id": "98765",
"amount": 150.00,
"currency": "USD"
}
}
What are VHooks?
VHooks, short for Verifiable Webhooks, are designed to relay information and events between systems with built-in sender verification and data integrity checks using JSON Web Tokens (JWT). This evolution of traditional webhooks ensures that every message comes from a legitimate source and has not been tampered with in transit.
Where traditional webhooks often leave key details—such as verification of the sender and ensuring data integrity—up to individual implementation, VHooks standardize these processes, providing a built-in way to guarantee that the notification comes from a legitimate source and hasn't been tampered with in transit.
With VHook-JS, you can easily create, send, decode, and verify VHooks in your Node.js applications, utilizing all the strengths of JWT while keeping the integration simple.
Why VHooks?
While webhooks are useful, they often lack the structure and security needed for high-trust scenarios. Many systems implement custom verification methods, if any at all, leading to inconsistencies and vulnerabilities. With VHooks, developers get a secure and standard way to transmit event data between systems while solving key issues with traditional webhooks, such as:
- Source Verification: VHooks use JWT to ensure the message originates from the expected source.
- Data Integrity: By signing the payload, VHooks ensure that the data has not been tampered with in transit.
- Simplicity: With a standardized format for sending and verifying messages, developers can implement VHooks quickly and with minimal custom logic.
Webhooks and VHooks compared
| Feature | Webhooks | VHooks (Verifiable Webhooks) |
|---------------------|-----------------------------|-------------------------------|
| Sender Verification | Custom or none | JWT-based identity |
| Tamper Resistance | Optional, custom signing | Built-in JWT signatures |
| Data Integrity | Limited | Ensured with JWT |
| Relay Format | Custom | Standardized (JSON + JWT) |
| Idempotency | Requires custom implementation | Built-in with message_id
for deduplication |
| Security | Varies by implementation | Strong, standardized verification and encryption |
Key Features of VHooks:
- Tamper-Resistant: Every VHook is signed using JWT, ensuring the data hasn't been altered during transmission.
- Strong Verification: JWT allows the receiver to validate the identity of the sender, solving the problem of spoofed webhooks.
- Standardized Format: VHooks are sent via HTTP POST as a simple JSON payload, making routing and handling easy in any receiving system.
- Idempotency: Each VHook contains a
message_id
for deduplication and idempotency, preventing the same event from being processed multiple times. - Compatible and Extendable: VHooks can be used with existing systems and APIs that already work with webhooks, thanks to their flexible payload structure.
Idempotency?
Idempotency ensures that repeated processing of the same message results in
the same outcome, preventing duplicate actions. For example, imagine a VHook is
sent to update an order status to "shipped." Without idempotency, if the same
webhook is accidentally processed twice, the system might send two shipping
confirmations or charge the customer twice. VHooks solve this by including a
unique message_id
for each event. When a system receives a VHook, it can
check the message_id
and ignore any duplicates, ensuring the action is only
performed once, even if the VHook is accidentally sent multiple times.
The JWT Connection
VHooks leverage JSON Web Tokens (JWT) to implement most of the security and verification features, ensuring data integrity, sender authenticity, and tamper resistance. This design choice was intentional: rather than reinventing the wheel, we wanted to elevate traditional webhooks to the next level using a widely accepted and proven standard. JWTs are widely implemented and battle-tested across many programming languages, making them a robust and reliable solution. By utilizing JWT, VHooks gain the power of a trusted security framework, allowing developers to rely on well-established practices for secure message transmission. VHooks are powerful because JWT is awesome, bringing enhanced security without the complexity of custom solutions.
VHooks on the Network
When a VHook is sent over HTTP, it is delivered via an HTTP POST request with a JSON body in the following format:
{ "vhook": "vhookjwtdata" }
This structure makes encoding and processing straightforward. Since VHook tokens are self-contained, they can also be stored or sent using other protocols, such as WebSockets, without modification. However, when using HTTP, this standard JSON format should be expected for consistency and ease of integration.
Generating Keys
One challenge of working with JWTs has always been the somewhat confusing and
arcane process of generating new keys, often involving tools like OpenSSL. The
vhook-js module solves this problem by providing a simple, single-function
solution to generate key pairs for use with VHooks. With just one call to
vhook.create_vhook_keypair(options)
, you can create both public and private
keys, ready to use, without needing to navigate complex key generation
processes.
Quick Start Examples
Creating a VHook
Here's how to create a VHook using default settings:
const vhook = require('vhook-js');
const fs = require('fs');
(async () => {
// Load your private key (PEM format)
const privateKey = fs.readFileSync('private.pem', 'utf8');
// Create the VHook payload
const payload = {
issuer: 'https://yourservice.com',
audience: 'https://receiving-service.com/webhook-endpoint',
expiration: Math.floor(Date.now() / 1000) + 3600, // Expires in 1 hour
issued_at: Math.floor(Date.now() / 1000),
origin: 'order',
event: 'order.created',
data: {
order_id: '98765',
amount: 150.00,
currency: 'USD',
},
};
// Create the VHook (signed JWT)
const my_vhook = vhook.create_vhook(payload, privateKey);
console.log('VHook:', my_vhook);
})();
Decoding and Validating a VHook
const vhook = require('vhook-js');
const fs = require('fs');
(async () => {
// Load the sender's public key (PEM format)
const publicKey = fs.readFileSync('public.pem', 'utf8');
// Assume you have received a VHook
const received_vhook = '...'; // The VHook string received
try {
// Decode and verify the VHook
const decodedPayload = vhook.decode_vhook(received_vhook, publicKey);
console.log('Verified VHook Payload:', decodedPayload);
// Proceed with processing the payload
} catch (err) {
console.error('Failed to verify VHook:', err.message);
}
})();
Key Generation Made Easy
One of the challenges in dealing with JWTs is creating and managing cryptographic keys. VHook-JS simplifies this by providing a function to generate key pairs that are ready to use.
Generating and Saving a Key Pair
const vhook = require('vhook-js');
const fs = require('fs');
(async () => {
// Generate a key pair (defaults to RSA 2048 bits)
const keys = await vhook.create_vhook_keypair();
// Save the private key to a file
fs.writeFileSync('private.pem', keys.privateKey.pem);
// Save the public key to a file
fs.writeFileSync('public.pem', keys.publicKey.pem);
console.log('Key pair generated and saved to disk.');
})();
API Documentation
1. create_vhook_keypair(options)
Generates a key pair for use with VHooks, supporting customizable algorithms and parameters.
Parameters:
options
(Object):key_type
(string, default'RSA'
): The type of key to generate ('RSA'
,'EC'
, or'oct'
).key_size
(number, default2048
): Key size in bits (for RSA and oct keys).curve
(string, default'P-256'
): The elliptic curve name (for EC keys, e.g.,'P-256'
,'P-384'
,'P-521'
).algorithm
(string, default'RS256'
): The algorithm intended for use with the key.usage
(string, default'sig'
): The intended usage of the key ('sig'
for signature or'enc'
for encryption).key_id
(string): A unique identifier for the key (optional).
Returns:
- A
Promise
that resolves to an object containing keys in both JWK and PEM formats.
Example:
const keys = await vhook.create_vhook_keypair({
key_type: 'RSA',
key_size: 3072,
algorithm: 'RS512',
usage: 'sig',
});
console.log(keys);
2. create_vhook(options, privateKey)
Creates a VHook (signed JWT) using the provided payload and private key.
Parameters:
options
(Object): The VHook payload, using human-friendly names:issuer
(string, required): The issuer of the VHook (mapped toiss
).audience
(string, required): The intended audience of the VHook (mapped toaud
).origin
(string, required): The origin of the event (e.g.,'customer'
,'order'
).event
(string, required): The type of event (e.g.,'customer.created'
).data
(Object, required): The event data payload.message_id
(string, optional): A unique message ID (mapped tojti
), a UUID is generated if not provided.expiration
(Date|number, optional): Expiration time as aDate
, UNIX timestamp, or seconds from now.issued_at
(Date|number, optional): Issued-at time, defaults to current time.not_before
(Date|number, optional): Not-before time.algorithm
(string, default'RS256'
): Signing algorithm (e.g.,'RS256'
).
privateKey
(string|Object): The private key in PEM format or JWK object used to sign the VHook.
Returns:
- The signed JWT (VHook) as a string.
Example:
const my_vhook = vhook.create_vhook({
issuer: 'https://yourservice.com',
audience: 'https://receiver.com/webhook',
origin: 'customer',
event: 'customer.updated',
data: { id: 123, name: 'Jane Doe' },
message_id: 'unique-id',
}, privateKey);
3. decode_vhook(received_vhook, publicKey, jwtVerifyOptions)
Decodes and verifies a VHook using the provided public key and optional JWT verification options.
Parameters:
received_vhook
(string): The VHook token (JWT string).publicKey
(string|Object): The public key in PEM format or JWK object used to verify the VHook.jwtVerifyOptions
(Object, optional): Options forjsonwebtoken.verify
, such as:algorithms
(Array): List of allowed algorithms.audience
(string): Expected audience (aud
).issuer
(string): Expected issuer (iss
).ignoreExpiration
(boolean): Ignore theexp
claim (defaultfalse
).
Returns:
- An object containing:
- The decoded and extracted payload with human-friendly field names.
raw_token
: The raw JWT payload.
Example:
const decoded = vhook.decode_vhook(received_vhook, publicKey, { algorithms: ['RS256'] });
console.log(decoded);
4. decode_vhook_without_validation(vhook)
Decodes a VHook (JWT) without verifying its signature.
Parameters:
vhook
(string): The VHook token (JWT string).
Returns:
- An object containing the decoded and extracted payload with human-friendly field names, and the raw token payload.
Example:
const decoded = vhook.decode_vhook_without_validation(vhookToken);
console.log(decoded);
5. send_vhook(my_vhook, url, options)
Sends a VHook token to a specified URL via a POST request.
Parameters:
my_vhook
(string): The VHook token (JWT string).url
(string): The URL to send the VHook to.options
(Object, optional):fireAndForget
(boolean, defaultfalse
): If true, returns immediately without waiting for the response.
Returns:
- A
Promise
resolving to the status code, headers, and response body (if not infireAndForget
mode).
Example:
vhook.send_vhook(my_vhook, 'https://receiver.com/webhook', { fireAndForget: true });
6. prepare_vhook_payload(params)
Prepares a VHook payload by mapping human-friendly parameter names to JWT field names and handles date-related fields.
This is used internally to vhook-js
and is not usually needed when working with vhooks, but is provided for
completeness.
Parameters:
params
(Object): Payload data with human-friendly field names such asissuer
,audience
,expiration
, etc.
Returns:
- The prepared JWT payload.
Example:
const payload = vhook.prepare_vhook_payload({
issuer: 'https://yourservice.com',
audience: 'https://receiver.com',
origin: 'order',
event: 'order.created',
data: { id: 987, amount: 150.0 }
});
7. extract_vhook_payload(payload)
Extracts the VHook payload by mapping JWT field names back to human-friendly
parameter names and converts timestamp fields to Date
objects. This is used
internally to vhook-js
and is not usually needed when working with vhooks,
but is provided for completeness.
Parameters:
payload
(Object): The decoded JWT payload.
Returns:
- The extracted payload with human-friendly field names.
Example:
const extractedPayload = vhook.extract_vhook_payload(decodedJWT);
console.log(extractedPayload);
Sending a VHook
const vhook = require('vhook-js');
const my_vhook = '...'; // The VHook you have created
const webhookUrl = 'https://receiving-service.com/webhook-endpoint';
// Send VHook and wait for the response
vhook.send_vhook(my_vhook, webhookUrl)
.then((response) => {
console.log('VHook sent successfully!');
console.log('Status Code:', response.statusCode);
console.log('Response Body:', response.body);
})
.catch((error) => {
console.error('Error sending VHook:', error);
});
// Or, send VHook in fire-and-forget mode
vhook.send_vhook(vhook, webhookUrl, { fireAndForget: true });
Fire-and-Forget Mode
When using fireAndForget: true
, the function returns immediately after
scheduling the request, without waiting for the response. This can improve
performance but comes with risks:
- No Delivery Confirmation: You won't know if the VHook was successfully received or if an error occurred.
- Use with Caution: Only use fire-and-forget mode when immediate feedback isn't critical, and you can tolerate potential delivery failures.
While Fire-and-Forget mode can improve performance, it should only be used in scenarios where the delivery of the VHook is not critical or where failures can be tolerated. For important workflows, waiting for confirmation of delivery is strongly recommended.
VHook Response Format
When a VHook is processed, the receiver should respond with a JSON object in the following format:
{
"status": "ok", // or "failed"
"message": "Request received", // Optional - A human-readable message providing additional context.
"detail": { "order_id": 92 }, // Optional - An object containing additional system-specific information.
"message_id": "unique-message-id" // Required - The message ID from the original VHook.
}
Required Fields:
status
: Indicates whether the VHook was processed successfully or if an error occurred."ok"
: The VHook was processed successfully."failed"
: There was an error processing the VHook.
message_id
: This is the samemessage_id
(mapped tojti
in JWT) from the VHook that was processed. Including this field ensures that the response is always linked to the correct VHook. It's important to note that this may also be the stringunknown
if there was a failure decoding the vhook and themessage_id
was not available.
Optional Fields:
Optional fields may be omitted entirely. If they are provided, they must be of the appropriate type.
message
: string, A human-readable message providing more information about the success or failure of the request. This can help clarify what happened during processing or what went wrong in case of an error.detail
: object, An object containing any additional data or context that the system wants to return. This could include information related to the processed event (e.g., anorder_id
,user_id
) or additional error details whenstatus
is"failed"
. This field is optional but allows flexibility for more complex system-specific behavior. Note thatnull
is not a validdetail
value. If you do not have a valid object to return, omit thedetail
field altogether.
HTTP Status
HTTP status codes returned may be set as appropriate in the receiving application. It is recommended that appropriate HTTP status codes used, for example 200->299 for success and 400+ for failure. Below are the suggested http status codes.
** Success **
200
: status 'ok', vhook processed correctly.202
: status 'ok', vhook received but will be processed later
** Failure **
400
: status 'failed', vhook could not be decoded (incorrect format or otherwise undecodable)401
: status 'failed', vhook could not be verified (bad signature, unknown sending entity)403
: status 'failed', vhook was decoded but is expired so could not be processed.422
: status 'failed', vhook was decoded and verified, but an error occurred during processing
It's important to clarify that Vhooks don't require any specific status, the above are suggestions of good practices.
Examples
1. Success Response with Message ID and Details:
{
"status": "ok",
"message": "Request received and processed successfully",
"detail": { "order_id": 92 },
"message_id": "123e4567-e89b-12d3-a456-426614174000"
}
2. Minimal Success Response with Message ID:
{
"status": "ok",
"message_id": "123e4567-e89b-12d3-a456-426614174000"
}
3. Failure Response with Message ID and Error Details:
{
"status": "failed",
"message": "Order not found",
"detail": { "order_id": 93 },
"message_id": "123e4567-e89b-12d3-a456-426614174000"
}
4. Minimal Failure Response with Message ID:
{
"status": "failed",
"message_id": "123e4567-e89b-12d3-a456-426614174000"
}
Best Practices for VHook Response:
Always include
message_id
: Ensure that themessage_id
is always included in the response, whether the VHook was processed successfully or not. This makes it easier to track the response and match it with the original VHook.Include a
message
field for failures: While optional, it’s recommended to include a meaningfulmessage
in case of failure to provide more information about the error and help with troubleshooting.Use the
detail
field for system-specific information: Thedetail
object provides a flexible way to return additional information. Use this field to include any event-specific or error-specific data that the sender might need.
Receiving and Verifying VHooks with Express.js
Below is an example of setting up an Express.js server to receive, decode, and verify a VHook.
Server Setup (server.js
)
const express = require('express');
const bodyParser = require('body-parser');
const vhook = require('vhook-js');
const fs = require('fs');
const app = express();
app.use(bodyParser.json()); // Parse JSON body
// Load the sender's public key (PEM format)
const publicKey = fs.readFileSync('public.pem', 'utf8');
app.post('/webhook-endpoint', (req, res) => {
const received_vhook = req.body.vhook;
if (!received_vhook) {
return res.status(400).json({
status: 'failed',
message: 'VHook missing',
message_id: null
});
}
try {
// Decode and verify the VHook
const payload = vhook.decode_vhook(received_vhook, publicKey);
// Extract the message ID (from the 'jti' field or 'message_id')
const messageId = payload.message_id;
// Process the payload (your business logic goes here)
console.log('Received VHook:', payload);
// Prepare the success response
const response = {
status: 'ok',
message: 'VHook received and processed successfully',
message_id: messageId
};
// Example: Add 'detail' only if relevant
const detail = { processed_event: payload.event }; // Example detail
if (Object.keys(detail).length > 0) {
response.detail = detail;
}
// Send the success response
res.status(200).json(response);
} catch (err) {
console.error('Failed to verify VHook:', err.message);
// Return the error response without detail if not applicable
res.status(401).json({
status: 'failed',
message: err.message || 'Invalid VHook',
message_id: payload?.message_id || 'unknown'
});
}
});
app.listen(3000, () => {
console.log('VHook receiver listening on port 3000');
});
Starting the Server
node server.js
Security Considerations
- Private Key Management: Keep your private keys secure. Do not expose them in client-side applications or logs. Regularly rotate keys and store them securely.
- Algorithm Specification: The module uses RS256 (RSA with SHA-256) by default for signing and verification. You can specify different algorithms and key types when generating key pairs.
- Time Validation: The
exp
(expiration time) andiat
(issued at time) claims help prevent replay attacks. Ensure your system clocks are synchronized. - Error Handling: Always handle errors appropriately, especially when verification fails. Do not disclose sensitive information in error messages.
- Fire-and-Forget Risks: When using
fireAndForget
mode, be aware that delivery is not guaranteed. Avoid using this mode for critical notifications. - Key Rotation: Your VHooks are only as secure as your keys. Ensure that private keys are rotated regularly to minimize the risk of compromised keys. Consider automating key rotation and updating public keys across your services.
Installation
npm install vhook-js
Feedback and Support
For any questions, issues, or suggestions, please open an issue on the Git repository.
License
This project is licensed under the MIT License.
Contributing
Contributions are welcome! Please submit a pull request or open an issue for any bugs or feature requests.