Files
nexus/frontend/src/js/secrets.js
T
andy e6bdbef26c Fix JS execution: wrap all scripts in IIFEs to avoid WebKit global scope conflicts
WebKit's WKWebView rejects top-level const/let declarations that shadow
global function names. Since utils.js defines functions like stateLabel()
at the global scope (non-module scripts), and later files destructure
the same names from window.NexusTerminal using const, WebKit throws:
  SyntaxError: Can't create duplicate variable that shadows a global property

Fix: wrap every JS file in an IIFE so all declarations are function-scoped.
Also remove temporary diagnostic instrumentation.
2026-04-15 00:00:00 +01:00

307 lines
12 KiB
JavaScript

(function() {
/**
* Nexus Terminal — secret/passphrase prompt dialogs.
* Provides modal prompts for collecting passwords, master-vault passphrases,
* and SSH host-key verification confirmations.
*/
const { createElement, escapeHtml } = window.NexusTerminal;
class SecretPrompt {
constructor(app) {
/** @type {App} */
this.app = app;
/** @type {HTMLElement|null} Currently open overlay. */
this._overlay = null;
}
// ── Generic password prompt ──────────────────────────────────────
/**
* Displays a modal with a single password field and returns the entered value.
* Rejects if the user cancels.
* @param {string} title - Dialog heading.
* @param {string} message - Instructional text shown above the input.
* @returns {Promise<string>}
*/
prompt(title, message) {
return new Promise((resolve, reject) => {
this._close(); // dismiss any existing prompt
const overlay = createElement('div', 'modal-overlay');
const modal = createElement('div', 'modal modal-secret');
// Header
const header = createElement('div', 'modal-header');
header.innerHTML = `
<h2 class="modal-title">${escapeHtml(title)}</h2>
<button class="modal-close" aria-label="Close">&times;</button>
`;
modal.appendChild(header);
// Body
const body = createElement('div', 'modal-body');
body.innerHTML = `
<p class="secret-message">${escapeHtml(message)}</p>
<div class="form-group">
<input type="password" class="form-input secret-input"
autocomplete="off" spellcheck="false" placeholder="Enter value…" />
</div>
`;
modal.appendChild(body);
// Footer
const footer = createElement('div', 'modal-footer');
const cancelBtn = createElement('button', 'btn btn-secondary', { textContent: 'Cancel' });
const submitBtn = createElement('button', 'btn btn-primary', { textContent: 'Submit' });
footer.appendChild(cancelBtn);
footer.appendChild(submitBtn);
modal.appendChild(footer);
overlay.appendChild(modal);
document.body.appendChild(overlay);
this._overlay = overlay;
const input = modal.querySelector('.secret-input');
input.focus();
const handleSubmit = () => {
const value = input.value;
this._close();
resolve(value);
};
const handleCancel = () => {
this._close();
reject(new Error('User cancelled'));
};
submitBtn.addEventListener('click', handleSubmit);
cancelBtn.addEventListener('click', handleCancel);
header.querySelector('.modal-close').addEventListener('click', handleCancel);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) handleCancel();
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') handleSubmit();
if (e.key === 'Escape') handleCancel();
});
});
}
// ── Master password (vault initialisation / unlock) ──────────────
/**
* Prompts for the master vault password.
* When `isNew` is true, a confirmation field is shown.
* @param {boolean} isNew - Whether this is the first time (create vault).
* @returns {Promise<string>} The master password.
*/
promptMasterPassword(isNew) {
return new Promise((resolve, reject) => {
this._close();
const overlay = createElement('div', 'modal-overlay');
const modal = createElement('div', 'modal modal-secret');
const title = isNew ? 'Create Master Password' : 'Unlock Secret Store';
const description = isNew
? 'Choose a strong master password. This protects all saved credentials.'
: 'Enter your master password to unlock the secret store.';
const header = createElement('div', 'modal-header');
header.innerHTML = `
<h2 class="modal-title">${escapeHtml(title)}</h2>
<button class="modal-close" aria-label="Close">&times;</button>
`;
modal.appendChild(header);
const body = createElement('div', 'modal-body');
let formHtml = `
<p class="secret-message">${escapeHtml(description)}</p>
<div class="form-group">
<label class="form-label">Master password</label>
<input type="password" class="form-input master-password"
autocomplete="off" spellcheck="false" placeholder="Password…" />
</div>
`;
if (isNew) {
formHtml += `
<div class="form-group">
<label class="form-label">Confirm password</label>
<input type="password" class="form-input master-confirm"
autocomplete="off" spellcheck="false" placeholder="Confirm…" />
</div>
<p class="form-hint secret-error" style="display:none;"></p>
`;
}
body.innerHTML = formHtml;
modal.appendChild(body);
const footer = createElement('div', 'modal-footer');
const cancelBtn = createElement('button', 'btn btn-secondary', { textContent: 'Cancel' });
const submitBtn = createElement('button', 'btn btn-primary', { textContent: isNew ? 'Create' : 'Unlock' });
footer.appendChild(cancelBtn);
footer.appendChild(submitBtn);
modal.appendChild(footer);
overlay.appendChild(modal);
document.body.appendChild(overlay);
this._overlay = overlay;
const passInput = modal.querySelector('.master-password');
const confirmInput = modal.querySelector('.master-confirm');
const errorEl = modal.querySelector('.secret-error');
passInput.focus();
const handleSubmit = () => {
const pw = passInput.value;
if (!pw) {
passInput.classList.add('input-error');
return;
}
if (isNew) {
const confirm = confirmInput.value;
if (pw !== confirm) {
errorEl.textContent = 'Passwords do not match.';
errorEl.style.display = 'block';
confirmInput.classList.add('input-error');
return;
}
}
this._close();
resolve(pw);
};
const handleCancel = () => {
this._close();
reject(new Error('User cancelled'));
};
submitBtn.addEventListener('click', handleSubmit);
cancelBtn.addEventListener('click', handleCancel);
header.querySelector('.modal-close').addEventListener('click', handleCancel);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) handleCancel();
});
const handleKeydown = (e) => {
if (e.key === 'Enter') handleSubmit();
if (e.key === 'Escape') handleCancel();
};
passInput.addEventListener('keydown', handleKeydown);
if (confirmInput) {
confirmInput.addEventListener('keydown', handleKeydown);
}
});
}
// ── SSH host-key verification ────────────────────────────────────
/**
* Asks the user whether to accept an SSH host key.
* @param {string} host
* @param {string} keyType - e.g. "ssh-ed25519"
* @param {string} fingerprint - SHA256 fingerprint
* @returns {Promise<boolean>} true = accept, false = reject.
*/
promptHostKeyVerification(host, keyType, fingerprint) {
return new Promise((resolve) => {
this._close();
const overlay = createElement('div', 'modal-overlay');
const modal = createElement('div', 'modal modal-secret modal-hostkey');
const header = createElement('div', 'modal-header');
header.innerHTML = `
<h2 class="modal-title">Unknown Host Key</h2>
<button class="modal-close" aria-label="Close">&times;</button>
`;
modal.appendChild(header);
const body = createElement('div', 'modal-body');
body.innerHTML = `
<p class="secret-message">
The authenticity of host <strong>${escapeHtml(host)}</strong> cannot be established.
</p>
<div class="hostkey-details">
<div class="hostkey-row">
<span class="hostkey-label">Key type</span>
<code class="hostkey-value">${escapeHtml(keyType)}</code>
</div>
<div class="hostkey-row">
<span class="hostkey-label">Fingerprint</span>
<code class="hostkey-value">${escapeHtml(fingerprint)}</code>
</div>
</div>
<p class="secret-message">
Are you sure you want to continue connecting?
</p>
`;
modal.appendChild(body);
const footer = createElement('div', 'modal-footer');
const rejectBtn = createElement('button', 'btn btn-secondary', { textContent: 'Reject' });
const acceptBtn = createElement('button', 'btn btn-primary', { textContent: 'Accept & Save' });
footer.appendChild(rejectBtn);
footer.appendChild(acceptBtn);
modal.appendChild(footer);
overlay.appendChild(modal);
document.body.appendChild(overlay);
this._overlay = overlay;
acceptBtn.focus();
acceptBtn.addEventListener('click', () => {
this._close();
resolve(true);
});
rejectBtn.addEventListener('click', () => {
this._close();
resolve(false);
});
header.querySelector('.modal-close').addEventListener('click', () => {
this._close();
resolve(false);
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
this._close();
resolve(false);
}
});
overlay.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this._close();
resolve(false);
}
});
});
}
// ── Internal helpers ─────────────────────────────────────────────
/**
* Closes and removes the current overlay, if any.
* @private
*/
_close() {
if (this._overlay) {
this._overlay.remove();
this._overlay = null;
}
}
}
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.SecretPrompt = SecretPrompt;
})();