Source

bosdyn-client/token_cache.js

'use strict';

const {
  readFileSync,
  unlinkSync,
  readdirSync,
  writeFileSync,
  existsSync,
  mkdirSync,
  chmodSync,
  copyFileSync,
} = require('node:fs');
const { homedir, constants } = require('node:os');
const { join, dirname } = require('node:path');
const process = require('node:process');

const tmp = require('tmp');

class TokenCacheError extends Error {
  constructor(msg) {
    super(msg);
    this.name = 'TokenCacheError';
  }
}

class ClearFailedError extends TokenCacheError {
  constructor(msg) {
    super(msg);
    this.name = 'ClearFailedError';
  }
}

class NotInCacheError extends TokenCacheError {
  constructor(msg) {
    super(msg);
    this.name = 'NotInCacheError';
  }
}

class WriteFailedError extends TokenCacheError {
  constructor(msg) {
    super(msg);
    this.name = 'WriteFailedError';
  }
}

function atomicFileWrite(data, filename, permissions = 0o600) {
  const temp = tmp.fileSync({ mode: permissions });
  writeFileSync(temp.name, data);

  const original_umask = process.umask(0);

  try {
    const directory = dirname(filename);
    if (!existsSync(directory)) {
      mkdirSync(directory, { mode: 0o700 });
    }
  } catch (e) {
    console.error(e);
  }

  process.umask(original_umask);

  try {
    copyFileSync(temp.name, filename);
  } catch (e) {
    if (e.errno !== constants.errno.EEXIST) {
      throw e;
    }
    unlinkSync(filename);
    copyFileSync(temp.name, filename);
  }

  temp.removeCallback();
  chmodSync(filename, permissions);
}

/**
 * No-op default cache that serves as an interface.
 */
/* eslint-disable */
class TokenCache {
  constructor() {
    // Pass
  }

  read(name) {
    throw new NotInCacheError();
  }

  clear(name) {
    // Pass
  }

  write(name, token) {
    // Pass
  }

  match(name) {
    return [];
  }
}
/* eslint-enable */

/**
 * Handles transfer from in memory tokens to arbitrary storage e.g. filesystem.
 */
class TokenCacheFilesystem {
  /**
   * @param {string} [cache_directory] The cache's path.
   */
  constructor(cache_directory = `${homedir()}/.bosdyn/user_tokens`) {
    this.directory = join(cache_directory);
  }

  /**
   * @param {string} name The file's name.
   * @returns {string|Buffer} The content of the file.
   * @throws {NotInCacheError}
   */
  read(name) {
    const filename = this._name_to_filename(name);
    try {
      const data = readFileSync(filename, 'utf8');
      return data;
    } catch (err) {
      throw new NotInCacheError(err);
    }
  }

  /**
   * @param {string} name The file's name.
   * @returns {void}
   * @throws {ClearFailedError}
   */
  clear(name) {
    const filename = this._name_to_filename(name);
    try {
      unlinkSync(filename);
    } catch (err) {
      throw new ClearFailedError(err);
    }
  }

  /**
   * @param {string} name The file's name.
   * @param {string} token The token to write in the file.
   * @returns {void}
   * @throws {WriteFailedError}
   */
  write(name, token) {
    const filename = this._name_to_filename(name);
    try {
      atomicFileWrite(token, filename);
    } catch (err) {
      throw new WriteFailedError(err);
    }
  }

  /**
   * @param {string} name The file's name to match.
   * @returns {Array<string>|Array}
   */
  match(name) {
    const matching_tokens = [];
    readdirSync(this.directory).forEach(e => {
      if (e === name || e.includes(name)) {
        matching_tokens.push(this._filename_to_name(e));
      }
    });
    return matching_tokens;
  }

  /**
   * @param {string} name The file's name.
   * @returns {string}
   * @private
   */
  _name_to_filename(name) {
    return `${join(this.directory, name)}.jwt`;
  }

  /**
   * @param {string} filename The complete file's name.
   * @returns {string}
   * @private
   */
  _filename_to_name(filename) {
    return join(filename).split('.')[0];
  }
}

module.exports = {
  TokenCache,
  TokenCacheFilesystem,
  TokenCacheError,
  ClearFailedError,
  NotInCacheError,
  WriteFailedError,
};