otplease.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. "use strict";
  2. const { promptTextInput } = require("@lerna/prompt");
  3. /**
  4. * @typedef {object} OneTimePasswordCache - Passed between concurrent executions
  5. * @property {string} [otp] The one-time password, passed as an option or received via prompt
  6. */
  7. // basic single-entry semaphore
  8. const semaphore = {
  9. wait() {
  10. return new Promise((resolve) => {
  11. if (!this._promise) {
  12. // not waiting, block other callers until 'release' is called.
  13. this._promise = new Promise((release) => {
  14. this._resolve = release;
  15. });
  16. resolve();
  17. } else {
  18. // wait for 'release' to be called and try to lock the semaphore again.
  19. resolve(this._promise.then(() => this.wait()));
  20. }
  21. });
  22. },
  23. release() {
  24. const resolve = this._resolve;
  25. // istanbul ignore else
  26. if (resolve) {
  27. this._resolve = undefined;
  28. this._promise = undefined;
  29. // notify waiters that the semaphore has been released.
  30. resolve();
  31. }
  32. },
  33. };
  34. module.exports.otplease = otplease;
  35. module.exports.getOneTimePassword = getOneTimePassword;
  36. /**
  37. * Attempt to execute Promise callback, prompting for OTP if necessary.
  38. * @template {Record<string, unknown>} T
  39. * @param {(opts: T) => Promise<unknown>} fn
  40. * @param {T} _opts The options to be passed to `fn`
  41. * @param {OneTimePasswordCache} otpCache
  42. */
  43. function otplease(fn, _opts, otpCache) {
  44. // always prefer explicit config (if present) to cache
  45. const opts = { ...otpCache, ..._opts };
  46. return attempt(fn, opts, otpCache);
  47. }
  48. /** @returns {Promise<unknown>} */
  49. function attempt(fn, opts, otpCache) {
  50. return new Promise((resolve) => {
  51. resolve(fn(opts));
  52. }).catch((err) => {
  53. if (err.code !== "EOTP" && !(err.code === "E401" && /one-time pass/.test(err.body))) {
  54. throw err;
  55. } else if (!process.stdin.isTTY || !process.stdout.isTTY) {
  56. throw err;
  57. } else {
  58. // check the cache in case a concurrent caller has already updated the otp.
  59. if (otpCache != null && otpCache.otp != null && otpCache.otp !== opts.otp) {
  60. return attempt(fn, { ...opts, ...otpCache }, otpCache);
  61. }
  62. // only allow one getOneTimePassword attempt at a time to reuse the value
  63. // from the preceeding prompt
  64. return semaphore.wait().then(() => {
  65. // check the cache again in case a previous waiter already updated it.
  66. if (otpCache != null && otpCache.otp != null && otpCache.otp !== opts.otp) {
  67. semaphore.release();
  68. return attempt(fn, { ...opts, ...otpCache }, otpCache);
  69. }
  70. return getOneTimePassword()
  71. .then(
  72. (otp) => {
  73. // update the otp and release the lock so that waiting
  74. // callers can see the updated otp.
  75. if (otpCache != null) {
  76. // eslint-disable-next-line no-param-reassign
  77. otpCache.otp = otp;
  78. }
  79. semaphore.release();
  80. return otp;
  81. },
  82. (promptError) => {
  83. // release the lock and reject the promise.
  84. semaphore.release();
  85. return Promise.reject(promptError);
  86. }
  87. )
  88. .then((otp) => {
  89. return fn({ ...opts, otp });
  90. });
  91. });
  92. }
  93. });
  94. }
  95. /**
  96. * Prompt user for one-time password.
  97. * @returns {Promise<string>}
  98. */
  99. function getOneTimePassword(message = "This operation requires a one-time password:") {
  100. // Logic taken from npm internals: https://git.io/fNoMe
  101. return promptTextInput(message, {
  102. filter: (otp) => otp.replace(/\s+/g, ""),
  103. validate: (otp) =>
  104. (otp && /^[\d ]+$|^[A-Fa-f0-9]{64,64}$/.test(otp)) ||
  105. "Must be a valid one-time-password. " +
  106. "See https://docs.npmjs.com/getting-started/using-two-factor-authentication",
  107. });
  108. }