Full Example

API Signed Request Examples

const buffer = require('buffer');
const crypto = require('crypto');

/* Message Signing Helpers */

/**
 * Convert a string to an array buffer
 * @param {string} str
 * @returns {string}
 */
function str2ab(str) {
    const buf = new ArrayBuffer(str.length);
    const bufView = new Uint8Array(buf);
    for (let i = 0, strLen = str.length; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i);
    }
    return buf;
}

/**
 * Convert a string to base64-url safe encoding
 * @param {string} str
 * @returns {string}
 */
function base64UrlEncode(str) {
    return Buffer.from(str).toString('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/, '');
}

/**
 * Import a private key and return a CryptoKey
 * @param {string} privateKeyDer base64 encoded private key der
 * @returns {Promise<CryptoKey>}
 */
async function getPrivateKeyFromDer(privKeyDer) {
    const decoded = Buffer.from(privKeyDer, 'base64').toString('binary');
    const buf = str2ab(decoded);
    return crypto.subtle.importKey('pkcs8', buf, {
        name: 'ECDSA',
        namedCurve: 'P-256',
    }, true, ['sign']);
}

/**
 * Sign a message with a private CryptoKey
 * @param {CryptoKey} privateKey
 * @param {string} message The message to sign
 * @returns {string} The signed message
 */
async function signMessage(privateKey, message) {
    return crypto.subtle.sign(
        {
            name: 'ECDSA',
            hash: 'SHA-256'
        }, 
        privateKey, 
        new TextEncoder().encode(message)
    );
}

/**
 * Create a request signature
 * @param {CryptoKey} privateKey
 * @param {string} date HTTP header date value
 * @param {string} method HTTP method value
 * @param {string} path HTTP path 
 * @param {string} [queryString] HTTP path query string (? not included)
 */
async function createRequestSignature({
    privateKey,
    date, 
    method, 
    path, 
    queryString = undefined
}) {
    const nonce = Date.now().toString();
    const sigParts = [
        method.toUpperCase(),
        path,
        queryString,
        date,
        nonce
    ].filter(str => str && str.length !== 0);
    const signature = await signMessage(privateKey, sigParts.join('\n'));
    return base64UrlEncode(signature) + '.' + base64UrlEncode(nonce);
}

/* HTTP Request */

const API_URL = 'https://int.quadrata.com';
const API_KEY = '...';

/**
 * Send a signed request
 * @param {CryptoKey} privateKey
 * @param {string} method HTTP method value
 * @param {string} path HTTP path
 * @param {string} [queryString] HTTP path query string (? not included)
 * @param {object} [body] HTTP body payload
 * @param {object} [extraHeaders] Optional headers to send with the request
 * @returns {Promise<Response|void>}
 */
async function makeRequest({
    privateKey, 
    method, 
    path, 
    queryString = undefined,
    body = undefined,
    extraHeaders = undefined
}) {
    const date = (new Date()).toUTCString();
    const signature = await createRequestSignature({
        privateKey,
        method,
        path,
        queryString,
        date
    });
    const headers = {
        ...extraHeaders,
        'Authorization': `Basic ${Buffer.from(API_KEY).toString('base64')}`,
        'Date': date,
        'Signature': signature
    };
    let urlPath = `${API_URL}${path}`;
    if (queryString && queryString.length !== 0) {
        urlPath += `?${queryString}`;
    }
    const response = await fetch(urlPath, {
        method,
        headers,
        body: body ? JSON.stringify(body) : undefined
    });
    if (!response.ok) {
        console.error(response);
        return;
    }
    return response.json();
}

/**
 * Base64 Encoded Private Key DER
 * @type {string}
 */
const privateKeyDer = '...';

/**
 * @type {CryptoKey}
 */
const privateKey = await getPrivateKeyFromDer(privateKeyDer);

/**
 * @type {string}
 */
const walletAddress = '...';

// make the request
const json = await makeRequest({
    method: 'GET',
    privateKey: privateKey,
    path: `/api/v1/privacy/grants/${encodeURIComponent(walletAddress)}`,
});

Last updated