box-typescript-sdk-gen
Version:
Official Box TypeScript Generated SDK
307 lines (269 loc) • 8.61 kB
text/typescript
import { Buffer } from 'buffer';
import { Readable } from 'stream';
import { SignJWT, importPKCS8 } from 'jose';
import { default as crypto } from 'crypto';
import { default as fs } from 'fs';
import { ProxyAgent } from 'proxy-agent';
import { default as FormData } from 'form-data';
import util from 'util';
export { Buffer, Readable as ByteStream, FormData, util as utilLib };
export type AgentOptions = any;
export type Agent = any;
export type HashName = 'sha1';
export type DigestHashType = 'base64';
export class Hash {
#hash: any;
algorithm: HashName;
constructor({ algorithm }: { algorithm: HashName }) {
this.algorithm = algorithm;
this.#hash = crypto.createHash(algorithm);
}
async updateHash(data: Buffer) {
this.#hash.update(data);
}
async digestHash(encoding: DigestHashType = 'base64'): Promise<string> {
return this.#hash.digest(encoding);
}
}
export function generateByteBuffer(size: number): Buffer {
return crypto.randomBytes(size);
}
export function generateReadableStreamFromFile(
file: any,
chunkSize: number = 1024 * 1024,
): ReadableStream {
throw new Error('This function is only supported in the browser');
}
export function generateByteStreamFromBuffer(
buffer: Buffer | ArrayBuffer,
): Readable {
return Readable.from(Buffer.from(buffer));
}
export function decodeBase64ByteStream(data: string): Readable {
return Readable.from(Buffer.from(data, 'base64'));
}
export function stringToByteStream(data: string): Readable {
return Readable.from(Buffer.from(data, 'ascii'));
}
export async function readByteStream(byteStream: Readable): Promise<Buffer> {
const buffers: Buffer[] = [];
for await (const data of byteStream) {
buffers.push(data);
}
return Buffer.concat(buffers);
}
export async function* iterateChunks(
stream: Readable,
chunkSize: number,
fileSize: number,
): AsyncGenerator<Readable> {
let buffers: Buffer[] = [];
let totalSize = 0;
let consumedSize = 0;
while (consumedSize < fileSize && !stream.readableEnded) {
for await (const chunk of stream) {
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
if (!Buffer.isBuffer(data)) {
throw new Error('Expecting a chunk of stream to be a Buffer');
}
consumedSize += data.length;
buffers.push(data);
totalSize += data.length;
if (totalSize < chunkSize) {
continue;
}
const buffer = Buffer.concat(buffers);
let start = 0;
while (totalSize >= chunkSize) {
yield generateByteStreamFromBuffer(
buffer.subarray(start, start + chunkSize),
);
start += chunkSize;
totalSize -= chunkSize;
}
buffers = totalSize > 0 ? [buffer.subarray(start)] : [];
}
}
if (consumedSize !== fileSize) {
throw new Error(
`Stream size ${consumedSize} does not match expected file size ${fileSize}`,
);
}
if (totalSize > 0) {
yield generateByteStreamFromBuffer(Buffer.concat(buffers));
}
}
export type JwtKey = {
key: string;
passphrase: string;
};
export type JwtAlgorithm =
| 'HS256'
| 'HS384'
| 'HS512'
| 'RS256'
| 'RS384'
| 'RS512'
| 'ES256'
| 'ES384'
| 'ES512'
| 'PS256'
| 'PS384'
| 'PS512'
| 'none';
export type JwtSignOptions = {
algorithm?: JwtAlgorithm;
keyid?: string | undefined;
expiresIn?: string | number | undefined;
notBefore?: string | number | undefined;
audience?: string | string[] | undefined;
subject?: string | undefined;
issuer?: string | undefined;
jwtid?: string | undefined;
};
/**
* Creates a JWT assertion.
*
* @param claims
* @param key
* @param options
* @returns
*/
export async function createJwtAssertion(
claims: {
readonly [key: string]: any;
},
key: JwtKey,
options: JwtSignOptions,
): Promise<string> {
const privateKey = crypto.createPrivateKey({
key: key.key,
format: 'pem',
type: 'pkcs8',
passphrase: key.passphrase,
});
const pem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
const pkcs8 = await importPKCS8(pem, options.algorithm || 'RS256');
let signer = new SignJWT(claims);
signer = options.audience ? signer.setAudience(options.audience) : signer;
signer = options.expiresIn
? signer.setExpirationTime(options.expiresIn)
: signer;
signer = options.issuer ? signer.setIssuer(options.issuer) : signer;
signer = options.jwtid ? signer.setJti(options.jwtid) : signer;
signer = options.notBefore ? signer.setNotBefore(options.notBefore) : signer;
signer = options.subject ? signer.setSubject(options.subject) : signer;
signer = options.algorithm
? signer.setProtectedHeader({ alg: options.algorithm })
: signer;
signer = signer.setIssuedAt();
return await signer.sign(pkcs8);
}
/**
* Reads a text file and returns its content.
*/
export function readTextFromFile(filepath: string): string {
return fs.readFileSync(filepath, 'utf8');
}
/**
* Create web agent from proxy agent options.
*/
export function createAgent(options?: AgentOptions, proxyConfig?: any): Agent {
let agentOptions = options;
if (proxyConfig && proxyConfig.url) {
if (!proxyConfig.url.startsWith('http')) {
throw new Error('Invalid proxy URL');
}
const proxyHost = proxyConfig.url.split('//')[1];
const proxyAuth =
proxyConfig.username && proxyConfig.password
? `${proxyConfig.username}:${proxyConfig.password}@`
: '';
const proxyUrl = `http://${proxyAuth}${proxyHost}`;
agentOptions = Object.assign(
{ getProxyForUrl: (url: string) => proxyUrl },
options || {},
);
}
return agentOptions ? new ProxyAgent(agentOptions) : new ProxyAgent();
}
/**
* Stringify JSON with escaped multibyte Unicode characters and slashes to ensure computed signatures match PHP's default behavior
*
* @param {Object} body - The parsed JSON object
* @returns {string} - Stringified JSON with escaped multibyte Unicode characters
* @private
*/
export function jsonStringifyWithEscapedUnicode(body: string) {
return body
.replace(
/[\u007f-\uffff]/g,
(char) => `\\u${`0000${char.charCodeAt(0).toString(16)}`.slice(-4)}`,
)
.replace(/(?<!\\)\//g, '\\/');
}
/**
* Compute the message signature
* @see {@Link https://developer.box.com/en/guides/webhooks/handle/setup-signatures/}
*
* @param {string} body - The request body of the webhook message
* @param {Object} headers - The request headers of the webhook message
* @param {string} signatureKey - The signature to verify the message with
* @param {string} escapeBody - Indicates if payload should be escaped or left as is
* @returns {?string} - The message signature (or null, if it can't be computed)
* @private
*/
export async function computeWebhookSignature(
body: string,
headers: {
[key: string]: string;
},
signatureKey: string,
escapeBody: boolean = false,
): Promise<string | null> {
if (headers['box-signature-version'] !== '1') {
return null;
}
if (headers['box-signature-algorithm'] !== 'HmacSHA256') {
return null;
}
let signature: string | null = null;
const escapedBody = escapeBody ? jsonStringifyWithEscapedUnicode(body) : body;
let hmac = crypto.createHmac('sha256', signatureKey);
hmac.update(escapedBody);
hmac.update(headers['box-delivery-timestamp']);
signature = hmac.digest('base64');
return signature;
}
export async function compareSignatures(
expectedSignature: string | null,
receivedSignature: string | null,
): Promise<boolean> {
if (!expectedSignature || !receivedSignature) {
return false;
}
const expectedBuffer = Buffer.from(expectedSignature, 'base64');
const receivedBuffer = Buffer.from(receivedSignature, 'base64');
if (expectedBuffer.length !== receivedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(expectedBuffer, receivedBuffer);
}
export function random(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
export async function calculateMD5Hash(data: string | Buffer): Promise<string> {
return crypto.createHash('sha1').update(data).digest('hex');
}
export function getEnvVar(name: string): string {
if (typeof process === 'undefined' || !process.env) {
throw new Error('This function requires a Node.js environment');
}
return process.env[name] || '';
}
export function setEnvVar(name: string, value: string): void {
if (typeof process === 'undefined' || !process.env) {
throw new Error('This function requires a Node.js environment');
}
process.env[name] = value;
}