From: Ardis Lu Date: Thu, 21 May 2026 05:59:14 +0000 (-0700) Subject: Only get passkey if actually required X-Git-Tag: v0.1.2~13 X-Git-Url: https://git.ardis.lu/?a=commitdiff_plain;h=339387adadc5185dd4e718b5576fe4b640bfae83;p=envpass Only get passkey if actually required Getting passkey is not required when `encrypt` is run on a file where all variables are already encrypted, or `decrypt` is run on a file where all variables are already decrypted. Previously, the passkey flow would always initiate. Now, it will only initiate the first time it is required. This change improves the UX in situations where `encrypt` or `decrypt` are called on a continuous basis. For example, when calling `envpass encrypt` in a `prestart` npm script hook. --- diff --git a/src/command.js b/src/command.js index ad3c9f2..9c4d0ff 100644 --- a/src/command.js +++ b/src/command.js @@ -46,15 +46,7 @@ export async function encrypt(options = {}) { return; } - const { url, prf: prfPromise } = await getPrf(getPrfOptions); - if (!getPrfOptions.autoOpen) { - if (!silent) { console.log(`Open this page in a web browser to complete the passkey flow: ${url}`); } - } - const prf = await prfPromise; - if (prf === null) { - throw new InputError('Unable to get passkey.'); - } - + let prf = null; /** @type {Env} */const encryptedEnv = Object.create(null); for (const [envVar, value] of Object.entries(env)) { if (value === undefined) { @@ -74,6 +66,16 @@ export async function encrypt(options = {}) { } } + if (prf === null) { + const { url, prf: prfPromise } = await getPrf(getPrfOptions); + if (!getPrfOptions.autoOpen) { + if (!silent) { console.log(`Open this page in a web browser to complete the passkey flow: ${url}`); } + } + prf = await prfPromise; + if (prf === null) { + throw new InputError('Unable to get passkey.'); + } + } const encryptedValue = await encryptRaw({ prf, value }); encryptedEnv[envVar] = encryptedValue; } @@ -127,15 +129,7 @@ export async function decrypt(options = {}, { args = [] } = {}) { return; } - const { url, prf: prfPromise } = await getPrf(getPrfOptions); - if (!getPrfOptions.autoOpen) { - if (!silent) { console.log(`Open this page in a web browser to complete the passkey flow: ${url}`); } - } - const prf = await prfPromise; - if (prf === null) { - throw new InputError('Unable to get passkey.'); - } - + let prf = null; /** @type {Env} */const decryptedEnv = {}; for (const [envVar, value] of Object.entries(env)) { if (value === undefined) { @@ -153,6 +147,17 @@ export async function decrypt(options = {}, { args = [] } = {}) { case 'error': throw new Error(`Environment variable "${envVar}" is not encrypted.`); } } + + if (prf === null) { + const { url, prf: prfPromise } = await getPrf(getPrfOptions); + if (!getPrfOptions.autoOpen) { + if (!silent) { console.log(`Open this page in a web browser to complete the passkey flow: ${url}`); } + } + prf = await prfPromise; + if (prf === null) { + throw new InputError('Unable to get passkey.'); + } + } const decryptedValue = await decryptRaw({ prf, value: parsedValue }); decryptedEnv[envVar] = decryptedValue; } diff --git a/test/index.test.js b/test/index.test.js index b6a8ccc..aeb0c1a 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,7 +1,8 @@ import { suite, test } from 'node:test'; -import { readFile } from 'node:fs/promises'; +import { deepStrictEqual } from 'node:assert'; import { encrypt, decrypt } from '#src/index.js'; +import { BROWSER } from '#src/getPrf.js'; import { setupPlaywright, setupEnv, mockOpen, assertConsole, fileEqual, fileNotEqual } from '#test/setup.js'; /** Standard value of the .env file used for most setups. */ @@ -56,30 +57,25 @@ suite('e2e', () => { await decrypt({ inFile: envFile, silent: true }); await fileEqual(envFile, ''); }); - test('ignores variables already encrypted', async (t) => { - const { page } = await setupPlaywright(t); - const envFile = await setupEnv(t, STD_ENV); - mockOpen(t, page); - assertConsole(t, { log: 3 }); - - await fileEqual(envFile, STD_ENV); - await encrypt({ inFile: envFile }); + test('encrypt does nothing when all variables are already encrypted', async (t) => { + const openMock = t.mock.method(BROWSER, 'open'); + const env = 'A=envpass:v1:AAA\nB=envpass:v1:BBB\nC=envpass:v1:CCC\n'; + const envFile = await setupEnv(t, env); + assertConsole(t, { log: 1 }); - const newContent = await readFile(envFile, { encoding: 'utf8' }); await encrypt({ inFile: envFile }); - await fileEqual(envFile, newContent); - await decrypt({ inFile: envFile });; - await fileEqual(envFile, STD_ENV); + await fileEqual(envFile, env); + deepStrictEqual(openMock.mock.calls.length, 0); }); - test('ignores variables already decrypted', async (t) => { - const { page } = await setupPlaywright(t); + test('decrypt does nothing when all variables are already decrypted', async (t) => { + const openMock = t.mock.method(BROWSER, 'open'); const envFile = await setupEnv(t, STD_ENV); - mockOpen(t, page); assertConsole(t, { log: 1 }); - await fileEqual(envFile, STD_ENV); await decrypt({ inFile: envFile }); + await fileEqual(envFile, STD_ENV); + deepStrictEqual(openMock.mock.calls.length, 0); }); });