Skip to content

Overview

The next section covers manual signing and sending of requests, however we have also authored this AuthHelpers class with signAndSend to simplify the process.

Please copy and use this in your codebase, it will be referenced throughout the docs.

import crypto from "crypto";
 
export type DefinitiveApiKey = `dpka_${string}_${string}_${string}_${string}`;
export type DefinitiveApiSecret = `dpks_${string}`;
export type HTTPMethod = "GET" | "POST";
 
export type PrehashParams = {
  method: HTTPMethod;
  path: string;
  timestamp: string;
  headers: Record<string, string | number | object>;
  queryParams?: Record<string, string>;
  body?: string;
};
 
export class AuthHelpers {
  /**
   * Signs the message using HMAC SHA-256.
   * @param key - The secret key used for HMAC generation.
   * @param message - The message to be signed.
   * @returns HMAC signature.
   */
  static generateHMAC(key: string, message: string): string {
    return crypto.createHmac("sha256", key).update(message).digest("hex");
  }
 
  /**
   * Client-side signing of the prehash message.
   * @param apiSecret - The secret key.
   * @param message - The message to sign.
   * @returns Signed message.
   */
  static clientSignMessage(apiSecret: DefinitiveApiSecret, message: string) {
    const secret = apiSecret.replace("dpks_", "");
    return AuthHelpers.generateHMAC(secret, message);
  }
 
  /**
   * Generates a prehash string to sign the request.
   * @param params - The request parameters.
   * @returns The prehash string.
   */
  static preparePrehash({
    method,
    timestamp,
    path,
    headers,
    queryParams,
    body,
  }: PrehashParams) {
    const filteredHeaders = Object.entries(headers)
      .filter(([key]) => key.toLowerCase().startsWith("x-definitive-"))
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([key, value]) => `${key}:${JSON.stringify(value)}`)
      .join(",");
 
    if (filteredHeaders.length > 2) {
      throw new Error("Headers are too long - are you adding a new header?");
    }
 
    const queryParamsString = new URLSearchParams(queryParams).toString();
    const bodyString = body ?? "";
 
    return `${method}:${path}?${queryParamsString}:${timestamp}:${filteredHeaders}${bodyString}`;
  }
 
  /**
   * Prepares, signs, and sends the request.
   * @param apiKey - The API key.
   * @param apiSecret - The API secret.
   * @param path - The request path.
   * @param method - The HTTP method.
   */
  static async signAndSend<TResponse, TBody = void>({
    path,
    method,
    queryParams = {},
    body,
  }: {
    path: string;
    method: "GET" | "POST" | "DELETE";
    queryParams?: Record<string, string>;
    body?: TBody;
  }) {
    const timestamp = Date.now().toString();
    const apiKey = process.env.API_KEY as string;
    const apiSecret = process.env.API_SECRET as DefinitiveApiSecret;
 
    const headers = {
      "x-definitive-api-key": apiKey,
      "x-definitive-timestamp": timestamp,
    };
 
    // Prepare prehash message
    const message = AuthHelpers.preparePrehash({
      method,
      path,
      timestamp,
      headers,
      queryParams,
      body: JSON.stringify(body),
    });
 
    // Generate signature
    const signature = AuthHelpers.clientSignMessage(apiSecret, message);
 
    // Send request
    const baseURL = "https://ddp.definitive.fi";
    const queryString = new URLSearchParams(queryParams).toString();
    const url = queryString
      ? `${baseURL}${path}?${queryString}`
      : `${baseURL}${path}`;
 
    const result = await fetch(url, {
      method,
      headers: {
        ...headers,
        "x-definitive-signature": signature,
      },
      body: JSON.stringify(body),
    });
 
    const json = await result.json();
 
    return json as TResponse;
  }
}