Add a passphrase-locked vault
A second notebook whose pages are encrypted with a key made from a passphrase plus the server key. It is unlocked into memory only (15 minutes idle, Lock, or sign-out), is not in search, lists or downloads of the ordinary notebook, and limits wrong guesses. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7ff424fa55
commit
c08a8b8718
12 files changed
+706
-28
No files matched your search
@@ -33,8 +33,8 @@ const SLASH = [
|
||||
{ name: 'Quote', hint: '>', keys: 'quote callout', text: '> ' },
|
||||
{ name: 'Code block', hint: '```', keys: 'code pre', text: '```\n\n```', caret: 4 },
|
||||
{ name: 'Divider', hint: '---', keys: 'divider line hr rule', text: '---\n' },
|
||||
{ name: 'Link to a page', hint: '[[', keys: 'link page wiki mention', text: '[[', again: true },
|
||||
{ name: 'Link to today', hint: () => isoDay(new Date()), keys: 'today date journal day', text: () => '[[' + isoDay(new Date()) + ']]' },
|
||||
{ name: 'Link to a page', hint: '[[', keys: 'link page wiki mention', text: '[[', again: true, link: true },
|
||||
{ name: 'Link to today', hint: () => isoDay(new Date()), keys: 'today date journal day', text: () => '[[' + isoDay(new Date()) + ']]', link: true },
|
||||
{ name: 'Current time', hint: () => hhmm(new Date()), keys: 'time clock now', text: () => hhmm(new Date()) + ' ' },
|
||||
];
|
||||
|
||||
@@ -76,7 +76,8 @@ function pageChoices(q) {
|
||||
return list;
|
||||
}
|
||||
|
||||
function enhance(ta, wrap) {
|
||||
// With plain set (the vault) there are no page links, so no [[ picker and no link commands.
|
||||
function enhance(ta, wrap, plain) {
|
||||
const pop = makePopup(ta, wrap);
|
||||
|
||||
function check() {
|
||||
@@ -84,7 +85,7 @@ function enhance(ta, wrap) {
|
||||
if (document.activeElement !== ta || ta.selectionStart !== ta.selectionEnd) return pop.hide();
|
||||
const pos = ta.selectionStart, ls = lineStart(ta.value, pos), before = ta.value.slice(ls, pos);
|
||||
let m;
|
||||
if ((m = /\[\[([^\[\]|\n]*)$/.exec(before))) {
|
||||
if (!plain && (m = /\[\[([^\[\]|\n]*)$/.exec(before))) {
|
||||
const from = pos - m[1].length;
|
||||
pop.show(pageChoices(m[1]), it => {
|
||||
const closed = ta.value.startsWith(']]', pos);
|
||||
@@ -92,7 +93,7 @@ function enhance(ta, wrap) {
|
||||
}, pos);
|
||||
} else if ((m = /^(\s*)\/([a-z0-9]{0,16})$/i.exec(before))) {
|
||||
const q = m[2].toLowerCase(), from = ls + m[1].length;
|
||||
pop.show(SLASH.filter(c => !q || c.name.toLowerCase().includes(q) || c.keys.includes(q)), c => {
|
||||
pop.show(SLASH.filter(c => !(plain && c.link) && (!q || c.name.toLowerCase().includes(q) || c.keys.includes(q))), c => {
|
||||
const text = typeof c.text === 'function' ? c.text() : c.text;
|
||||
replaceRange(ta, from, pos, text);
|
||||
// in the live view a command that adds lines redraws it, and the box being typed in is a new one
|
||||
@@ -154,7 +155,7 @@ function enhance(ta, wrap) {
|
||||
// ---------- live preview: every line shows rendered except the one being edited ----------
|
||||
// The text itself stays in the hidden text box `ta`; this view splices each edit back into it, so saving,
|
||||
// conflicts and the other modes need to know nothing about it. A fenced code block is edited as a whole.
|
||||
function makeLive(ta) {
|
||||
function makeLive(ta, plain) {
|
||||
const el = h('div', { class: 'live', hidden: true });
|
||||
// act: the block starting at line s is being edited, caret at col. With e set, lines s to e are edited
|
||||
// together as raw text (a selection across lines), and sel is [start, end, direction] inside them.
|
||||
@@ -178,7 +179,7 @@ function makeLive(ta) {
|
||||
function rendered(b) {
|
||||
const text = lines.slice(b.s, b.e + 1).join('\n'), box = h('div', { class: 'live-line' });
|
||||
if (text.trim()) {
|
||||
const root = md(text, (_, checked) => { lines[b.s] = lines[b.s].replace(/\[( |x|X)\]/, checked ? '[x]' : '[ ]'); commit(); draw(); }, true);
|
||||
const root = md(text, (_, checked) => { lines[b.s] = lines[b.s].replace(/\[( |x|X)\]/, checked ? '[x]' : '[ ]'); commit(); draw(); }, true, plain);
|
||||
const m = /^\s*(\d+)[.)]\s/.exec(text);
|
||||
if (m) root.querySelector('ol')?.setAttribute('start', m[1]);
|
||||
box.append(root);
|
||||
@@ -235,7 +236,7 @@ function makeLive(ta) {
|
||||
const empty = lines.length === 1 && !lines[0];
|
||||
const t = h('textarea', { class: 'live-edit', rows: 1, spellcheck: 'true', 'aria-label': 'Editing', placeholder: empty ? "Write something, or type '/' for commands, '[[' to link a page" : '' });
|
||||
t.value = lines.slice(b.s, b.e + 1).join('\n');
|
||||
enhance(t, el);
|
||||
enhance(t, el, plain);
|
||||
const multi = b.fence || b.span; // a code block or a selection over lines: Enter just adds a line
|
||||
t.addEventListener('input', () => {
|
||||
const fresh = t.value.split('\n');
|
||||
@@ -357,7 +358,9 @@ function makeLive(ta) {
|
||||
|
||||
// ---------- the page editor: autosaves title and text, and notices when a page changed elsewhere ----------
|
||||
// With no note it starts a new page, which is only created once something is typed.
|
||||
function pageEditor({ note, parent = '', fixedTitle, onCreated, onSaved }) {
|
||||
// With vault set the page lives in the vault: its own API, and no links into the ordinary notebook.
|
||||
function pageEditor({ note, parent = '', fixedTitle, onCreated, onSaved, vault = false }) {
|
||||
const base = vault ? '/api/vault' : '/api';
|
||||
let cur = note || null, titleDirty = false, bodyDirty = false, saving = false, queued = false, blocked = false;
|
||||
let timer = null, retry = null, badTitle = null, mode = 'live', lastEdit = 'live';
|
||||
|
||||
@@ -368,13 +371,13 @@ function pageEditor({ note, parent = '', fixedTitle, onCreated, onSaved }) {
|
||||
const ta = h('textarea', { class: 'write big', 'aria-label': 'Page text', placeholder: "Write something, or type '/' for commands, '[[' to link a page" });
|
||||
ta.value = note?.body ?? '';
|
||||
const view = h('div', { class: 'view', hidden: true });
|
||||
const live = makeLive(ta);
|
||||
const live = makeLive(ta, vault);
|
||||
const wrap = h('div', { class: 'editor-wrap' }, ta, live.el, view);
|
||||
const status = h('span', { class: 'sub', role: 'status' });
|
||||
const notice = h('div', { class: 'notice notice-warning', hidden: true });
|
||||
const say = t => { status.textContent = t; };
|
||||
const fit = () => { ta.style.height = 'auto'; ta.style.height = ta.scrollHeight + 2 + 'px'; };
|
||||
enhance(ta, wrap);
|
||||
enhance(ta, wrap, vault);
|
||||
|
||||
const dirty = () => titleDirty || bodyDirty;
|
||||
const ctl = { save, dirty };
|
||||
@@ -393,12 +396,12 @@ function pageEditor({ note, parent = '', fixedTitle, onCreated, onSaved }) {
|
||||
saving = true; say('Saving');
|
||||
try {
|
||||
let n;
|
||||
if (!cur) n = await api('POST', '/api/notes', { title: sendTitle ? t : '', body: b, parent }, { keepalive: b.length < 15000 });
|
||||
if (!cur) n = await api('POST', base + '/notes', { title: sendTitle ? t : '', body: b, parent }, { keepalive: b.length < 15000 });
|
||||
else {
|
||||
const payload = { base: cur.updated };
|
||||
if (sendTitle) payload.title = t;
|
||||
if (sendBody) payload.body = b;
|
||||
n = await api('PUT', '/api/notes/' + cur.id, payload, { keepalive: b.length < 15000 });
|
||||
n = await api('PUT', base + '/notes/' + cur.id, payload, { keepalive: b.length < 15000 });
|
||||
}
|
||||
const created = !cur;
|
||||
cur = n;
|
||||
@@ -419,6 +422,12 @@ function pageEditor({ note, parent = '', fixedTitle, onCreated, onSaved }) {
|
||||
h('a', { class: 'btn-link', href: '/auth/login?next=' + encodeURIComponent(location.hash), target: '_blank', rel: 'noopener' }, 'Sign in again'),
|
||||
h('span', { class: 'sub' }, 'Opens in a new tab. Come back afterwards and this page saves itself.')]);
|
||||
retry = setTimeout(save, 4000);
|
||||
} else if (e.status === 423) {
|
||||
say('Not saved');
|
||||
flag([h('span', { class: 'grow' }, 'The vault locked itself, so your latest changes are not saved yet. Your text is still here.'),
|
||||
h('a', { class: 'btn-link', href: '#/vault', target: '_blank', rel: 'noopener' }, 'Unlock the vault'),
|
||||
h('span', { class: 'sub' }, 'Opens in a new tab. Come back afterwards and this page saves itself.')]);
|
||||
retry = setTimeout(save, 4000);
|
||||
} else {
|
||||
say('Not saved');
|
||||
if (e.offline || e.status >= 500) { flag(e.message + ' Trying again.'); retry = setTimeout(save, 5000); }
|
||||
@@ -449,7 +458,7 @@ function pageEditor({ note, parent = '', fixedTitle, onCreated, onSaved }) {
|
||||
ta.addEventListener('input', () => { bodyDirty = true; say('Unsaved'); fit(); later(); });
|
||||
|
||||
// Write shows the text box; Read shows the page as it will look, with tasks you can tick and links you can follow.
|
||||
function render() { view.replaceChildren(ta.value.trim() ? md(ta.value, ticked, true) : h('p', { class: 'dim' }, 'Nothing here yet. Click to start writing.')); }
|
||||
function render() { view.replaceChildren(ta.value.trim() ? md(ta.value, ticked, true, vault) : h('p', { class: 'dim' }, 'Nothing here yet. Click to start writing.')); }
|
||||
function ticked(line, checked) {
|
||||
const lines = ta.value.split('\n');
|
||||
lines[line] = lines[line].replace(/\[( |x|X)\]/, checked ? '[x]' : '[ ]');
|
||||
|
||||
Reference in new issue
Block a user