Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d74839ec3 | |||
| df2d0f55b9 | |||
| 67b726e08c | |||
| a2ac0e393b | |||
| 329b0e3ce5 | |||
| 365ee2f596 | |||
| 994aaccac4 | |||
| ab1da7e319 | |||
| f1b7dd9bff |
@@ -1352,6 +1352,22 @@ champ — ça détruisait le focus sur les forms qui n'étaient pas centrés
|
||||
(hostname), mettre aussi `title = "Gitea"` (brand) couvre le browser
|
||||
title sans ambiguïté
|
||||
|
||||
### Exclure une entrée : étiquette `no-autofill` (2026-09-18)
|
||||
|
||||
`AUTOFILL_SKIP_TAG` (js/app.autofill.js) retire l'entrée du pool **avant** le
|
||||
scoring : raccourcis `Ctrl+Shift+L/P/T` et barrette l'ignorent. Une étiquette et
|
||||
non une colonne : elle est déjà chiffrée, synchronisée, exportée et reprise par
|
||||
la rotation — un champ de plus aurait demandé les 7 points de passage de
|
||||
« Entry payload ». La recherche rapide `Ctrl+Shift+Q` garde ces entrées (choix
|
||||
explicite, pas une correspondance devinée sur un titre).
|
||||
|
||||
**Idée gardée (rien n'est fait)** : une case « Ne pas proposer à l'autofill »
|
||||
dans l'éditeur d'entrée (slideover), qui **ajoute ou retire la même étiquette**
|
||||
— même stockage, mais plus besoin de connaître son nom exact. À brancher sur
|
||||
`soDirtyCheck` (la case modifie les tags, donc l'entrée devient modifiée) et
|
||||
sur la reconstruction des chips d'étiquettes pour que les deux restent
|
||||
d'accord.
|
||||
|
||||
### Contraintes connues autofill
|
||||
|
||||
- **UIPI** : SendInput vers process élevé bloqué. Notepad++ admin,
|
||||
|
||||
@@ -140,7 +140,8 @@ begin
|
||||
LHttp.ResponseTimeout := FTimeoutMs;
|
||||
LHttp.HandleRedirects := False;
|
||||
LHttp.UserAgent := 'PMServer';
|
||||
LHttp.CustomHeaders['Authorization'] := 'token ' + FToken;
|
||||
// SANS JETON, expres : "public" = visible par n'importe qui. Un jeton faux
|
||||
// faisait repondre 401 avant tout verdict ; prive et anonyme = 404.
|
||||
try
|
||||
LResp := LHttp.Get(RepoApiUrl(FBaseUrl));
|
||||
except
|
||||
|
||||
@@ -323,6 +323,24 @@ secret (`hasTotp`, un booléen — jamais le secret dans la page). Le code est
|
||||
calculé à l'instant de la copie ; le message dit combien de secondes il lui
|
||||
reste.
|
||||
|
||||
### Idées gardées pour plus tard (décidé le 2026-09-18)
|
||||
|
||||
**Rien n'est fait.** Deux propositions retenues pour copier encore plus vite :
|
||||
|
||||
- **Identifiant puis mot de passe, en un appui** (effort moyen). Un appui copie
|
||||
l'identifiant ; au retour dans Vault après l'avoir collé (`BecameActive`), le
|
||||
mot de passe de la même entrée est **déjà** dans le presse-papiers, avec un
|
||||
message qui le dit. À borner : seulement si le retour a lieu dans la grâce de
|
||||
2 minutes, et une seule fois — sinon un retour sans rapport recopierait un
|
||||
mot de passe que personne n'a demandé.
|
||||
- **Remplissage automatique Android** (gros chantier, plusieurs jours). Un
|
||||
`AutofillService` comme Bitwarden : Android propose les comptes dans le champ
|
||||
de connexion de n'importe quelle app ou de Chrome, sans copier-coller. Exige
|
||||
une classe **Java** (Delphi ne sait pas l'écrire) compilée en `.jar` et
|
||||
déclarée au manifeste avec `BIND_AUTOFILL_SERVICE`, et une authentification
|
||||
(code, empreinte) **dans le service** avant de proposer quoi que ce soit : le
|
||||
coffre ne vit que dans l'app, ouverte ou non.
|
||||
|
||||
## Saisie : ce qui a résisté
|
||||
|
||||
- **`user-select: none` du `body`** était hérité par les champs : le curseur ne
|
||||
|
||||
@@ -2885,6 +2885,7 @@ body[data-editor-position="center"]:has(#settingsPanel.is-open)::before {
|
||||
.col-actions { width: 100%; min-width: 80px; text-align: right; white-space: nowrap; }
|
||||
.col-actions .icon-btn { opacity: 0; transition: opacity var(--t-fast); }
|
||||
.entry-row:hover .col-actions .icon-btn { opacity: 0.7; }
|
||||
.col-actions .icon-btn.is-always { opacity: 0.7; }
|
||||
.col-actions .entry-kebab-wrap { display: inline-block; vertical-align: middle; }
|
||||
|
||||
/* Hide Edge/IE native password reveal eye (we ship our own toggle). */
|
||||
|
||||
@@ -1659,7 +1659,12 @@ begin
|
||||
// "token <PAT>" et non "Bearer" : c'est la forme que Gitea attend
|
||||
// d'un jeton d'acces personnel, et l'autre donne un 401 muet.
|
||||
if LGitea then
|
||||
LHttp.CustomHeaders['Authorization'] := 'token ' + LPwd
|
||||
begin
|
||||
// Pas d'en-tete du tout sans jeton : la verification "depot
|
||||
// public" se fait volontairement en anonyme.
|
||||
if LPwd <> '' then
|
||||
LHttp.CustomHeaders['Authorization'] := 'token ' + LPwd;
|
||||
end
|
||||
else if (LUser <> '') then
|
||||
begin
|
||||
LHttp.CredentialsStorage.AddCredential(
|
||||
|
||||
+10
-9
@@ -841,12 +841,12 @@
|
||||
</div>
|
||||
|
||||
<div class="slideover-field" id="syncField" style="display:none">
|
||||
<div class="slideover-field-label">Sync (WebDAV or Gitea)</div>
|
||||
<div class="slideover-field-label">Sync (WebDAV or Git repository)</div>
|
||||
<p style="font-size:11px;color:var(--text-faint);margin:0 0 8px;line-height:1.5">
|
||||
Bidirectional sync with a WebDAV server (Nextcloud,
|
||||
ownCloud, Apache mod_dav) <b>or a private Gitea
|
||||
repository</b> — paste a contents-API URL and it switches
|
||||
transport on its own. Auto-merge by entry
|
||||
ownCloud, Apache mod_dav) <b>or a private Git
|
||||
repository</b> (Gitea or GitHub — not GitLab) — paste the
|
||||
repository address and it switches transport on its own. Auto-merge by entry
|
||||
timestamp. <b>Master password is never sent to the
|
||||
remote</b> — the snapshot is encrypted with a separate
|
||||
sync password you set once per device (same on each).
|
||||
@@ -857,9 +857,9 @@
|
||||
</label>
|
||||
<div id="syncConfig" style="display:none;border-left:2px solid var(--border);padding-left:10px;margin-left:4px">
|
||||
<div style="margin-bottom:6px">
|
||||
<div style="font-size:11px;color:var(--text-faint);margin-bottom:4px">URL — full path to the snapshot file, or a Gitea contents path</div>
|
||||
<div style="font-size:11px;color:var(--text-faint);margin-bottom:4px">URL — the repository address (github.com/you/repo.git or your Gitea), or a WebDAV file URL</div>
|
||||
<input type="text" id="syncUrl" class="so-input"
|
||||
placeholder="https://git.example.com/api/v1/repos/OWNER/REPO/contents/vault-sync.json"
|
||||
placeholder="https://github.com/you/repo.git"
|
||||
style="width:100%;font-size:12px">
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-bottom:6px">
|
||||
@@ -873,9 +873,10 @@
|
||||
</label>
|
||||
</div>
|
||||
<p style="font-size:11px;color:var(--text-faint);margin:0 0 8px;line-height:1.5">
|
||||
On Gitea the username is ignored and the token needs
|
||||
<b>write:repository</b> — a read-only one can pull but
|
||||
never push. Each sync is a commit, and Git history is
|
||||
On a Git repository the username is ignored and the
|
||||
token needs write access (Gitea: <b>write:repository</b>,
|
||||
GitHub: <b>Contents: Read and write</b>) — a read-only
|
||||
one can pull but never push. Each sync is a commit, and Git history is
|
||||
kept: an old sync password stays attackable in earlier
|
||||
versions, so prune the history if you rotate it.
|
||||
</p>
|
||||
|
||||
+10
-3
@@ -147,6 +147,8 @@ function autofillPushHotkeys(silent) {
|
||||
const BROWSER_SUFFIX_RE =
|
||||
/\s*[-—–|]\s*(google chrome|chromium|mozilla firefox|firefox|microsoft edge|edge|brave|opera|vivaldi|safari|tor browser|tor|arc)\s*$/i;
|
||||
|
||||
const AUTOFILL_SKIP_TAG = 'no-autofill';
|
||||
|
||||
function autofillStripBrowserSuffix(title) {
|
||||
return (title || '').replace(BROWSER_SUFFIX_RE, '').trim();
|
||||
}
|
||||
@@ -274,9 +276,14 @@ async function autofillHandleRequest(windowTitle, kind) {
|
||||
// The 2FA hotkey can only serve entries that actually carry a TOTP secret
|
||||
// — filtering here (not after scoring) also means the picker never lists a
|
||||
// candidate that would fail once chosen.
|
||||
const pool = (kind === 'totp')
|
||||
? state.entries.filter(e => e.totp_secret && e.totp_iv)
|
||||
: state.entries;
|
||||
// EXCLUE DE L'AUTOFILL par l'etiquette reservee AUTOFILL_SKIP_TAG : une
|
||||
// etiquette et non un champ, car elle voyage deja partout (chiffree, synchro,
|
||||
// export, rotation) sans une colonne de plus, et un clic dessus liste toutes
|
||||
// les entrees exclues. La recherche rapide (Ctrl+Shift+Q) les garde : c'est
|
||||
// un choix explicite, pas une correspondance devinee sur un titre.
|
||||
const pool = state.entries
|
||||
.filter(e => !parseTags(e.tags).some(t => t.toLowerCase() === AUTOFILL_SKIP_TAG))
|
||||
.filter(e => kind !== 'totp' || (e.totp_secret && e.totp_iv));
|
||||
const scored = pool
|
||||
.map(e => ({ entry: e, score: autofillScore(e, titleLower) }))
|
||||
.filter(x => x.score > 0)
|
||||
|
||||
@@ -3606,8 +3606,10 @@ function renderTableRow(e) {
|
||||
break;
|
||||
case 'actions': {
|
||||
td = el('td', { class: 'col-actions' });
|
||||
// TOUJOURS VISIBLE, contrairement aux autres actions de la
|
||||
// ligne : c'est le geste le plus frequent de la vue tableau.
|
||||
const pwBtn = el('button', {
|
||||
class: 'icon-btn icon-btn-sm',
|
||||
class: 'icon-btn icon-btn-sm is-always',
|
||||
title: e.kind === 'note' ? 'Copy note content' : 'Copy password',
|
||||
on: { click: ev => { ev.stopPropagation(); copyPassword(e); } },
|
||||
});
|
||||
@@ -6859,7 +6861,7 @@ const SETTINGS_TAB_OF = {
|
||||
'Fill bar': 'fillbar',
|
||||
'Account': 'account',
|
||||
'PIN unlock': 'account', 'Quick unlock': 'account', 'Recovery key': 'account',
|
||||
'Export': 'sync', 'Sync (WebDAV or Gitea)': 'sync',
|
||||
'Export': 'sync', 'Sync (WebDAV or Git repository)': 'sync',
|
||||
'Auto-backup': 'sync', 'Import': 'sync',
|
||||
};
|
||||
let settingsActiveTab = 'general';
|
||||
@@ -8219,8 +8221,17 @@ async function init() {
|
||||
if (syncCfgBox) syncCfgBox.style.display = on ? '' : 'none';
|
||||
toast(on ? 'Sync enabled' : 'Sync disabled');
|
||||
});
|
||||
if (syncUrlEl) syncUrlEl.addEventListener('change', e =>
|
||||
Bridge.setPref(SYNC_PREFS.url, e.target.value.trim()));
|
||||
// L'adresse du depot suffit (github.com/O/R.git, Gitea) : elle est
|
||||
// convertie en URL d'API, et le champ montre ce qui est enregistre.
|
||||
if (syncUrlEl) syncUrlEl.addEventListener('change', e => {
|
||||
const raw = e.target.value.trim();
|
||||
const url = syncNormalizeRepoUrl(raw);
|
||||
if (url !== raw) {
|
||||
e.target.value = url;
|
||||
toast('Repository address converted to its API URL');
|
||||
}
|
||||
Bridge.setPref(SYNC_PREFS.url, url);
|
||||
});
|
||||
if (syncUserEl) syncUserEl.addEventListener('change', e =>
|
||||
Bridge.setPref(SYNC_PREFS.user, e.target.value.trim()));
|
||||
if (syncPwdEl) syncPwdEl.addEventListener('change', e =>
|
||||
|
||||
+30
-4
@@ -90,13 +90,36 @@ function syncIsGitea(url) {
|
||||
return u.includes('/api/v1/repos/') || u.includes('api.github.com/repos/');
|
||||
}
|
||||
|
||||
// ADRESSE DU DEPOT TELLE QUE LE NAVIGATEUR (OU "git clone") LA DONNE.
|
||||
// https://github.com/O/R(.git) -> API de GitHub ; https://hote/O/R(.git) -> API
|
||||
// de Gitea. Fichier par defaut vault-sync.json, ou celui d'un lien .../blob/B/F
|
||||
// (GitHub) ou .../src/branch/B/F (Gitea). Tout le reste passe tel quel - une
|
||||
// URL WebDAV, une URL d'API deja complete. GARDE : un dernier segment qui a
|
||||
// l'air d'un fichier (.json...) n'est pas un depot - sinon
|
||||
// https://serveur/webdav/vault.json deviendrait une API Gitea imaginaire.
|
||||
function syncNormalizeRepoUrl(u) {
|
||||
u = (u || '').replace(/\s+/g, '');
|
||||
if (!u || syncIsGitea(u)) return u;
|
||||
const g = u.match(/^https?:\/\/(www\.)?github\.com\/([^\/]+)\/([^\/]+?)(?:\.git)?(?:\/blob\/[^\/]+\/(.+?))?\/?$/i);
|
||||
if (g) return 'https://api.github.com/repos/' + g[2] + '/' + g[3] +
|
||||
'/contents/' + (g[4] || 'vault-sync.json');
|
||||
const m = u.match(/^(https?:\/\/[^\/]+)\/([^\/]+)\/([^\/]+?)(\.git)?(?:\/src\/branch\/[^\/]+\/(.+?))?\/?$/i);
|
||||
if (!m) return u;
|
||||
if (!m[4] && !m[5] && /\.[a-z0-9]{2,5}$/i.test(m[3])) return u; // un fichier, pas un depot
|
||||
return m[1] + '/api/v1/repos/' + m[2] + '/' + m[3] + '/contents/' + (m[5] || 'vault-sync.json');
|
||||
}
|
||||
|
||||
// UN DEPOT PUBLIC EST REFUSE. Chaque synchro est un commit : l'historique garde
|
||||
// tous les instantanes, chacun attaquable hors ligne par qui peut les lire.
|
||||
// '' = prive ou impossible a savoir (on ne bloque pas sur un doute reseau).
|
||||
async function syncRepoPublicWarning(cfg) {
|
||||
if (!syncIsGitea(cfg.url)) return '';
|
||||
const repoUrl = cfg.url.split('/contents/')[0];
|
||||
const r = await _webdavCall('get', repoUrl, cfg.user, cfg.pwd);
|
||||
// SANS JETON, expres : "public" veut dire "visible par n'importe qui", et
|
||||
// c'est exactement la question posee. Avec un jeton faux, GitHub repondait
|
||||
// 401 avant de dire quoi que ce soit du depot ; un depot prive repond 404
|
||||
// sans jeton, ce qui veut dire "pas public".
|
||||
const r = await _webdavCall('get', repoUrl, '', '');
|
||||
if (r.status < 200 || r.status >= 300 || !r.payload) return '';
|
||||
try {
|
||||
const o = JSON.parse(new TextDecoder().decode(base64ToBytes(r.payload)));
|
||||
@@ -273,7 +296,9 @@ async function loadSyncConfig(withSecrets) {
|
||||
]);
|
||||
return {
|
||||
enabled: enabled === '1',
|
||||
url: url || '',
|
||||
// Convertie aussi a la LECTURE : une adresse de depot enregistree par un
|
||||
// exe anterieur (ou tapee avant la conversion) ne l'avait jamais ete.
|
||||
url: syncNormalizeRepoUrl(url || ''),
|
||||
user: user || '',
|
||||
pwd: pwd || '',
|
||||
encPwd: encPwd || '',
|
||||
@@ -288,6 +313,9 @@ async function syncTestConnection() {
|
||||
const cfg = await loadSyncConfig(true); // needs the WebDAV credentials
|
||||
if (!cfg || !cfg.url) return toast('Set the WebDAV URL first', 'warning');
|
||||
toast('Testing connection…');
|
||||
// LE DEPOT PUBLIC D'ABORD : il est refuse quel que soit le jeton.
|
||||
const pubFirst = await syncRepoPublicWarning(cfg);
|
||||
if (pubFirst) return toast(pubFirst, 'error');
|
||||
const r = await _webdavCall('test', cfg.url, cfg.user, cfg.pwd);
|
||||
// A reachable Gitea with a valid token is not enough: a URL pointing one
|
||||
// path segment off answers 200 with JSON that is not a file. Checking the
|
||||
@@ -297,8 +325,6 @@ async function syncTestConnection() {
|
||||
try {
|
||||
un = giteaUnwrap(new TextDecoder().decode(base64ToBytes(r.payload || '')));
|
||||
} catch (e) { /* traite comme une forme inattendue */ }
|
||||
const pub = await syncRepoPublicWarning(cfg);
|
||||
if (pub) return toast(pub, 'error');
|
||||
return un
|
||||
? toast('Connection OK · file found in the repository')
|
||||
: toast('Reached Gitea, but this URL does not return a file — ' +
|
||||
|
||||
Reference in New Issue
Block a user