
Research
/Security News
Toptal’s GitHub Organization Hijacked: 10 Malicious Packages Published
Threat actors hijacked Toptal’s GitHub org, publishing npm packages with malicious payloads that steal tokens and attempt to wipe victim systems.
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.
{
"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"
}
}
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.
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:
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 |
message_id
for deduplication and idempotency, preventing the same event from being processed multiple times.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.
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.
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.
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.
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);
})();
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);
}
})();
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.
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.');
})();
create_vhook_keypair(options)
Generates a key pair for use with VHooks, supporting customizable algorithms and parameters.
options
(Object):
key_type
(string, default 'RSA'
): The type of key to generate ('RSA'
, 'EC'
, or 'oct'
).key_size
(number, default 2048
): 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).Promise
that resolves to an object containing keys in both JWK and PEM formats.const keys = await vhook.create_vhook_keypair({
key_type: 'RSA',
key_size: 3072,
algorithm: 'RS512',
usage: 'sig',
});
console.log(keys);
create_vhook(options, privateKey)
Creates a VHook (signed JWT) using the provided payload and private key.
options
(Object): The VHook payload, using human-friendly names:
issuer
(string, required): The issuer of the VHook (mapped to iss
).audience
(string, required): The intended audience of the VHook (mapped to aud
).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 to jti
), a UUID is generated if not provided.expiration
(Date|number, optional): Expiration time as a Date
, 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.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);
decode_vhook(received_vhook, publicKey, jwtVerifyOptions)
Decodes and verifies a VHook using the provided public key and optional JWT verification options.
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 for jsonwebtoken.verify
, such as:
algorithms
(Array): List of allowed algorithms.audience
(string): Expected audience (aud
).issuer
(string): Expected issuer (iss
).ignoreExpiration
(boolean): Ignore the exp
claim (default false
).raw_token
: The raw JWT payload.const decoded = vhook.decode_vhook(received_vhook, publicKey, { algorithms: ['RS256'] });
console.log(decoded);
decode_vhook_without_validation(vhook)
Decodes a VHook (JWT) without verifying its signature.
vhook
(string): The VHook token (JWT string).const decoded = vhook.decode_vhook_without_validation(vhookToken);
console.log(decoded);
send_vhook(my_vhook, url, options)
Sends a VHook token to a specified URL via a POST request.
my_vhook
(string): The VHook token (JWT string).url
(string): The URL to send the VHook to.options
(Object, optional):
fireAndForget
(boolean, default false
): If true, returns immediately without waiting for the response.Promise
resolving to the status code, headers, and response body (if not in fireAndForget
mode).vhook.send_vhook(my_vhook, 'https://receiver.com/webhook', { fireAndForget: true });
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.
params
(Object): Payload data with human-friendly field names such as issuer
, audience
, expiration
, etc.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 }
});
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.
payload
(Object): The decoded JWT payload.const extractedPayload = vhook.extract_vhook_payload(decodedJWT);
console.log(extractedPayload);
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 });
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:
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.
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.
}
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 same message_id
(mapped to jti
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 string unknown
if there was a failure decoding the vhook and the message_id
was not available.
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., an order_id
, user_id
) or additional error details when status
is "failed"
. This field is optional but allows flexibility for more complex system-specific behavior. Note that null
is not a valid detail
value. If you do not have a valid object to return, omit the detail
field altogether.
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 processingIt's important to clarify that Vhooks don't require any specific status, the above are suggestions of good practices.
{
"status": "ok",
"message": "Request received and processed successfully",
"detail": { "order_id": 92 },
"message_id": "123e4567-e89b-12d3-a456-426614174000"
}
{
"status": "ok",
"message_id": "123e4567-e89b-12d3-a456-426614174000"
}
{
"status": "failed",
"message": "Order not found",
"detail": { "order_id": 93 },
"message_id": "123e4567-e89b-12d3-a456-426614174000"
}
{
"status": "failed",
"message_id": "123e4567-e89b-12d3-a456-426614174000"
}
Always include message_id
: Ensure that the message_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 meaningful message
in case of failure to provide more information about the error and help with troubleshooting.
Use the detail
field for system-specific information: The detail
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.
Below is an example of setting up an Express.js server to receive, decode, and verify a VHook.
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');
});
node server.js
exp
(expiration time) and iat
(issued at time) claims help prevent replay attacks. Ensure your system clocks are synchronized.fireAndForget
mode, be aware that delivery is not guaranteed. Avoid using this mode for critical notifications.npm install vhook-js
For any questions, issues, or suggestions, please open an issue on the Git repository.
This project is licensed under the MIT License.
Contributions are welcome! Please submit a pull request or open an issue for any bugs or feature requests.
FAQs
Verifyable Webhooks
We found that vhook-js demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
/Security News
Threat actors hijacked Toptal’s GitHub org, publishing npm packages with malicious payloads that steal tokens and attempt to wipe victim systems.
Research
/Security News
Socket researchers investigate 4 malicious npm and PyPI packages with 56,000+ downloads that install surveillance malware.
Security News
The ongoing npm phishing campaign escalates as attackers hijack the popular 'is' package, embedding malware in multiple versions.