feat(licence): ecran d'activation au demarrage

ensureLicence() en tete d'init(), avant tout le reste : un ecran
d'authentification qui s'affiche puis se fait recouvrir donnerait
l'impression que le coffre etait accessible.

L'ecran reprend les classes de l'ecran d'authentification — zero style
nouveau hormis un z-index. Le statut est demande deux fois avant de le
montrer : un demarrage a froid lent manquerait la fenetre de reponse, et
le silence se lirait comme un refus.

Une cle refusee ne dit pas pourquoi, et le journal note le verdict sans
jamais la cle.

ATTENTION : tant que LicencePublicKeyBlob est a zero, l'ecran est
infranchissable. Poser la cle publique avant de deployer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-09-05 14:58:19 +01:00
parent 960d2b388a
commit cd6e70449d
5 changed files with 156 additions and 1 deletions
+69
View File
@@ -14,6 +14,7 @@ const API = (location.pathname.indexOf('/password-manager/') === 0)
// Active only when running inside the Delphi-hosted WebView2 (API === '').
// Falls back to navigator.clipboard for the standalone PHP frontend.
const prefResolvers = {};
let licenceResolver = null;
const fileSaveResolvers = {};
const fileChunkResolvers = {};
// Stream a base64 string to Delphi in URL-sized pieces via cmd://file/chunk,
@@ -293,6 +294,33 @@ const Bridge = (() => {
cmd('cmd://prefs/set?key=' + encodeURIComponent(key) +
'&value=' + encodeURIComponent(value || ''));
},
// Licence check / activation, verified in Delphi against the
// publisher's public key (PM.Licence). Hors du navigateur hôte il n'y
// a pas de licence à vérifier — le harnais de test et le dev en
// navigateur passent.
// ponytail: un seul résolveur, pas une table comme prefResolvers — les
// deux appels ne se croisent jamais (statut au démarrage, activation
// sur clic). Passer à une table le jour où ils se croiseraient.
licence(action, key) {
if (!active) return Promise.resolve({ ok: true, holder: '' });
return new Promise(resolve => {
licenceResolver = resolve;
cmd('cmd://licence/' + action +
(action === 'activate' ? '?key=' + encodeURIComponent(key || '') : ''));
setTimeout(() => {
if (licenceResolver === resolve) {
licenceResolver = null;
resolve({ ok: false, holder: '' });
}
}, 3000);
});
},
onLicenceResult(ok, holder) {
const r = licenceResolver;
if (!r) return;
licenceResolver = null;
r({ ok: !!ok, holder: holder || '' });
},
onPrefResult(key, value) {
const r = prefResolvers[key];
if (r) {
@@ -6908,6 +6936,43 @@ function resetAutoLock() {
}, { passive: true })
);
// La porte d'entrée : sans licence valable, rien d'autre ne se peint.
// Ne rend la main QUE sur une licence acceptée — il n'y a pas de bouton
// "plus tard", c'est tout l'objet de l'écran.
async function ensureLicence() {
if ((await Bridge.licence('status')).ok) return;
// Un démarrage à froid lent peut manquer la fenêtre de réponse, et le
// silence se lit alors comme un refus : redemander une fois coûte moins
// qu'un écran d'activation présenté à quelqu'un qui a déjà payé.
if ((await Bridge.licence('status')).ok) return;
const screen = $('#licenceScreen');
const hint = $('#licenceHint');
const input = $('#licenceKey');
screen.classList.remove('is-hidden');
input.focus();
await new Promise(resolve => {
$('#licenceForm').addEventListener('submit', async e => {
e.preventDefault();
const key = input.value.trim();
if (!key) return;
hint.textContent = 'Checking…';
const res = await Bridge.licence('activate', key);
if (!res.ok) {
// Une clé refusée ne dit pas POURQUOI : signature invalide,
// recopie fautive et charge utile trafiquée se ressemblent, et
// détailler n'aiderait que celui qui essaie d'en fabriquer une.
hint.textContent = 'This licence key is not valid.';
input.select();
return;
}
screen.classList.add('is-hidden');
resolve();
});
});
}
async function showAuth() {
$('#authScreen').classList.remove('is-hidden');
$('#appShell').classList.add('is-hidden');
@@ -7353,6 +7418,10 @@ async function init() {
history.replaceState(null, '', location.pathname + location.hash);
}
// Avant tout le reste : un écran d'authentification qui s'affiche puis se
// fait recouvrir donnerait l'impression que le coffre était accessible.
await ensureLicence();
installCustomContextMenu();
document.addEventListener('keydown', e => {
const mod = e.ctrlKey || e.metaKey;