npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

vhook-js

v1.0.4

Published

Verifyable Webhooks

Downloads

347

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, 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).

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 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.

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 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).

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, default false): If true, returns immediately without waiting for the response.

Returns:

  • A Promise resolving to the status code, headers, and response body (if not in fireAndForget 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 as issuer, 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 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:

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

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 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.

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) and iat (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.