diff --git a/apps/web/public/app/app.js b/apps/web/public/app/app.js index e37e94f..a16bc35 100644 --- a/apps/web/public/app/app.js +++ b/apps/web/public/app/app.js @@ -1813,8 +1813,8 @@ function recordVaultCard(status, files, manifestError, userId) { if (!status.configured) { content = `

Create a separate vault passphrase and recovery key. Your account sign-in does not unlock this vault. Files are encrypted in this browser before upload.

- - + +

`; } else if (!unlocked) { @@ -1892,8 +1892,8 @@ function showRecordVaultRecoveryKey({ title, recoveryKey, handle, userId, envelo function openRecordVaultRecovery(status, userId) { modal.innerHTML = `

Recover encrypted vault

- - + +

`; modal.showModal(); diff --git a/apps/web/public/health/health.js b/apps/web/public/health/health.js index d50e0fa..f20027c 100644 --- a/apps/web/public/health/health.js +++ b/apps/web/public/health/health.js @@ -194,10 +194,10 @@ async function showGate(message = '') { if (!config) { vaultScreen.innerHTML = `

First visit

Create your encrypted tracker

-

Choose a passphrase with at least 6 characters. The tracker encrypts every reading in your browser before saving it.

+

Choose a passphrase. The tracker encrypts every reading in your browser before saving it.

- - + +

${message}

`; @@ -255,7 +255,7 @@ async function createLocalVault(event) { if (passphrase !== confirm) { message.textContent = 'The passphrases do not match.'; return; } try { const userId = makeId(); - const created = await vaultCrypto.createVault(userId, passphrase, { minLength: 6 }); + const created = await vaultCrypto.createVault(userId, passphrase, { minLength: 1 }); await putVault({ userId, envelope: created.envelope }); vaultHandle = created.handle; vaultUserId = userId; @@ -272,7 +272,7 @@ async function unlockWithPassphrase(event) { const message = document.getElementById('gate-message'); try { const config = await getVault(); - const handle = await vaultCrypto.unlockWithPassphrase(config.userId, passphrase, config.envelope, { minLength: 6 }); + const handle = await vaultCrypto.unlockWithPassphrase(config.userId, passphrase, config.envelope, { minLength: 1 }); unlockForm.reset(); activateVault(handle, config.userId); } catch (err) { diff --git a/packages/cli/package.json b/packages/cli/package.json index 80c1798..bc8c928 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@profullstack/tleehealth", - "version": "0.5.3", + "version": "0.5.4", "description": "tleehealth from the terminal: the CLI, the front-desk TUI, and the API client the MCP server uses.", "type": "module", "license": "MIT", diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index a63620b..e27a4fc 100644 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -302,14 +302,14 @@ async function establishVault(auth, userId, { recover = false } = {}) { if (recover) { if (!status.configured) throw new Error('No encrypted vault exists to recover.'); const enteredRecoveryKey = await promptSecret('Recovery key'); - const passphrase = await promptSecret('New vault passphrase (at least 16 characters)'); + const passphrase = await promptSecret('New vault passphrase'); const confirm = await promptSecret('Repeat new passphrase'); if (passphrase !== confirm) throw new Error('Passphrases do not match.'); handle = await unlockWithRecoveryKey(userId, enteredRecoveryKey, status.envelope); ({ envelope, recoveryKey } = await rewrapVaultKey(userId, handle, passphrase)); } else { if (status.configured) throw new Error('A vault already exists. Use `tleehealth vault recover` to rotate its passphrase.'); - const passphrase = await promptSecret('New vault passphrase (at least 16 characters)'); + const passphrase = await promptSecret('New vault passphrase'); const confirm = await promptSecret('Repeat passphrase'); if (passphrase !== confirm) throw new Error('Passphrases do not match.'); const created = await createVault(userId, passphrase); diff --git a/packages/cli/src/vault-crypto.js b/packages/cli/src/vault-crypto.js index 0a8a5ef..0b3ea66 100644 --- a/packages/cli/src/vault-crypto.js +++ b/packages/cli/src/vault-crypto.js @@ -46,9 +46,9 @@ function assertUserId(userId) { } } -function assertPassphrase(passphrase, minLength = 16) { +function assertPassphrase(passphrase, minLength = 1) { if (typeof passphrase !== 'string' || passphrase.normalize('NFC').length < minLength) { - throw new Error(`Use a passphrase with at least ${minLength} characters.`); + throw new Error(minLength > 1 ? `Use a passphrase with at least ${minLength} characters.` : 'Enter a passphrase.'); } if (passphrase.length > 1024) throw new Error('Passphrase is too long.'); } @@ -64,7 +64,7 @@ function aadForObject(userId, objectId) { return encoder.encode(`tleehealth:record-vault:object:v${FORMAT_VERSION}:${userId}:${objectId}`); } -async function derivePassphraseKey(passphrase, salt, minLength = 16) { +async function derivePassphraseKey(passphrase, salt, minLength = 1) { assertPassphrase(passphrase, minLength); const crypto = webcrypto(); const passphraseBytes = encoder.encode(passphrase.normalize('NFC')); @@ -133,7 +133,7 @@ export function lockVault(handle) { handles.delete(handle); } -export async function createVault(userId, passphrase, { minLength = 16 } = {}) { +export async function createVault(userId, passphrase, { minLength = 1 } = {}) { assertUserId(userId); assertPassphrase(passphrase, minLength); const rawKey = randomBytes(32); @@ -182,7 +182,7 @@ export function validateVaultEnvelope(envelope) { return envelope; } -export async function unlockWithPassphrase(userId, passphrase, untrustedEnvelope, { minLength = 16 } = {}) { +export async function unlockWithPassphrase(userId, passphrase, untrustedEnvelope, { minLength = 1 } = {}) { const envelope = validateVaultEnvelope(untrustedEnvelope); let rawKey; try { diff --git a/packages/mcp/package.json b/packages/mcp/package.json index ab1cc40..d24db5c 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@profullstack/tleehealth-mcp", - "version": "0.5.3", + "version": "0.5.4", "description": "MCP server for tleehealth.com: read the practice schedule, the navigation caseload and superbill, and log navigation time from any MCP client.", "type": "module", "license": "MIT", diff --git a/test/api.test.js b/test/api.test.js index 359ac7a..c7cc8fb 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -66,7 +66,7 @@ describe.skipIf(!configured())('api', () => { expect(await page.text()).toContain('Sign in to record measurements'); const trackerScript = await app.request('/health/health.js', { headers: { cookie } }); expect(trackerScript.status).toBe(200); - expect(await trackerScript.text()).toContain('minlength="6"'); + expect(await trackerScript.text()).not.toContain('minlength='); }); test('CoinPay links to the signed-in account by provider subject and signs into that account', async () => { diff --git a/test/vault-crypto.test.js b/test/vault-crypto.test.js index 465eba5..c35e54c 100644 --- a/test/vault-crypto.test.js +++ b/test/vault-crypto.test.js @@ -56,10 +56,10 @@ describe('personal record vault cryptography', () => { }); test('rejects weak passphrases and malformed key envelopes', async () => { - await expect(createVault(userId, 'too short')).rejects.toThrow('at least 16 characters'); + await expect(createVault(userId, '')).rejects.toThrow('Enter a passphrase'); await expect(createVault(userId, '12345', { minLength: 6 })).rejects.toThrow('at least 6 characters'); - const shortPassphrase = await createVault(userId, '123456', { minLength: 6 }); - const unlocked = await unlockWithPassphrase(userId, '123456', shortPassphrase.envelope, { minLength: 6 }); + const shortPassphrase = await createVault(userId, 'abc'); + const unlocked = await unlockWithPassphrase(userId, 'abc', shortPassphrase.envelope); lockVault(shortPassphrase.handle); lockVault(unlocked); expect(() => validateVaultEnvelope({ version: 1, cipher: 'AES-ECB' })).toThrow('invalid vault key envelope');