Compare commits

..

1 Commits

Author SHA1 Message Date
perf3ct
79dda887a6 feat(docs): add asyncapi websocket docs 2025-08-20 19:01:24 +00:00
16 changed files with 983 additions and 2995 deletions

View File

@@ -35,7 +35,7 @@
"chore:generate-openapi": "tsx bin/generate-openapi.js"
},
"devDependencies": {
"@playwright/test": "1.55.0",
"@playwright/test": "1.54.2",
"@stylistic/eslint-plugin": "5.2.3",
"@types/express": "5.0.3",
"@types/node": "22.17.2",

View File

@@ -36,7 +36,7 @@
"draggabilly": "3.0.0",
"force-graph": "1.50.1",
"globals": "16.3.0",
"i18next": "25.4.0",
"i18next": "25.3.6",
"i18next-http-backend": "3.0.2",
"jquery": "3.7.1",
"jquery.fancytree": "2.38.5",
@@ -52,7 +52,7 @@
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.27.1",
"react-i18next": "15.7.0",
"react-i18next": "15.6.1",
"split.js": "1.6.5",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",

View File

@@ -1 +0,0 @@
{}

View File

@@ -409,11 +409,6 @@
"template": "Esta nota aparecerá na seleção de modelos disponíveis ao criar uma nova nota",
"toc": "<code>#toc</code> ou <code>#toc=show</code> irá forçar a exibição do Sumário, <code>#toc=hide</code> irá forçar que ele fique oculto. Se o rótulo não existir, será considerado o ajuste global",
"color": "define a cor da nota na árvore de notas, links etc. Use qualquer valor de cor CSS válido, como 'red' ou #a13d5f",
"keyboard_shortcut": "Define um atalho de teclado que irá pular imediatamente para esta nota. Exemplo: 'ctrl+alt+e'. É necessário recarregar o frontend para que a alteração tenha efeito.",
"hide_highlight_widget": "Ocultar o widget da lista de destaques",
"keep_current_hoisting": "Abrir este link não alterará o destaque, mesmo que a nota não seja exibível na subárvore destacada atual.",
"execute_button": "Titulo do botão que executará a nota de código atual",
"exclude_from_note_map": "Notas com este rótulo ficarão ocultas no Mapa de Notas",
"new_notes_on_top": "Novas notas serão criadas no topo da nota raiz, não na parte inferior."
"keyboard_shortcut": "Define um atalho de teclado que irá pular imediatamente para esta nota. Exemplo: 'ctrl+alt+e'. É necessário recarregar o frontend para que a alteração tenha efeito."
}
}

View File

@@ -12,7 +12,7 @@
"branch_prefix": {
"save": "Зберегти",
"edit_branch_prefix": "Редагувати префікс гілки",
"help_on_tree_prefix": "Довідка щодо префіксу дерева",
"help_on_tree_prefix": "Довідка щодо префіксів гілок",
"prefix": "Префікс: ",
"branch_prefix_saved": "Префікс гілки збережено."
},
@@ -27,32 +27,7 @@
"sync_version": "Версія синхронізації:"
},
"global_menu": {
"about": "Про Trilium Notes",
"menu": "Меню",
"options": "Параметри",
"open_new_window": "Відкрити Нове вікно",
"switch_to_mobile_version": "Перейти на мобільну версію",
"switch_to_desktop_version": "Перейти на версію для ПК",
"zoom": "Масштаб",
"toggle_fullscreen": "Увімкнути повноекранний режим",
"zoom_out": "Зменшити масштаб",
"reset_zoom_level": "Скинути масштабування",
"zoom_in": "Збільшити масштаб",
"configure_launchbar": "Налаштувати панель запуску",
"show_shared_notes_subtree": "Показати піддерево спільних нотаток",
"advanced": "Розширені",
"open_dev_tools": "Відкрити інструменти розробника",
"open_sql_console": "Відкрити консоль SQL",
"open_sql_console_history": "Відкрити історію консолі SQL",
"open_search_history": "Відкрити історію пошуку",
"show_backend_log": "Показати Backend Log",
"reload_hint": "Перезавантаження може допомогти з деякими візуальними збоями без перезавантаження всієї програми.",
"reload_frontend": "Перезавантажити інтерфейс",
"show_hidden_subtree": "Показати приховане піддерево",
"show_help": "Показати довідку",
"logout": "Вийти",
"show-cheatsheet": "Показати Шпаргалку",
"toggle-zen-mode": "Дзен-режим"
"about": "Про Trilium Notes"
},
"modal": {
"help_title": "Показати більше інформації про це вікно"
@@ -63,13 +38,13 @@
"message": "Сталася критична помилка, яка перешкоджає запуску клієнтської програми:\n\n{{message}}\n\nНайімовірніше, це спричинено несподіваною помилкою скрипту. Спробуйте запустити програму в безпечному режимі та вирішити проблему."
},
"widget-error": {
"title": "Не вдалася ініціалізація віджета",
"title": "Помилка ініціалізації віджета",
"message-custom": "Не вдалося ініціалізувати користувацький віджет із нотатки з ID \"{{id}}\" під назвою \"{{title}}\" через:\n\n{{message}}",
"message-unknown": "Невідомий віджет не вдалося ініціалізувати через:\n\n{{message}}"
},
"bundle-error": {
"title": "Не вдалося завантажити користувацький скрипт",
"message": "Скрипт з нотатки з ID \"{{id}}\" з заголовком \"{{title}}\" не вдалося виконати через:\n\n{{message}}"
"title": "Не вдалося завантажити власний скрипт",
"message": "Скрипт із нотатки з ID \"{{id}}\" під назвою \"{{title}}\" не вдалося виконати через:\n\n{{message}}"
}
},
"bulk_actions": {
@@ -82,15 +57,15 @@
"none_yet": "Поки що немає... додайте дію, натиснувши одну з доступних вище.",
"include_descendants": "Включити нащадків вибраних нотаток",
"labels": "Мітки",
"relations": "Зв'язки",
"relations": "Відносини",
"notes": "Нотатки",
"other": "Інше"
},
"clone_to": {
"clone_notes_to": "Клонувати нотатки до...",
"target_parent_note": "Цільова батьківська нотатка",
"search_for_note_by_its_name": "пошук нотатки за назвою",
"help_on_links": "Довідка щодо посилань",
"search_for_note_by_its_name": "Знайти нотатку за назвою",
"help_on_links": "Допомога з посиланнями",
"notes_to_clone": "Нотатки для клонування",
"cloned_note_prefix_title": "Клонована нотатка буде відображатися в дереві нотаток із заданим префіксом",
"prefix_optional": "Префікс (необов'язково)",
@@ -148,7 +123,7 @@
"delete_notes_preview": "Видалити попередній перегляд нотаток",
"close": "Закрити",
"delete_all_clones_description": "Видалити також усі клони (можна скасувати в останніх змінах)",
"erase_notes_description": "Звичайне (м’яке) видалення лише позначає нотатки як видалені і їх можна відновити (у діалоговому вікні останніх змін) протягом певного періоду часу. Якщо позначити цю опцію, нотатки будуть видалені негайно і їх неможливо буде відновити.",
"erase_notes_description": "Звичайне (м’яке) видалення лише позначає нотатки як видалені, і їх можна відновити (у діалоговому вікні останніх змін) протягом певного періоду часу. Якщо позначити цю опцію, нотатки будуть видалені негайно, і їх неможливо буде відновити.",
"erase_notes_warning": "Стерти нотатки назавжди (скасувати не можна), включаючи всі клони. Це призведе до перезавантаження програми.",
"notes_to_be_deleted": "Наступні нотатки будуть видалені ({{notesCount}})",
"no_note_to_delete": "Жодну нотатку не буде видалено (лише клони).",
@@ -161,16 +136,16 @@
"export_note_title": "Експорт нотатки",
"close": "Закрити",
"export_type_subtree": "Ця нотатка та всі її нащадки",
"format_html": "HTML рекомендовано, оскільки зберігає форматування",
"format_html_zip": "HTML у ZIP-архіві рекомендовано, зберігає форматування.",
"format_markdown": "Markdown зберігає більшу частину форматування.",
"format_html": "HTML рекомендовано, оскільки він зберігає всі формати",
"format_html_zip": "HTML у ZIP-архіві це рекомендовано, оскільки це зберігає все форматування.",
"format_markdown": "Markdown це зберігає більшу частину форматування.",
"format_opml": "OPML формат обміну структурами лише для тексту. Форматування, зображення та файли не включено.",
"opml_version_1": "OPML версії 1.0 лише звичайний текст",
"opml_version_2": "OPML v2.0 - також дозволяє HTML",
"export_type_single": "Тільки ця нотатка без її нащадків",
"export": "Експорт",
"choose_export_type": "Спочатку виберіть тип експорту",
"export_status": "Статус експорту",
"export_status": "Стан експорту",
"export_in_progress": "Триває експорт: {{progressCount}}",
"export_finished_successfully": "Експорт успішно завершено.",
"format_pdf": "PDF для друку або спільного використання."
@@ -210,7 +185,7 @@
"pasteNotes": "вставити нотатку(и) як піднотатку в активну нотатку (яка або переміщується, або клонується залежно від того, чи була вона скопійована, чи вирізана в буфер обміну)",
"deleteNotes": "видалити нотатку / піддерево",
"editingNotes": "Редагування нотаток",
"editNoteTitle": "на панелі дерева перемкнеться з панелі дерева на заголовок нотатки. Введення з заголовку нотатки перемкне фокус на текстовий редактор. <kbd>Ctrl+.</kbd> перемкнеться назад з редактора на панель дерева.",
"editNoteTitle": "На панелі дерева перемкнеться з панелі дерева на назву нотатки. Введення з назви нотатки перемкне фокус на текстовий редактор. <kbd>Ctrl+.</kbd> перемкнеться назад з редактора на панель дерева.",
"createEditLink": "створити / редагувати зовнішнє посилання",
"createInternalLink": "створити внутрішнє посилання",
"followLink": "перейти за посиланням під курсором",
@@ -226,14 +201,14 @@
"showDevTools": "показати інструменти розробника",
"showSQLConsole": "показати консоль SQL",
"other": "Інше",
"quickSearch": "фокус на швидкому введенні пошуку",
"quickSearch": "зосередження на швидкому введенні пошукового запиту",
"inPageSearch": "пошук на сторінці"
},
"import": {
"importIntoNote": "Імпортувати в нотатку",
"chooseImportFile": "Вибрати файл імпорту",
"importDescription": "Вміст вибраного(их) файлу(ів) буде імпортовано як дочірню(і) нотатку(и) до",
"options": "Параметри",
"options": "Опції",
"safeImportTooltip": "Експортовані файли Trilium <code>.zip</code> можуть містити виконувані скрипти, які можуть мати шкідливу поведінку. Безпечний імпорт деактивує автоматичне виконання всіх імпортованих скриптів. Зніміть позначку \"Безпечний імпорт\", лише якщо імпортований архів має містити виконувані скрипти, і ви повністю довіряєте вмісту файлу імпорту.",
"safeImport": "Безпечний імпорт",
"explodeArchivesTooltip": "Якщо цей прапорець позначено, Trilium читатиме файли <code>.zip</code>, <code>.enex</code> та <code>.opml</code> і створюватиме нотатки з файлів усередині цих архівів. Якщо прапорець знято, Trilium додаватиме самі архіви до нотатки.",
@@ -241,19 +216,10 @@
"shrinkImagesTooltip": "<p>Якщо ви позначите цей параметр, Trilium спробує зменшити імпортовані зображення шляхом масштабування та оптимізації, що може вплинути на сприйняту якість зображення. Якщо не позначити, зображення будуть імпортовані без змін.</p><p>Це не стосується імпорту <code>.zip</code> з метаданими, оскільки передбачається, що ці файли вже оптимізовані.</p>",
"shrinkImages": "Зменшити зображення",
"textImportedAsText": "Імпортувати HTML, Markdown та TXT як текстові нотатки, якщо це незрозуміло з метаданих",
"codeImportedAsCode": "Імпортувати розпізнані файли коду (наприклад, <code>.json</code>) як нотатки з кодом, якщо це незрозуміло з метаданих",
"codeImportedAsCode": "Імпортувати розпізнані файли коду (наприклад, <code>.json</code>) як нотатки до коду, якщо це незрозуміло з метаданих",
"replaceUnderscoresWithSpaces": "Замінити підкреслення пробілами в назвах імпортованих нотаток",
"import": "Імпорт",
"failed": "Помилка імпорту: {{message}}.",
"html_import_tags": {
"title": "Теги імпорту HTML",
"description": "Налаштуйте, які теги HTML слід зберігати під час імпорту нотаток. Теги, яких немає в цьому списку, будуть видалені під час імпорту. Деякі теги (наприклад, 'script') завжди видаляються з міркувань безпеки.",
"placeholder": "Введіть теги HTML, по одному на рядок",
"reset_button": "Скинути до Список за замовчуванням"
},
"import-status": "Статус імпорту",
"in-progress": "Триває імпорт: {{progress}}",
"successful": "Імпорт успішно завершено."
"failed": "Помилка імпорту: {{message}}."
},
"prompt": {
"title": "Підказка",
@@ -276,455 +242,7 @@
"confirm_undelete": "Ви хочете відновити цю нотатку та її піднотатки?"
},
"revisions": {
"note_revisions": "Версії нотаток",
"delete_all_revisions": "Видалити всі версії цієї нотатки",
"delete_all_button": "Видалити всі версії",
"help_title": "Довідка щодо версій нотаток",
"revision_last_edited": "Цю версію востаннє редагували {{date}}",
"confirm_delete_all": "Ви хочете видалити всі версії цієї нотатки?",
"no_revisions": "Поки що немає версій цієї нотатки...",
"restore_button": "Відновити",
"confirm_restore": "Ви хочете відновити цю версію? Це замінить поточний заголовок та вміст нотатки цієї версії.",
"delete_button": "Видалити",
"confirm_delete": "Ви хочете видалити цю версію?",
"revisions_deleted": "Версії нотаток видалено.",
"revision_restored": "Версію нотатки відновлено.",
"revision_deleted": "Версію нотатки видалено.",
"snapshot_interval": "Інтервал знімків версій нотатки: {{seconds}}s.",
"maximum_revisions": "Ліміт знімків версій нотатки: {{number}}.",
"settings": "Налаштування версій нотатки",
"download_button": "Завантажити",
"mime": "МІМЕ: ",
"file_size": "Розмір файлу:",
"preview": "Попередній перегляд:",
"preview_not_available": "Попередній перегляд недоступний для цього типу нотатки."
},
"include_note": {
"dialog_title": "Включити нотатку",
"label_note": "Нотатка",
"placeholder_search": "пошук нотатки за її назвою",
"box_size_prompt": "Розмір вмісту з вкладеною нотаткою:",
"box_size_small": "маленький (~ 10 рядків)",
"box_size_medium": "середній (~ 30 рядків)",
"box_size_full": "повний (вміст показує повний текст)",
"button_include": "Включити Нотатку"
},
"info": {
"modalTitle": "Інформаційне повідомлення",
"closeButton": "Закрити",
"okButton": "ОК"
},
"jump_to_note": {
"search_placeholder": "Пошук нотатки за її назвою або типом > для команд...",
"search_button": "Повнотекстовий Пошук"
},
"markdown_import": {
"dialog_title": "Імпорт з Markdown",
"modal_body_text": "Через \"пісочницю\" браузера неможливо безпосередньо зчитувати буфер обміну з JavaScript. Будь ласка, вставте код Markdown для імпорту в текстове поле нижче та натисніть кнопку \"Імпортувати\"",
"import_button": "Імпорт",
"import_success": "Вміст Markdown імпортовано в документ."
},
"move_to": {
"dialog_title": "Перемістити нотатки до ...",
"notes_to_move": "Нотатки для переміщення",
"search_placeholder": "пошук нотатки за її назвою",
"move_button": "Перейти до вибраної нотатки",
"error_no_path": "Немає шляху для переміщення.",
"move_success_message": "Вибрані нотатки переміщено до ",
"target_parent_note": "Цільова батьківська нотатка"
},
"note_type_chooser": {
"change_path_prompt": "Змінити місце створення нової нотатки:",
"search_placeholder": "пошук шляху за назвою (за замовчуванням, якщо порожня)",
"modal_title": "Вибрати тип нотатки",
"modal_body": "Вибрати тип/шаблон нової нотатки:",
"templates": "Шаблони",
"builtin_templates": "Вбудовані Шаблони"
},
"password_not_set": {
"title": "Пароль не встановлено",
"body1": "Захищені нотатки шифруються за допомогою пароля користувача, але пароль ще не встановлено.",
"body2": "Щоб захистити нотатки, натисніть кнопку нижче, щоб відкрити діалогове вікно Параметри та встановити пароль.",
"go_to_password_options": "Перейти до параметрів пароля"
},
"sort_child_notes": {
"sort_children_by": "Сортування дочірніх за...",
"sorting_criteria": "Критерії сортування",
"title": "заголовок",
"date_created": "дата створення",
"date_modified": "дата зміни",
"sorting_direction": "Напрямок сортування",
"ascending": "зростання",
"descending": "спадний",
"folders": "Папки",
"sort_folders_at_top": "сортувати папки зверху",
"natural_sort": "Нативне сортування",
"sort_with_respect_to_different_character_sorting": "сортувати з урахуванням різних правил сортування та порівняння символів у різних мовах або регіонах.",
"natural_sort_language": "Мова нативного сортування",
"the_language_code_for_natural_sort": "Код мови для нативного сортування, наприклад, \"zh-CN\" для китайської.",
"sort": "Сортування"
},
"upload_attachments": {
"upload_attachments_to_note": "Завантажити вкладення до нотатки",
"choose_files": "Вибрати файли",
"files_will_be_uploaded": "Файли будуть завантажені як вкладення до {{noteTitle}}",
"options": "Параметри",
"shrink_images": "Зменшити зображення",
"upload": "Скачати",
"tooltip": "Якщо ви позначите цей параметр, Trilium спробує зменшити розмір завантажених зображень шляхом масштабування та оптимізації, що може вплинути на сприйняту якість зображення. Якщо вимкнути цей параметр, зображення будуть завантажені без змін."
},
"attribute_detail": {
"attr_detail_title": "Деталі Атрибуту Заголовок",
"close_button_title": "Скасувати зміни та закрити",
"attr_is_owned_by": "Атрибут належить",
"attr_name_title": "Ім'я атрибута може складатися лише з буквено-цифрових символів, двокрапки та символу підкреслення",
"name": "Назва",
"value": "Значення",
"target_note_title": "Зв'язок — це іменований зв'язок між джерелом та цільовою нотаткою.",
"target_note": "Цільова нотатка",
"promoted_title": "Просунутий атрибут відображається на нотатці у помітному вигляді.",
"promoted": "Просунуті",
"promoted_alias_title": "Назва, яка відображатиметься в інтерфейсі просунутих атрибутів.",
"promoted_alias": "Псевдонім",
"multiplicity_title": "Множинність визначає, скільки атрибутів з однаковою назвою можна створити — максимум 1 або більше 1.",
"multiplicity": "Множинність",
"single_value": "Одне значення",
"multi_value": "Декілька значень",
"label_type_title": "Тип мітки допоможе Trilium вибрати відповідний інтерфейс для введення значення мітки.",
"label_type": "Тип",
"text": "Текст",
"number": "Номер",
"boolean": "Булева",
"date": "Дата",
"date_time": "Дата & Час",
"time": "Час",
"url": "URL",
"precision_title": "Яка кількість цифр після числа з плаваючою комою має бути доступна в інтерфейсі налаштування значень.",
"precision": "Точність",
"digits": "цифри",
"inverse_relation_title": "Додаткове налаштування для визначення, який зв'язок є зворотнім цьому зв'язку. Приклад: Батьківська - Дочірня інверсним зв'язком одне до одного.",
"inverse_relation": "Інверсний зв'язок",
"inheritable_title": "Спадковий атрибут буде успадкований усіма нащадками в цьому дереві.",
"inheritable": "Спадковий",
"save_and_close": "Зберегти & закрити <kbd>Ctrl+Enter</kbd>",
"delete": "Видалити",
"related_notes_title": "Інші нотатки з цією міткою",
"more_notes": "Більше нотаток",
"label": "Деталі Мітки",
"label_definition": "Деталі визначення мітки",
"relation": "Деталі зв'язку",
"relation_definition": "Деталі визначення зв'язку",
"disable_versioning": "вимикає автоматичне керування версіями. Корисно, наприклад, для великих, але неважливих нотаток, наприклад, великих JS-бібліотек, що використовуються для написання скриптів",
"calendar_root": "позначити нотатку, яка буде використовуватись для щоденника за замовчуванням. Тільки одна може бути такою.",
"archived": "нотатки з цією міткою не будуть видимими за замовчуванням у результатах пошуку (також у діалогових вікнах Перейти до..., Додати посилання... тощо).",
"exclude_from_export": "нотатки (з їхнім піддеревом) не будуть включені до жодного експорту нотаток",
"run": "визначає, за яких подій має запускатися скрипт. Можливі значення:\n<ul>\n<li>frontendStartup коли запускається (або оновлюється) інтерфейс Trilium, але не на мобільному пристрої.</li>\n<li>mobileStartup коли запускається (або оновлюється) інтерфейс Trilium на мобільному пристрої.</li>\n<li>backendStartup коли запускається бекенд Trilium</li>\n<li>hourly запускається раз на годину. Ви можете використовувати додаткову мітку <code>runAtHour</code>, щоб вказати, о котрій годині.</li>\n<li>daily запускається раз на день</li>\n</ul>",
"run_on_instance": "Визначити, на якому екземплярі Trilium це має бути запущено. За замовчуванням використовувати всі екземпляри.",
"run_at_hour": "О котрій годині це має запускатися? Слід використовувати разом з <code>#run=hourly</code>. Можна визначити кілька разів для більшої кількості запусків протягом дня.",
"disable_inclusion": "скрипти з цією міткою не будуть включені до виконання базового скрипта.",
"sorted": "зберігає дочірні нотатки, відсортовані за заголовком в алфавітному порядку",
"sort_direction": "ASC (за замовчуванням) або DESC",
"sort_folders_first": "Папки (нотатки з дочірніми) слід сортувати зверху",
"top": "зберегти задану нотатку зверху в батьківській (застосовується лише до відсортованих батьківських)",
"hide_promoted_attributes": "Сховати просунуті атрибути для цієї нотатки",
"read_only": "редактор перебуває в режимі лише для читання. Працює лише для тексту та нотаток з кодом.",
"auto_read_only_disabled": "текстові/кодові нотатки можна автоматично перевести в режим читання, якщо вони занадто великі. Ви можете вимкнути цю поведінку для кожної окремої нотатки, додавши до неї цю позначку",
"app_css": "позначає CSS-нотатки, які завантажуються в програму Trilium і, таким чином, можуть бути використані для зміни зовнішнього вигляду Trilium.",
"app_theme": "позначає CSS-нотатки, які є повноцінними темами Trilium і тому доступні в параметрах Trilium.",
"app_theme_base": "встановіть значення \"наступна\", \"наступна-світла\" або \"наступна-темна\", щоб використовувати відповідну тему TriliumNext (автоматичну, світлу або темну) як основу для власної теми, замість застарілої.",
"css_class": "значення цієї мітки потім додається як CSS-клас до вузла, що представляє задану нотатку в дереві. Це може бути корисним для розширеного налаштування тем. Можна використовувати в шаблонах нотаток.",
"icon_class": "значення цієї мітки додається як CSS-клас до значка на дереві, що може допомогти візуально розрізнити нотатки в дереві. Прикладом може бути bx bx-home - значки взяті з boxicons. Можна використовувати в шаблонах нотаток.",
"page_size": "кількість елементів на сторінці у списку нотаток",
"custom_request_handler": "див. <a href=\"javascript:\" data-help-page=\"custom-request-handler.html\">Спеціальний обробник запитів</a>",
"custom_resource_provider": "див. <a href=\"javascript:\" data-help-page=\"custom-request-handler.html\">Спеціальний обробник запитів</a>",
"widget": "позначає цю нотатку як користувацький віджет, який буде додано до дерева компонентів Trilium",
"workspace": "позначає цю нотатку як робочу область, що дозволяє легко хостити",
"workspace_icon_class": "визначає значка CSS-класу, який буде використовуватися у вкладці при хостингу цієї нотатки",
"workspace_tab_background_color": "Колір CSS, що використовується у вкладці нотатки під час перенесення до цієї нотатки",
"workspace_calendar_root": "Визначає календар для кожного робочого простору",
"workspace_template": "Ця нотатка з'явиться у списку доступних шаблонів під час створення нової нотатки, але лише після перенесення її в робочу область, що містить цей шаблон",
"search_home": "нові пошукові нотатки будуть створені як дочірні елементи цієї нотатки",
"workspace_search_home": "нові пошукові нотатки будуть створені як дочірні елементи цієї нотатки після хостингу до якогось предка цієї нотатки робочої області",
"inbox": "розташування Вхідних за замовчуванням для нових нотаток під час створення нотатки за допомогою кнопки Нова нотатка на бічній панелі, нотатки будуть створені як дочірні нотатки в нотатці з міткою <code>#inbox</code>.",
"workspace_inbox": "розташування Вхідні за замовчуванням для нових нотаток, коли вони переносяться до якоїсь батьківської нотатки в робочій області",
"sql_console_home": "розташування нотаток консолі SQL за замовчуванням",
"bookmark_folder": "нотатка з цією міткою відображатиметься в закладках як папка (що дозволить доступ до її дочірніх елементів)",
"share_hidden_from_tree": "ця нотатка прихована в лівому дереві навігації, але все ще доступна за її URL-адресою",
"share_external_link": "нотатка діятиме як посилання на зовнішній вебсайт у дереві спільного доступу",
"share_alias": "визначає псевдонім, за допомогою якого нотатка буде доступна за адресою https://your_trilium_host/share/[your_alias]",
"share_omit_default_css": "CSS сторінки спільного доступу за замовчуванням буде пропущено. Використовуйте, коли ви вносите значні зміни стилю.",
"share_description": "визначити текст, який буде додано до метатегу HTML для опису",
"share_raw": "нотатка буде надаватися у необробленому форматі, без HTML-оболонки",
"share_disallow_robot_indexing": "заборонити індексацію цієї нотатки роботами через заголовок <code>X-Robots-Tag: noindex</code>",
"share_credentials": "потрібні облікові дані для доступу до цієї спільної нотатки. Значення повинно бути у форматі «ім'я користувача:пароль». Не забудьте зробити це спадковим, щоб застосувати до дочірніх нотаток/зображень.",
"share_index": "нотатка з цією міткою відобразить список усіх коренів спільних нотаток",
"display_relations": "назви зв'язків, розділені комами, які слід відображати. Усі інші будуть приховані.",
"hide_relations": "назви зв'язків, розділені комами, які слід приховати. Усі інші будуть відображені.",
"title_template": "заголовок нотаток за замовчуванням, створених як дочірні елементи цієї нотатки. Значення оцінюється як рядок JavaScript\nі таким чином може бути збагачене динамічним контентом за допомогою вставлених змінних <code>now</code> та <code>parentNote</code>. Приклади:\n\n<ul>\nЛітературні твори <li><code>${parentNote.getLabelValue('authorName')}</code></li>\n<li><code>Журнал для ${now.format('РРРР-ММ-ДД ГГ:мм:сс')</code></li>\n</ul>\n\nДив. <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">вікі з деталями</a>, документацію API для <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> та <a href=\"https://day.js.org/docs/en/display/format\">now</a> для отримання детальної інформації.",
"template": "Ця нотатка з'явиться у списку доступних шаблонів під час створення нової нотатки",
"toc": "<code>#toc</code> або <code>#toc=show</code> примусово покаже Зміст, <code>#toc=hide</code> примусово приховає його. Якщо мітка не існує, дотримується глобального налаштування",
"color": "визначає колір нотатки в дереві нотаток, посиланнях тощо. Використовуйте будь-яке дійсне значення кольору CSS, наприклад, 'red' або #a13d5f",
"keyboard_shortcut": "Визначає комбінацію клавіш, щоб негайно перейти до цієї нотатки. Приклад: «ctrl+alt+e». Щоб зміни набули чинності, потрібне перезавантаження інтерфейсу.",
"keep_current_hoisting": "Відкриття цього посилання не змінить хостинг, навіть якщо нотатка не відображається в поточному піддереві хостінгу.",
"execute_button": "Назва кнопки, яка виконає поточну нотатку з кодом",
"execute_description": "Більш детальний опис поточного коду, що відображається разом із кнопкою виконання",
"exclude_from_note_map": "Нотатки з цією міткою будуть приховані на Карті Нотатки",
"new_notes_on_top": "Нові нотатки будуть створені зверху батьківської нотатки, а не знизу.",
"hide_highlight_widget": "Приховати віджет Списку виділення",
"run_on_note_creation": "виконується, коли нотатка створюється на серверній частині. Використовуйте цей зв'язок, якщо потрібно запустити скрипт для всіх нотаток, створених у певному піддереві. У такому випадку створіть його на батьківській нотатці піддерева та зробіть його успадковуваним. Нова нотатка, створена в піддереві (будь-якої глибини), запустить скрипт.",
"run_on_child_note_creation": "виконується, коли створюється нова нотатка під нотаткою, де визначено цей зв'язок",
"run_on_note_title_change": "виконується, коли змінюється заголовок нотатки (включає також створення нотатки)",
"run_on_note_content_change": "виконується, коли змінюється вміст нотатки (включаючи також створення нотатки).",
"run_on_note_change": "виконується, коли нотатку змінено (включає також створення нотатки). Не включає зміни вмісту",
"run_on_note_deletion": "виконується під час видалення нотатки",
"run_on_branch_creation": "виконується під час створення гілки. Гілка — це зв'язок між батьківською та дочірньою нотаткою та створюється, наприклад, під час клонування або переміщення нотатки.",
"run_on_branch_change": "виконується, коли гілка оновлюється.",
"run_on_branch_deletion": "виконується, коли гілку видаляють. Гілка — це зв'язок між батьківською та дочірньою нотатками та видаляється, наприклад, під час переміщення нотатки (стара гілка/посилання видаляється).",
"share_root": "позначає нотатку, яка подається на кореневому каталозі /share.",
"run_on_attribute_creation": "виконується, коли для нотатки створюється новий атрибут, який визначає цей зв'язок",
"run_on_attribute_change": " виконується, коли змінюється атрибут нотатки, яка визначає цей зв'язок. Це також спрацьовує, коли атрибут видаляється",
"relation_template": "атрибути нотатки будуть успадковані навіть без зв'язку \"батьківський-дочірній\", вміст нотатки та піддерево будуть додані до екземпляра нотатки, якщо він порожній. Див. документацію для отримання детальної інформації.",
"inherit": "атрибути нотатки будуть успадковані навіть без зв'язку «батьківський-дочірній». Див. шаблон зв'язку для подібної концепції. Див. успадкування атрибутів у документації.",
"render_note": "нотатки типу \"render HTML note\" будуть відображатися за допомогою нотатки з кодом (HTML або скрипта), і необхідно вказати за допомогою цього зв'язку, яку нотатку слід відображати",
"widget_relation": "ціль цього зв'язку буде виконано та відображено як віджет на бічній панелі",
"share_css": "CSS-нотатка, яка буде вставлена на сторінку спільного доступу. CSS-нотатка також має бути в спільному піддереві. Також розгляньте можливість використання 'share_hidden_from_tree' та 'share_omit_default_css'.",
"share_js": "JavaScript-нотатка, яка буде вставлена на сторінку спільного доступу. JS-нотатка також має бути в спільному піддереві. Розгляньте можливість використання 'share_hidden_from_tree'.",
"share_template": "Вбудована нотатка JavaScript, яка використовуватиметься як шаблон для відображення спільної нотатки. Повертатиметься до шаблону за замовчуванням. Розгляньте можливість використання 'share_hidden_from_tree'.",
"share_favicon": "Нотатку до значка веб-сторінки, яку потрібно встановити на спільній сторінці. Зазвичай потрібно встановити її як спільний кореневий каталог і зробити успадковуваною. Нотатку до значка веб-сторінки також потрібно розмістити у спільному піддереві. Розгляньте можливість використання 'share_hidden_from_tree'.",
"is_owned_by_note": "належить до нотатки",
"other_notes_with_name": "Інші нотатки з назвою {{attributeType}} \"{{attributeName}}\"",
"and_more": "... та ще {{count}}.",
"print_landscape": "Під час експорту в PDF змінює орієнтацію сторінки на альбомну замість портретної.",
"print_page_size": "Під час експорту в PDF змінює розмір сторінки. Підтримувані значення: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
"color_type": "Колір"
},
"multi_factor_authentication": {
"mfa_method": "Метод МФА"
},
"attribute_editor": {
"help_text_body1": "Щоб додати мітку, просто введіть, наприклад, <code>#rock</code> або, якщо ви хочете додати також значення, то, наприклад, <code>#year = 2020</code>",
"help_text_body2": "Для зв'язку введіть <code>~author = @</code>, що має викликати автозаповнення, де ви зможете знайти потрібну нотатку.",
"help_text_body3": "Або ж ви можете додати мітку та зв'язок за допомогою кнопки <code>+</code> праворуч.",
"save_attributes": "Зберегти атрибути <enter>",
"add_a_new_attribute": "Додати новий атрибут",
"add_new_label": "Додати нову мітку <kbd data-command=\"addNewLabel\"></kbd>",
"add_new_relation": "Додати новий зв'язок <kbd data-command=\"addNewRelation\"></kbd>",
"add_new_label_definition": "Додати нове визначення мітки",
"add_new_relation_definition": "Додати нове визначення зв'язку",
"placeholder": "Введіть мітки та зв'язки тут"
},
"abstract_bulk_action": {
"remove_this_search_action": "Видалити цю дію пошуку"
},
"execute_script": {
"execute_script": "Виконати скрипт",
"help_text": "Ви можете виконувати прості скрипти у нотатках, що збігаються.",
"example_1": "Наприклад, щоб додати рядок до заголовка нотатки, використовуйте цей невеликий скрипт:",
"example_2": "Більш складним прикладом може бути видалення всіх атрибутів нотаток, що збігаються:"
},
"add_label": {
"add_label": "Додати мітку",
"label_name_placeholder": "назва мітки",
"label_name_title": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку.",
"to_value": "до значення",
"new_value_placeholder": "нове значення",
"help_text": "У всіх нотатках, що збігаються:",
"help_text_item1": "створити вказану мітку, якщо нотатка ще її не має",
"help_text_item2": "або змінити значення існуючої мітки",
"help_text_note": "Ви також можете викликати цей метод без значення, у такому випадку мітку буде присвоєно нотатці без значення."
},
"delete_label": {
"delete_label": "Видалити мітку",
"label_name_placeholder": "назва мітки",
"label_name_title": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку."
},
"rename_label": {
"rename_label": "Перейменувати мітку",
"rename_label_from": "Перейменувати мітку з",
"old_name_placeholder": "стара назва",
"to": "До",
"new_name_placeholder": "нова назва",
"name_title": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку."
},
"update_label_value": {
"update_label_value": "Оновити значення мітки",
"label_name_placeholder": "назва мітки",
"label_name_title": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку.",
"to_value": "до значення",
"new_value_placeholder": "нове значення",
"help_text": "Для всіх нотаток, що збігаються, змініть значення існуючої мітки.",
"help_text_note": "Ви також можете викликати цей метод без значення, у такому випадку мітку буде присвоєно нотатці без значення."
},
"delete_note": {
"delete_note": "Видалити нотатку",
"delete_matched_notes": "Видалити нотатки, що збігаються",
"delete_matched_notes_description": "Це видалить нотатки, що збігаються.",
"undelete_notes_instruction": "Після видалення їх можна відновити в діалоговому вікні «Останні зміни».",
"erase_notes_instruction": "Щоб остаточно видалити нотатки, після видалення перейдіть до меню Опції -> Інше та натисніть кнопку «Стерти видалені нотатки зараз»."
},
"delete_revisions": {
"all_past_note_revisions": "Усі попередні версії нотаток, що збігаються, будуть видалені. Сама нотатка буде повністю збережена. Іншими словами, історія нотатки буде видалена.",
"delete_note_revisions": "Видалити версії нотаток"
},
"move_note": {
"on_all_matched_notes": "У всіх нотатках, що збігаються",
"move_note": "Перемістити нотатку",
"to": "до",
"target_parent_note": "цільова батьківська нотатка",
"move_note_new_parent": "перемістити нотатку до нового батьківського елемента, якщо нотатка має лише один батьківський елемент (тобто стара гілка видаляється, а нова гілка в новому батьківському елементі створюється)",
"clone_note_new_parent": "клонувати нотатку до нового батьківського елемента, якщо нотатка має кілька клонів/гілок (незрозуміло, яку гілку слід видалити)",
"nothing_will_happen": "нічого не станеться, якщо нотатку не можна перемістити до цільової нотатки (тобто це створить деревоподібний цикл)"
},
"rename_note": {
"example_note": "<code>Нотатка</code> усі нотатки, що збігаються будуть перейменовані на \"Нотатка\"",
"example_new_title": "<code>NEW: ${note.title}</code> назви нотаток, що збігаються, мають префікс 'НОВА:'",
"example_date_prefix": "<code>${note.dateCreatedObj.format('MM-DD:')}: ${note. Title}</code> - нотатки, що збігаються мають префікс у вигляді місяця та дати створення нотатки",
"rename_note": "Перейменувати нотатку",
"rename_note_title_to": "Перейменувати заголовок нотатки на",
"new_note_title": "новий заголовок нотатки",
"click_help_icon": "Натисніть значок довідки праворуч, щоб переглянути всі опції",
"evaluated_as_js_string": "Надане значення обчислюється як рядок JavaScript і тому може бути доповнено динамічним контентом за допомогою вставленої змінної <code>note</code> (нотатка перейменовується). Приклади:",
"api_docs": "Див. документацію API для <a href='https://zadam.github.io/trilium/backend_api/Note.html'>note</a> та її <a href='https://day.js.org/docs/en/display/format'>властивостей dateCreatedObj / utcDateCreatedObj</a> для отримання детальної інформації."
},
"add_relation": {
"create_relation_on_all_matched_notes": "Для всіх нотаток, що збігаються, створити задані зв'язки.",
"add_relation": "Додати зв'язок",
"relation_name": "назва зв'язку",
"allowed_characters": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку.",
"to": "до",
"target_note": "цільова нотатка"
},
"update_relation_target": {
"on_all_matched_notes": "У всіх нотатках, що збігаються",
"update_relation": "Оновити зв'язок",
"relation_name": "назва зв'язку",
"allowed_characters": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку.",
"to": "до",
"target_note": "цільова нотатка",
"change_target_note": "змінити цільову нотатку існуючого зв'язку",
"update_relation_target": "Оновити ціль зв'язку"
},
"search_script": {
"example_code": "// 1. попередня фільтрація за допомогою стандартного пошуку\nconst candidateNotes = api.searchForNotes(\"#journal\");\n\n// 2. застосування користувацьких критеріїв пошуку\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;"
},
"delete_relation": {
"delete_relation": "Видалити зв'язок",
"relation_name": "назва зв'язку",
"allowed_characters": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку."
},
"rename_relation": {
"rename_relation": "Перейменувати зв'язок",
"rename_relation_from": "Перейменувати зв'язок з",
"old_name": "стара назва",
"to": "До",
"new_name": "нова назва",
"allowed_characters": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку."
},
"attachments_actions": {
"open_externally": "Відкрити у зовнішній програмі",
"open_externally_title": "Файл буде відкрито в зовнішній програмі та відстежуватися на наявність змін. Після цього ви зможете завантажити змінену версію назад до Trilium.",
"open_custom": "Відкрити користувацький",
"open_custom_title": "Файл буде відкрито в зовнішній програмі та відстежуватися на наявність змін. Після цього ви зможете завантажити змінену версію назад до Trilium.",
"download": "Завантажити",
"rename_attachment": "Перейменувати вкладення",
"upload_new_revision": "Завантажити нову версію",
"copy_link_to_clipboard": "Копіювати посилання в буфер обміну",
"convert_attachment_into_note": "Перетворити вкладення на нотатку",
"delete_attachment": "Видалити вкладення",
"upload_success": "Нову версію вкладеного файлу завантажено.",
"upload_failed": "Не вдалося завантажити нову версію вкладеного файлу.",
"open_externally_detail_page": "Відкриття вкладення ззовні доступне лише зі сторінки з деталями, спочатку натисніть на деталі вкладення та повторіть дію.",
"open_custom_client_only": "Налаштування відкриття вкладень можна виконати лише з клієнтської версії для ПК.",
"delete_confirm": "Ви впевнені, що хочете видалити вкладення '{{title}}'?",
"delete_success": "Вкладення '{{title}}' видалено.",
"convert_confirm": "Ви впевнені, що хочете перетворити вкладення '{{title}}' на окрему нотатку?",
"convert_success": "Вкладення '{{title}}' перетворено на нотатку.",
"enter_new_name": "Будь ласка, введіть назву нового вкладення"
},
"calendar": {
"mon": "Пн",
"tue": "Вт",
"wed": "Ср",
"thu": "Чт",
"fri": "Пт",
"sat": "Сб",
"sun": "Нд",
"cannot_find_day_note": "Не вдається знайти денну нотатку",
"cannot_find_week_note": "Не вдається знайти нотатку тижня",
"january": "Січень",
"febuary": "Лютий",
"march": "Березень",
"april": "Квітень",
"may": "Травень",
"june": "Червень",
"july": "Липень",
"august": "Серпень",
"september": "Вересень",
"october": "Жовтень",
"november": "Листопад",
"december": "Грудень"
},
"close_pane_button": {
"close_this_pane": "Закрити цю панель"
},
"create_pane_button": {
"create_new_split": "Створити новий поділ"
},
"edit_button": {
"edit_this_note": "Редагувати цю нотатку"
},
"show_toc_widget_button": {
"show_toc": "Показати зміст"
},
"show_highlights_list_widget_button": {
"show_highlights_list": "Показати Список основних моментів"
},
"zen_mode": {
"button_exit": "Вихід з Дзен-режиму"
},
"sync_status": {
"unknown": "<p>Стан синхронізації буде відомий після початку наступної спроби синхронізації.</p><p>Натисніть, щоб запустити синхронізацію зараз.</p>",
"connected_with_changes": "<p>Підключено до сервера синхронізації. <br>Є деякі невиконані зміни, які ще потрібно синхронізувати.</p><p>Натисніть, щоб розпочати синхронізацію.</p>",
"connected_no_changes": "<p>Підключено до сервера синхронізації.<br>Усі зміни вже синхронізовано.</p><p>Натисніть, щоб розпочати синхронізацію.</p>",
"disconnected_with_changes": "<p>Встановлення з’єднання із сервером синхронізації не вдалося.<br>Є деякі невиконані зміни, які ще потрібно синхронізувати.</p><p>Натисніть, щоб розпочати синхронізацію.</p>",
"disconnected_no_changes": "<p>Встановлення з’єднання із сервером синхронізації не вдалося.<br>Усі відомі зміни синхронізовано.</p><p>Натисніть, щоб розпочати синхронізацію.</p>",
"in_progress": "Триває синхронізація із сервером."
},
"left_pane_toggle": {
"show_panel": "Показати панель",
"hide_panel": "Приховати панель"
},
"move_pane_button": {
"move_left": "Переміститися вліво",
"move_right": "Переміститися вправо"
},
"note_actions": {
"convert_into_attachment": "Перетворити на вкладення",
"re_render_note": "Повторно відобразити нотатку",
"search_in_note": "Пошук у нотатці",
"note_source": "Джерело нотатки",
"note_attachments": "Вкладення нотатки",
"open_note_externally": "Відкрити нотатку у зовнішній програмі",
"open_note_externally_title": "Файл буде відкрито в зовнішній програмі та відстежуватися на наявність змін. Після цього ви зможете завантажити змінену версію назад до Trilium.",
"open_note_custom": "Відкрити нотатку користувача",
"import_files": "Імпорт файлів",
"export_note": "Експорт нотатки",
"delete_note": "Видалити нотатку",
"print_note": "Друк нотатки",
"save_revision": "Зберегти версію",
"convert_into_attachment_failed": "Не вдалося конвертувати нотатку '{{title}}'.",
"convert_into_attachment_successful": "Нотатку '{{title}}' перетворено на вкладення.",
"convert_into_attachment_prompt": "Ви впевнені, що хочете перетворити нотатку '{{title}}' на вкладення батьківської нотатки?",
"print_pdf": "Експортувати як PDF..."
},
"onclick_button": {
"no_click_handler": "Віджет кнопки '{{componentId}}' не має визначеного обробника кліків"
},
"protected_session_status": {
"active": "Захищений сеанс активний. Натисніть, щоб вийти з захищеного сеансу.",
"inactive": "Натисніть, щоб увійти до захищеного сеансу"
},
"revisions_button": {
"note_revisions": "Версії нотатки"
"note_revisions": "Зміни нотаток",
"delete_all_revisions": "Видалити всі редакції цієї нотатки"
}
}

View File

@@ -74,7 +74,7 @@
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"i18next": "25.4.0",
"i18next": "25.3.6",
"i18next-fs-backend": "2.6.0",
"image-type": "6.0.0",
"ini": "5.0.0",

View File

@@ -100,82 +100,9 @@ export default async function buildApp() {
app.use(sessionParser);
app.use(favicon(path.join(assetsDir, "icon.ico")));
if (openID.isOpenIDEnabled()) {
// Always log OAuth initialization for better debugging
log.info('OAuth: Initializing OAuth authentication middleware');
// Check for potential reverse proxy configuration issues
const baseUrl = config.MultiFactorAuthentication.oauthBaseUrl;
const trustProxy = app.get('trust proxy');
log.info(`OAuth: Configuration check - baseURL=${baseUrl}, trustProxy=${trustProxy}`);
// Log potential issue if OAuth is configured with HTTPS but trust proxy is not set
if (baseUrl.startsWith('https://') && !trustProxy) {
log.info('OAuth: baseURL uses HTTPS but trustedReverseProxy is not configured.');
log.info('OAuth: If you are behind a reverse proxy, this MAY cause authentication failures.');
log.info('OAuth: The OAuth library might generate HTTP redirect_uris instead of HTTPS.');
log.info('OAuth: If authentication fails with redirect_uri errors, try setting:');
log.info('OAuth: In config.ini: trustedReverseProxy=true');
log.info('OAuth: Or environment: TRILIUM_NETWORK_TRUSTEDREVERSEPROXY=true');
log.info('OAuth: Note: This is only needed if running behind a reverse proxy.');
}
// Test OAuth connectivity on startup for non-Google providers
const issuerUrl = config.MultiFactorAuthentication.oauthIssuerBaseUrl;
const isCustomProvider = issuerUrl &&
issuerUrl !== "" &&
issuerUrl !== "https://accounts.google.com";
if (isCustomProvider) {
// For non-Google providers, verify connectivity
openID.testOAuthConnectivity().then(result => {
if (result.success) {
log.info('OAuth: Provider connectivity verified successfully');
} else {
log.error(`OAuth: Provider connectivity check failed: ${result.error}`);
log.error('OAuth: Authentication may not work. Please verify:');
log.error(' 1. The OAuth provider URL is correct');
log.error(' 2. Network connectivity between Trilium and the OAuth provider');
log.error(' 3. Any firewall or proxy settings');
}
}).catch(err => {
log.error(`OAuth: Connectivity test error: ${err.message || err}`);
});
}
// Register OAuth middleware
if (openID.isOpenIDEnabled())
app.use(auth(openID.generateOAuthConfig()));
// Add OAuth error logging middleware AFTER auth middleware
app.use(openID.oauthErrorLogger);
// Add diagnostic middleware for authentication initiation
app.use('/authenticate', (req, res, next) => {
log.info(`OAuth authenticate diagnostic: protocol=${req.protocol}, secure=${req.secure}, host=${req.get('host')}`);
log.info(`OAuth authenticate: baseURL from req = ${req.protocol}://${req.get('host')}`);
log.info(`OAuth authenticate: headers - x-forwarded-proto=${req.headers['x-forwarded-proto']}, x-forwarded-host=${req.headers['x-forwarded-host']}`);
// The actual redirect_uri will be logged by express-openid-connect
next();
});
// Add diagnostic middleware to log what protocol Express thinks it's using for callbacks
app.use('/callback', (req, res, next) => {
log.info(`OAuth callback diagnostic: protocol=${req.protocol}, secure=${req.secure}, originalUrl=${req.originalUrl}`);
log.info(`OAuth callback headers: x-forwarded-proto=${req.headers['x-forwarded-proto']}, x-forwarded-for=${req.headers['x-forwarded-for']}, host=${req.headers['host']}`);
// Log if there's a mismatch between expected and actual protocol
const expectedProtocol = baseUrl.startsWith('https://') ? 'https' : 'http';
if (req.protocol !== expectedProtocol) {
log.error(`OAuth callback: PROTOCOL MISMATCH DETECTED!`);
log.error(`OAuth callback: Expected ${expectedProtocol} (from baseURL) but got ${req.protocol}`);
log.error(`OAuth callback: This indicates trustedReverseProxy may need to be set.`);
}
next();
});
}
await assets.register(app);
routes.register(app);
custom.register(app);

View File

@@ -300,39 +300,6 @@
"create-note-into-inbox": "Neue Notiz in Inbox erstellen",
"delete-notes": "Notizen löschen",
"move-note-up": "Notiz nach oben verschieben",
"move-note-down": "Notiz nach unten verschieben",
"edit-note-title": "Bearbeite Notiz Titel",
"clone-notes-to": "Vervielfältige Notiz nach",
"move-notes-to": "Verschiebe Notiz nach",
"cut-notes-to-clipboard": "Notizen in Zwischenablage ausschneiden",
"add-note-above-to-selection": "Notiz oberhalb der Selektion hinzufügen",
"add-note-below-to-selection": "Notiz unterhalb der Selektion hinzufügen",
"open-new-tab": "Öffne im neuen Tab",
"close-active-tab": "Schließe aktiven Tab",
"reopen-last-tab": "Öffne zuletzt geschlossenen Tab",
"activate-next-tab": "Aktiviere nächsten Tab",
"activate-previous-tab": "Aktiviere vorherigen Tab",
"open-new-window": "Öffne im neuen Fenster",
"toggle-system-tray-icon": "Systemablage-Symbol umschalten",
"toggle-zen-mode": "Zen-Modus umschalten",
"switch-to-first-tab": "Wechsle zum ersten Tab",
"switch-to-second-tab": "Wechsle zum zweiten Tab",
"switch-to-third-tab": "Wechsle zum dritten Tab",
"switch-to-fourth-tab": "Wechsle zum vierten Tab",
"switch-to-fifth-tab": "Wechsle zum fünften Tab",
"switch-to-sixth-tab": "Wechsle zum sechsten Tab",
"switch-to-seventh-tab": "Wechsle zum siebten Tab",
"switch-to-eighth-tab": "Wechsle zum achten Tab",
"switch-to-ninth-tab": "Wechsle zum neunten Tab",
"switch-to-last-tab": "Wechsle zum letzten Tab",
"show-note-source": "Zeige Notiz Quelle",
"show-options": "Zeige Optionen",
"show-revisions": "Zeige Revisionen",
"show-recent-changes": "Zeige letzte Änderungen",
"show-sql-console": "Zeige SQL Konsole",
"show-backend-log": "Zeige Backend-Protokoll",
"show-help": "Zeige Hilfe",
"show-cheatsheet": "Zeige Cheatsheet",
"add-link-to-text": "Link zum Text hinzufügen"
"move-note-down": "Notiz nach unten verschieben"
}
}

View File

@@ -2,9 +2,9 @@
"keyboard_actions": {
"back-in-note-history": "Перейти до попередньої нотатки в історії",
"forward-in-note-history": "Перейти до наступної нотатки в історії",
"open-command-palette": "Відкрити палітру команд",
"open-command-palette": "Відкрити панель команд",
"scroll-to-active-note": "Прокрутити дерево нотаток до активної нотатки",
"quick-search": "Активувати панель швидкого пошуку",
"quick-search": "Показати панель швидкого пошуку",
"search-in-subtree": "Пошук нотаток в піддереві активної нотатки",
"expand-subtree": "Розкрити піддерево поточної нотатки",
"collapse-tree": "Згорнути все дерево нотаток",
@@ -13,8 +13,8 @@
"sort-child-notes": "Сортувати дочірні нотатки",
"creating-and-moving-notes": "Створення та переміщення нотаток",
"create-note-after": "Створити нотатку після активної нотатки",
"create-note-into": "Створити нотатку як дочірню до активної нотатки",
"create-note-into-inbox": "Створити нотатку у \"Вхідні\" (якщо визначено) або в щоденнику",
"create-note-into": "Створити нотатку як дочірній елемент активної нотатки",
"create-note-into-inbox": "Створити нотатку у «Вхідні» (якщо визначено) або в щоденнику",
"delete-note": "Видалити нотатку",
"move-note-up": "Перемістити нотатку вгору",
"move-note-down": "Перемістити нотатку вниз",
@@ -24,18 +24,18 @@
"edit-branch-prefix": "Показати вікно \"Редагувати префікс гілки\"",
"clone-notes-to": "Клонувати вибрані нотатки",
"move-notes-to": "Перемістити вибрані нотатки",
"note-clipboard": "Буфер обміну нотатки",
"note-clipboard": "Буфер обміну нотаток",
"copy-notes-to-clipboard": "Копіювати вибрані нотатки в буфер обміну",
"paste-notes-from-clipboard": "Вставити нотатки з буферу обміну в активну нотатку",
"paste-notes-from-clipboard": "Вставити нотатки з буфера обміну в активну нотатку",
"cut-notes-to-clipboard": "Вирізати вибрані нотатки в буфер обміну",
"select-all-notes-in-parent": "Вибрати всі нотатки з поточного рівня нотаток",
"add-note-above-to-the-selection": "Додати нотатку вище до виділення",
"add-note-below-to-selection": "Додати нотатку нижче до вибраного",
"duplicate-subtree": "Дублювання піддерева",
"duplicate-subtree": "Дублікат гілок дерева",
"tabs-and-windows": "Вкладки & Вікна",
"open-new-tab": "Відкрити нову вкладку",
"close-active-tab": "Закрити активну вкладку",
"reopen-last-tab": "Відкрити останню закриту вкладку",
"reopen-last-tab": "Знову відкрити останню закриту вкладку",
"activate-next-tab": "Активувати вкладку праворуч",
"activate-previous-tab": "Активувати вкладку ліворуч",
"open-new-window": "Відкрити нове порожнє вікно",
@@ -51,10 +51,10 @@
"ninth-tab": "Активувати дев'яту вкладку у списку",
"last-tab": "Активувати останню вкладку у списку",
"dialogs": "Діалоги",
"show-note-source": "Показати \"Джерело нотатки\"",
"show-note-source": "Показати вікно «Джерело нотатки»",
"show-options": "Відкрити \"Параметри\"",
"show-revisions": "Показати \"Версії нотаток\"",
"show-recent-changes": "Показати \"Останні зміни\"",
"show-revisions": "Показати вікно \"Зміни нотаток\"",
"show-recent-changes": "Показати вікно \"Останні зміни\"",
"show-sql-console": "Відкрити \"Консоль SQL\"",
"show-backend-log": "Відкрити \"Backend Log\"",
"show-help": "Відкрити вбудований Посібник користувача",
@@ -62,33 +62,33 @@
"text-note-operations": "Дії з текстовими нотатками",
"add-link-to-text": "Відкрити діалогове вікно для додавання посилання до тексту",
"follow-link-under-cursor": "Перейдіть за посиланням, на якому знаходиться курсор",
"insert-date-and-time-to-text": "Вставити поточну дату & час у текст",
"insert-date-and-time-to-text": "Вставити поточну дату та час у текст",
"paste-markdown-into-text": "Вставити Markdown з буфера обміну в текстову нотатку",
"cut-into-note": "Вирізати виділений фрагмент із поточної нотатки та створити піднотатку з виділеним текстом",
"add-include-note-to-text": "Відкрити вікно для додавання нотатки",
"add-include-note-to-text": "Відкрити вікно для додавання примітки",
"edit-readonly-note": "Редагувати нотатку, доступну тільки для читання",
"attributes-labels-and-relations": "Атрибути (мітки & зв'язки)",
"attributes-labels-and-relations": "Атрибути (мітки та зв'язки)",
"add-new-label": "Створити нову мітку",
"create-new-relation": "Створити новий зв'язок",
"ribbon-tabs": "Вкладки стрічки",
"toggle-basic-properties": "Увімкнути Основні Властивості",
"toggle-file-properties": "Увімкнути Властивості Файлу",
"toggle-image-properties": "Увімкнути Властивості Зображення",
"toggle-owned-attributes": "Увімкнути Власні Атрибути",
"toggle-inherited-attributes": "Увімкнути Успадковані Атрибути",
"toggle-promoted-attributes": "Увімкнути Просунуті Атрибути",
"toggle-link-map": "Увімкнути Карта Посилань",
"toggle-note-info": "Увімкнути Інформація про Нотатку",
"toggle-note-paths": "Увімкнути Шляхи Нотатки",
"toggle-similar-notes": "Увімкнути Схожі Нотатки",
"toggle-basic-properties": "Увімкнути Основні властивості",
"toggle-file-properties": "Увімкнути Властивості файлу",
"toggle-image-properties": "Увімкнути Властивості зображення",
"toggle-owned-attributes": "Увімкнути Власні атрибути",
"toggle-inherited-attributes": "Увімкнути Успадковані атрибути",
"toggle-promoted-attributes": "Увімкнути Рекламні атрибути",
"toggle-link-map": "Увімкнути Карта посилань",
"toggle-note-info": "Увімкнути Інформація про нотатку",
"toggle-note-paths": "Увімкнути Шляхі нотатки",
"toggle-similar-notes": "Увімкнути Схожі нотатки",
"other": "Інше",
"toggle-right-pane": "Увімкнути відображення правої панелі, яка містить Зміст та Основні моменти",
"print-active-note": "Друк активної нотатки",
"open-note-externally": "Відкрити нотатку як файл у програмі за замовчуванням",
"render-active-note": "Відтворити (повторно відтворити) активну нотатку",
"run-active-note": "Виконати активний код JavaScript (frontend/backend) нотатки з кодом",
"toggle-note-hoisting": "Увімкнути хостинг активної нотатки",
"unhoist": "Зняти з будь-якого місця",
"run-active-note": "Виконати активний код JavaScript (фронтенд/бекенд) нотатки",
"toggle-note-hoisting": "Увімкнути Хостинг активної нотатки",
"unhoist": "Зніміть з будь-якого місця",
"reload-frontend-app": "Перезавантажити інтерфейс",
"open-dev-tools": "Відкрити інструменти розробника",
"find-in-text": "Увімкнути панель пошуку",
@@ -97,53 +97,53 @@
"zoom-out": "Зменшити масштаб",
"zoom-in": "Збільшити масштаб",
"note-navigation": "Навігація по нотатках",
"reset-zoom-level": "Скинути масштабування",
"reset-zoom-level": "Скинути рівень масштабування",
"copy-without-formatting": "Копіювати виділений текст без форматування",
"force-save-revision": "Примусове створення/збереження нової версії активної нотатки",
"toggle-book-properties": "Увімкнути Властивості Колекції",
"toggle-book-properties": "Увімкнути Властивості колекції",
"toggle-classic-editor-toolbar": "Увімкнути вкладку Форматування для редактора з фіксованою панеллю інструментів",
"export-as-pdf": "Експортувати поточну нотатку в PDF",
"toggle-zen-mode": "Вмикає/вимикає Дзен-режим (мінімальний інтерфейс для більш сфокусованого редагування)"
"export-as-pdf": "Експортувати поточну нотатку у PDF",
"toggle-zen-mode": "Вмикає/вимикає дзен-режим (мінімальний інтерфейс для більш цілеспрямованого редагування)"
},
"keyboard_action_names": {
"back-in-note-history": "Назад до Історії Нотаток",
"forward-in-note-history": "Вперед в Історія Нотаток",
"back-in-note-history": "Назад до Історії нотаток",
"forward-in-note-history": "Вперед в Історії нотаток",
"jump-to-note": "Перейти до...",
"command-palette": "Палітра Команд",
"scroll-to-active-note": "Прокрутити до Активної Нотатки",
"quick-search": "Швидкий Пошук",
"search-in-subtree": "Пошук у Піддереві",
"expand-subtree": "Розгорнути Піддерево",
"collapse-tree": "Згорнути Дерево",
"collapse-subtree": "Згорнути Піддерево",
"sort-child-notes": "Сортувати Дочірні Нотатки",
"create-note-after": "Створити Нотатку після",
"create-note-into": "Створити Нотатку в",
"create-note-into-inbox": "Створити Нотатку у \"Вхідні\"",
"delete-notes": "Видалити Нотатки",
"move-note-up": "Перемістити Нотатку вгору",
"move-note-down": "Перемістити Нотатку вниз",
"move-note-up-in-hierarchy": "Перемістити Нотатку вгору в ієрархії",
"move-note-down-in-hierarchy": "Перемістити Нотатку вниз в ієрархії",
"edit-note-title": "Редагувати Заголовок Нотатки",
"edit-branch-prefix": "Редагувати Префікс Гілки",
"clone-notes-to": "Клонувати Нотатки до",
"move-notes-to": "Перемістити Нотатки до",
"copy-notes-to-clipboard": "Копіювати Нотатки в Буфер обміну",
"paste-notes-from-clipboard": "Вставити Нотатки з Буфера обміну",
"cut-notes-to-clipboard": "Вирізати Нотатки в Буфер обміну",
"select-all-notes-in-parent": "Вибрати всі Нотатки в \"Батьківські\"",
"add-note-above-to-selection": "Додати Нотатку вище до виділення",
"add-note-below-to-selection": "Додати Нотатку нижче до виділення",
"duplicate-subtree": "Дублікат Піддерева",
"open-new-tab": "Відкрити Нову Вкладку",
"close-active-tab": "Закрити Активну Вкладку",
"reopen-last-tab": "Відкрити Останню Вкладку",
"activate-next-tab": "Активувати Наступну Вкладку",
"activate-previous-tab": "Активувати Попередню Вкладку",
"open-new-window": "Відкрити Нове Вікно",
"command-palette": "Палітра команд",
"scroll-to-active-note": "Прокрутити до Активної нотатки",
"quick-search": "Швидкий пошук",
"search-in-subtree": "Пошук у піддереві",
"expand-subtree": "Розгорнути піддерево",
"collapse-tree": "Згорнути дерево",
"collapse-subtree": "Згорнути піддерево",
"sort-child-notes": "Сортувати дочірні нотатки",
"create-note-after": "Створити нотатку після",
"create-note-into": "Створити нотатку в",
"create-note-into-inbox": "Створити нотатку у \"Вхідні\"",
"delete-notes": "Видалити нотатки",
"move-note-up": "Перемістити нотатку вгору",
"move-note-down": "Перемістити нотатку вниз",
"move-note-up-in-hierarchy": "Перемістити нотатку вгору в ієрархії",
"move-note-down-in-hierarchy": "Перемістити нотатку вниз в ієрархії",
"edit-note-title": "Редагувати назву нотатки",
"edit-branch-prefix": "Редагувати префікс гілки",
"clone-notes-to": "Клонувати нотатки до",
"move-notes-to": "Перемістити нотатки до",
"copy-notes-to-clipboard": "Копіювати нотатки в буфер обміну",
"paste-notes-from-clipboard": "Вставити нотатки з буфера обміну",
"cut-notes-to-clipboard": "Вирізати нотатки в буфер обміну",
"select-all-notes-in-parent": "Вибрати всі нотатки в Батьківські",
"add-note-above-to-selection": "Додати примітку вище до виділення",
"add-note-below-to-selection": "Додати нотатку нижче до виділення",
"duplicate-subtree": "Дублікат піддерева",
"open-new-tab": "Відкрити нову вкладку",
"close-active-tab": "Закрити активну вкладку",
"reopen-last-tab": "Відкрити останню вкладку",
"activate-next-tab": "Активувати наступну вкладку",
"activate-previous-tab": "Активувати попередню вкладку",
"open-new-window": "Відкрити нове вікно",
"toggle-system-tray-icon": "Увімкнути Значок системного трея",
"toggle-zen-mode": "Увімкнути Дзен-режим",
"toggle-zen-mode": "Увімкнути режим дзен",
"switch-to-first-tab": "Перейти до першої вкладки",
"switch-to-second-tab": "Перейти до другої вкладки",
"switch-to-third-tab": "Перейти до третьої вкладки",
@@ -153,53 +153,13 @@
"switch-to-seventh-tab": "Перейти до сьомої вкладки",
"switch-to-eighth-tab": "Перейти до восьмої вкладки",
"find-in-text": "Знайти в тексті",
"toggle-left-pane": "Увімкнути ліву панель",
"toggle-full-screen": "Увімкнути повноекранний режим",
"toggle-left-pane": "Перемкнути ліву панель",
"toggle-full-screen": "Перемкнути повноекранний режим",
"zoom-out": "Зменшити масштаб",
"zoom-in": "Збільшити масштаб",
"reset-zoom-level": "Скинути масштабування",
"reset-zoom-level": "Скинути рівень масштабування",
"copy-without-formatting": "Копіювати без форматування",
"force-save-revision": "Примусове збереження версії",
"switch-to-ninth-tab": "Перейти на дев'яту вкладку",
"switch-to-last-tab": "Перейти до останньої вкладки",
"show-note-source": "Показати Джерело Нотатки",
"show-options": "Показати Параметри",
"show-revisions": "Показати Версії",
"show-recent-changes": "Показати Останні Зміни",
"show-sql-console": "Показати консоль SQL",
"show-backend-log": "Показати Backend Log",
"show-help": "Показати Довідку",
"show-cheatsheet": "Показати Шпаргалку",
"add-link-to-text": "Додати посилання до тексту",
"follow-link-under-cursor": "Перейти за посиланням під курсором",
"insert-date-and-time-to-text": "Вставити Дату та Час у текст",
"paste-markdown-into-text": "Вставити Markdown у текст",
"cut-into-note": "Вирізати у Нотатку",
"add-include-note-to-text": "Додати включену Нотатку до тексту",
"edit-read-only-note": "Редагувати нотатку лише для читання",
"add-new-label": "Додати Нову Мітку",
"add-new-relation": "Додати Новий Зв'язок",
"toggle-ribbon-tab-classic-editor": "Включити вкладку стрічки \"Класичний Редактор\"",
"toggle-ribbon-tab-basic-properties": "Включити вкладку стрічки \"Основні властивості\"",
"toggle-ribbon-tab-book-properties": "Включити вкладку стрічки \"Властивості Книги\"",
"toggle-ribbon-tab-file-properties": "Включити вкладку стрічки \"Властивості файлу\"",
"toggle-ribbon-tab-image-properties": "Включити вкладку стрічки \"Властивості Зображення\"",
"toggle-ribbon-tab-owned-attributes": "Включити вкладку стрічки \"Власні атрибути\"",
"toggle-ribbon-tab-inherited-attributes": "Включити вкладку стрічки \"Успадковані Атрибути\"",
"toggle-ribbon-tab-promoted-attributes": "Включити вкладку стрічки \"Просунуті Атрибути\"",
"toggle-ribbon-tab-note-map": "Включити вкладку стрічки \"Карта Нотатки\"",
"toggle-ribbon-tab-note-info": "Включити вкладку стрічки \"Інформація про Нотатку\"",
"toggle-ribbon-tab-note-paths": "Включити вкладку стрічки \"Шляхи Нотатки\"",
"toggle-ribbon-tab-similar-notes": "Включити вкладку стрічки \"Схожі Нотатки\"",
"toggle-right-pane": "Включити праву панель",
"print-active-note": "Друк Активної Нотатки",
"export-active-note-as-pdf": "Експорт активної нотатки у PDF",
"open-note-externally": "Відкрити Нотатку зовнішньою програмою",
"render-active-note": "Відтворити Активну Нотатку",
"run-active-note": "Запустити Активну Нотатку",
"toggle-note-hoisting": "Включити хостинг нотатки",
"reload-frontend-app": "Перезавантажити інтерфейс програми",
"open-developer-tools": "Відкрити Інструменти розробника"
"force-save-revision": "Примусове збереження версії"
},
"login": {
"title": "Увійти",
@@ -212,215 +172,6 @@
"sign_in_with_sso": "Увійти за допомогою {{ ssoIssuerName }}"
},
"set_password": {
"title": "Встановити пароль",
"heading": "Встановити пароль",
"description": "Перш ніж почати користуватися Trilium з веб-сайту, вам потрібно спочатку встановити пароль. Потім ви будете використовувати цей пароль для входу в систему.",
"password": "Пароль",
"password-confirmation": "Підтвердження пароля",
"button": "Встановити пароль"
},
"javascript-required": "Для роботи Trilium потрібен JavaScript.",
"setup": {
"heading": "Налаштування Trilium Notes",
"new-document": "Я новий користувач і хочу створити новий документ Trilium для своїх нотаток",
"sync-from-desktop": "У мене вже є екземпляр для ПК і я хочу налаштувати синхронізацію з ним",
"sync-from-server": "У мене вже є екземпляр сервера, і я хочу налаштувати синхронізацію з ним",
"next": "Наступна",
"init-in-progress": "Триває ініціалізація документа",
"redirecting": "Невдовзі вас буде перенаправлено до програми.",
"title": "Налаштування"
},
"setup_sync-from-desktop": {
"heading": "Синхронізація з ПК",
"description": "Це налаштування потрібно ініціювати з екземпляра ПК:",
"step1": "Відкрийте екземпляр Trilium Notes на ПК.",
"step2": "У меню Trilium натисніть Параметри.",
"step3": "Натисніть на Категорія Синхронізації.",
"step4": "Змініть адресу екземпляра сервера на: {{- host}} та натисніть кнопку Зберегти.",
"step5": "Натисніть \"Тест синхронізації\", щоб перевірити успішність підключення.",
"step6": "Після виконання цих кроків натисніть {{- link}}.",
"step6-here": "тут"
},
"setup_sync-from-server": {
"heading": "Синхронізація з сервера",
"instructions": "Будь ласка, введіть адресу сервера Trilium та облікові дані нижче. Це завантажить весь документ Trilium із сервера та налаштує синхронізацію з ним. Залежно від розміру документа та швидкості вашого з’єднання, це може зайняти деякий час.",
"server-host": "Адреса сервера Trilium",
"server-host-placeholder": "https://<hostname>:<port>",
"proxy-server": "Проксі-сервер (необов'язково)",
"proxy-server-placeholder": "https://<hostname>:<port>",
"note": "Нотатка:",
"proxy-instruction": "Якщо залишити налаштування проксі-сервера порожнім, використовуватиметься системний проксі-сервер (стосується лише ПК програми)",
"password": "Пароль",
"password-placeholder": "Пароль",
"back": "Назад",
"finish-setup": "Завершити налаштування"
},
"setup_sync-in-progress": {
"heading": "Триває синхронізація",
"successful": "Синхронізацію налаштовано правильно. Початкова синхронізація завершиться через деякий час. Після її завершення вас буде перенаправлено на сторінку входу.",
"outstanding-items": "Незавершені елементи синхронізації:",
"outstanding-items-default": "Недоступно"
},
"share_404": {
"title": "Не знайдено",
"heading": "Не знайдено"
},
"share_page": {
"parent": "батьківська:",
"clipped-from": "Цю нотатку було спочатку вирізано з {{- url}}",
"child-notes": "Дочірні нотатки:",
"no-content": "Ця нотатка не має вмісту."
},
"weekdays": {
"monday": "Понеділок",
"tuesday": "Вівторок",
"wednesday": "Середа",
"thursday": "Четвер",
"friday": "П'ятниця",
"saturday": "Субота",
"sunday": "Неділя"
},
"weekdayNumber": "Тиждень {weekNumber}",
"months": {
"january": "Січень",
"february": "Лютий",
"march": "Березень",
"april": "Квітень",
"may": "Травень",
"june": "Червень",
"july": "Липень",
"august": "Серпень",
"september": "Вересень",
"october": "Жовтень",
"november": "Листопад",
"december": "Грудень"
},
"quarterNumber": "Квартал {quarterNumber}",
"special_notes": {
"search_prefix": "Пошук:"
},
"test_sync": {
"not-configured": "Хост сервера синхронізації не налаштовано. Спочатку налаштуйте синхронізацію.",
"successful": "Установлення зв'язку з сервером синхронізації пройшло успішно, синхронізація розпочалася."
},
"hidden-subtree": {
"root-title": "Приховані Нотатки",
"search-history-title": "Історія Пошуку",
"note-map-title": "Карта Нотатки",
"sql-console-history-title": "Історія консолі SQL",
"shared-notes-title": "Спільні Нотатки",
"bulk-action-title": "Масова дія",
"backend-log-title": "Backend Log",
"user-hidden-title": "Прихований користувач",
"launch-bar-templates-title": "Шаблони Панелі запуску",
"base-abstract-launcher-title": "Базовий Абстрактний лаунчер",
"command-launcher-title": "Командний лаунчер",
"note-launcher-title": "Лаунчер нотаток",
"script-launcher-title": "Запуск скриптів",
"built-in-widget-title": "Вбудований віджет",
"custom-widget-title": "Користувацький віджет",
"launch-bar-title": "Панель запуску",
"available-launchers-title": "Доступні лаунчери",
"go-to-previous-note-title": "Перейти до попередньої нотатки",
"go-to-next-note-title": "Перейти до наступної нотатки",
"new-note-title": "Нова нотатка",
"search-notes-title": "Пошук нотаток",
"jump-to-note-title": "Перейти до...",
"calendar-title": "Календар",
"recent-changes-title": "Останні зміни",
"bookmarks-title": "Закладки",
"open-today-journal-note-title": "Відкрити Щоденник за сьогодні",
"quick-search-title": "Швидкий пошук",
"protected-session-title": "Захищений сеанс",
"sync-status-title": "Статус синхронізації",
"settings-title": "Налаштування",
"llm-chat-title": "Чат з Нотатками",
"options-title": "Параметри",
"appearance-title": "Зовнішній вигляд",
"shortcuts-title": "Швидкі клавіші",
"text-notes": "Текстові Нотатки",
"code-notes-title": "Нотатка з кодом",
"images-title": "Зображення",
"spellcheck-title": "Перевірка орфографії",
"password-title": "Пароль",
"multi-factor-authentication-title": "МФА",
"etapi-title": "ETAPI",
"backup-title": "Резервне копіювання",
"sync-title": "Синхронізація",
"ai-llm-title": "AI/LLM",
"other": "Інше",
"advanced-title": "Розширені",
"visible-launchers-title": "Видимі лаунчери",
"user-guide": "Посібник користувача",
"localization": "Мова & регіон",
"inbox-title": "Вхідні"
},
"notes": {
"new-note": "Нова нотатка",
"duplicate-note-suffix": "(дублікат)",
"duplicate-note-title": "{{- noteTitle }} {{ duplicateNoteSuffix }}"
},
"backend_log": {
"log-does-not-exist": "Файл backend log '{{ fileName }}' не існує.",
"reading-log-failed": "Не вдалося прочитати backend log file {{fileName}}."
},
"content_renderer": {
"note-cannot-be-displayed": "Цей тип нотатки не може бути відображений."
},
"pdf": {
"export_filter": "PDF Document (*.pdf)",
"unable-to-export-message": "Поточну нотатку не вдалося експортувати у PDF.",
"unable-to-export-title": "Не вдається експортувати у PDF",
"unable-to-save-message": "Не вдалося записати вибраний файл. Спробуйте ще раз або виберіть інше місце призначення."
},
"tray": {
"tooltip": "Trilium Notes",
"close": "Вихід з Trilium",
"recents": "Останні нотатки",
"bookmarks": "Закладки",
"today": "Відкрити Щоденник за сьогодні",
"new-note": "Нова нотатка",
"show-windows": "Показати вікна",
"open_new_window": "Відкрити нове вікно"
},
"migration": {
"old_version": "Пряма міграція з вашої поточної версії не підтримується. Будь ласка, спочатку оновіть систему до останньої версії v0.60.4, а потім лише потім до цієї.",
"error_message": "Помилка під час міграції до версії {{version}}: {{stack}}",
"wrong_db_version": "Версія бази даних ({{version}}) новіша за ту, яку очікує програма ({{targetVersion}}), це означає, що її було створено новішою та несумісною версією Trilium. Оновіть Trilium до останньої версії, щоб вирішити цю проблему."
},
"modals": {
"error_title": "Помилка"
},
"share_theme": {
"site-theme": "Тема сайту",
"search_placeholder": "Пошук...",
"image_alt": "Зображення статті",
"last-updated": "Останнє оновлення: {{- date}}",
"subpages": "Підсторінки:",
"on-this-page": "На цій сторінці",
"expand": "Розгорнути"
},
"hidden_subtree_templates": {
"text-snippet": "Фрагмент тексту",
"description": "Опис",
"list-view": "Список",
"grid-view": "Сітка",
"calendar": "Календар",
"table": "Таблиця",
"geo-map": "Географічна карта",
"start-date": "Дата початку",
"end-date": "Дата завершення",
"start-time": "Час початку",
"end-time": "Час завершення",
"geolocation": "Геолокація",
"built-in-templates": "Вбудовані шаблони",
"board": "Дошка",
"status": "Статус",
"board_note_first": "Перша нотатка",
"board_note_second": "Друга нотатка",
"board_note_third": "Третя нотатка",
"board_status_todo": "Зробити",
"board_status_progress": "У процесі",
"board_status_done": "Готово"
"title": "Встановити пароль"
}
}

View File

@@ -0,0 +1,487 @@
asyncapi: 2.6.0
info:
title: Trilium Notes WebSocket API
version: 0.98.0
description: |
Real-time WebSocket API for Trilium Notes client-server communication.
This API handles real-time updates, synchronization, task progress, and streaming responses.
## Authentication
WebSocket connections require the same session authentication as the HTTP API.
The session is validated during the initial WebSocket handshake.
license:
name: GNU Affero General Public License v3.0
url: https://www.gnu.org/licenses/agpl-3.0.html
servers:
development:
url: ws://localhost:8080
protocol: ws
description: Local development server
production:
url: wss://your-trilium-server.com
protocol: wss
description: Production server (secure WebSocket)
channels:
/:
description: Main WebSocket connection endpoint for real-time bidirectional communication
subscribe:
summary: Messages from server to client
message:
oneOf:
- $ref: '#/components/messages/Ping'
- $ref: '#/components/messages/FrontendUpdate'
- $ref: '#/components/messages/SyncPullInProgress'
- $ref: '#/components/messages/SyncPushInProgress'
- $ref: '#/components/messages/SyncFinished'
- $ref: '#/components/messages/SyncFailed'
- $ref: '#/components/messages/ReloadFrontend'
- $ref: '#/components/messages/ConsistencyChecksFailed'
- $ref: '#/components/messages/TaskProgressCount'
- $ref: '#/components/messages/TaskError'
- $ref: '#/components/messages/TaskSucceeded'
- $ref: '#/components/messages/ProtectedSessionLogin'
- $ref: '#/components/messages/ProtectedSessionLogout'
- $ref: '#/components/messages/OpenNote'
- $ref: '#/components/messages/ExecuteScript'
- $ref: '#/components/messages/ApiLogMessages'
- $ref: '#/components/messages/Toast'
- $ref: '#/components/messages/LlmStream'
publish:
summary: Messages from client to server
message:
oneOf:
- $ref: '#/components/messages/ClientPing'
- $ref: '#/components/messages/LogError'
- $ref: '#/components/messages/LogInfo'
components:
messages:
# Client to Server Messages
ClientPing:
name: ping
title: Client Ping
summary: Keep-alive ping from client
contentType: application/json
payload:
type: object
required:
- type
properties:
type:
type: string
const: ping
LogError:
name: log-error
title: Log Error
summary: Report JavaScript error to server
contentType: application/json
payload:
type: object
required:
- type
- error
properties:
type:
type: string
const: log-error
error:
type: string
description: Error message
stack:
type: string
description: Stack trace
LogInfo:
name: log-info
title: Log Info
summary: Send info log to server
contentType: application/json
payload:
type: object
required:
- type
- info
properties:
type:
type: string
const: log-info
info:
type: string
description: Information message
# Server to Client Messages
Ping:
name: ping
title: Server Ping
summary: Keep-alive ping from server
contentType: application/json
payload:
type: object
required:
- type
properties:
type:
type: string
const: ping
FrontendUpdate:
name: frontend-update
title: Frontend Update
summary: Notify frontend of entity changes
contentType: application/json
payload:
type: object
required:
- type
- data
properties:
type:
type: string
const: frontend-update
data:
type: object
properties:
entityChanges:
type: array
items:
type: object
properties:
entityName:
type: string
enum: [notes, branches, attributes, attachments]
entity:
type: object
description: The changed entity data
SyncPullInProgress:
name: sync-pull-in-progress
title: Sync Pull In Progress
summary: Sync pull operation started
contentType: application/json
payload:
type: object
required:
- type
properties:
type:
type: string
const: sync-pull-in-progress
lastSyncedPush:
type: integer
nullable: true
SyncPushInProgress:
name: sync-push-in-progress
title: Sync Push In Progress
summary: Sync push operation started
contentType: application/json
payload:
type: object
required:
- type
properties:
type:
type: string
const: sync-push-in-progress
lastSyncedPush:
type: integer
nullable: true
SyncFinished:
name: sync-finished
title: Sync Finished
summary: Sync operation completed successfully
contentType: application/json
payload:
type: object
required:
- type
properties:
type:
type: string
const: sync-finished
lastSyncedPush:
type: integer
nullable: true
SyncFailed:
name: sync-failed
title: Sync Failed
summary: Sync operation failed
contentType: application/json
payload:
type: object
required:
- type
properties:
type:
type: string
const: sync-failed
lastSyncedPush:
type: integer
nullable: true
ReloadFrontend:
name: reload-frontend
title: Reload Frontend
summary: Request frontend to reload
contentType: application/json
payload:
type: object
required:
- type
properties:
type:
type: string
const: reload-frontend
reason:
type: string
description: Reason for reload
ConsistencyChecksFailed:
name: consistency-checks-failed
title: Consistency Checks Failed
summary: Database consistency check failed
contentType: application/json
payload:
type: object
required:
- type
properties:
type:
type: string
const: consistency-checks-failed
TaskProgressCount:
name: taskProgressCount
title: Task Progress Count
summary: Update task progress
contentType: application/json
payload:
type: object
required:
- type
- taskId
properties:
type:
type: string
const: taskProgressCount
taskId:
type: string
taskType:
type: string
nullable: true
progressCount:
type: integer
TaskError:
name: taskError
title: Task Error
summary: Task encountered an error
contentType: application/json
payload:
type: object
required:
- type
- taskId
properties:
type:
type: string
const: taskError
taskId:
type: string
taskType:
type: string
nullable: true
message:
type: string
TaskSucceeded:
name: taskSucceeded
title: Task Succeeded
summary: Task completed successfully
contentType: application/json
payload:
type: object
required:
- type
- taskId
properties:
type:
type: string
const: taskSucceeded
taskId:
type: string
taskType:
type: string
nullable: true
result:
type: object
ProtectedSessionLogin:
name: protectedSessionLogin
title: Protected Session Login
summary: Protected session was entered
contentType: application/json
payload:
type: object
required:
- type
properties:
type:
type: string
const: protectedSessionLogin
ProtectedSessionLogout:
name: protectedSessionLogout
title: Protected Session Logout
summary: Protected session was exited
contentType: application/json
payload:
type: object
required:
- type
properties:
type:
type: string
const: protectedSessionLogout
OpenNote:
name: openNote
title: Open Note
summary: Request to open a note
contentType: application/json
payload:
type: object
required:
- type
- noteId
properties:
type:
type: string
const: openNote
noteId:
type: string
ExecuteScript:
name: execute-script
title: Execute Script
summary: Execute a script
contentType: application/json
payload:
type: object
required:
- type
- script
properties:
type:
type: string
const: execute-script
script:
type: string
params:
type: array
items:
type: any
startNoteId:
type: string
currentNoteId:
type: string
originEntityName:
type: string
const: notes
originEntityId:
type: string
nullable: true
ApiLogMessages:
name: api-log-messages
title: API Log Messages
summary: API log messages
contentType: application/json
payload:
type: object
required:
- type
- messages
properties:
type:
type: string
const: api-log-messages
messages:
type: array
items:
type: string
Toast:
name: toast
title: Toast Notification
summary: Show toast notification
contentType: application/json
payload:
type: object
required:
- type
- message
properties:
type:
type: string
const: toast
message:
type: string
LlmStream:
name: llm-stream
title: LLM Stream
summary: LLM streaming response
contentType: application/json
payload:
type: object
required:
- type
- chatNoteId
properties:
type:
type: string
const: llm-stream
chatNoteId:
type: string
content:
type: string
description: Response text content
thinking:
type: string
description: Internal reasoning/thinking process
toolExecution:
type: object
properties:
action:
type: string
tool:
type: string
toolCallId:
type: string
args:
type: object
result:
type: any
error:
type: string
done:
type: boolean
description: Whether streaming is complete
error:
type: string
nullable: true
raw:
type: any
description: Raw response data

View File

@@ -61,22 +61,21 @@ function verifyOpenIDSubjectIdentifier(subjectIdentifier: string) {
throw new OpenIdError("Database not initialized!");
}
if (!isUserSaved()) {
// No user exists yet - this is a new user registration, which is allowed
return true;
if (isUserSaved()) {
return false;
}
const salt = sql.getValue<string>("SELECT salt FROM user_data;");
const salt = sql.getValue("SELECT salt FROM user_data;");
if (salt == undefined) {
console.log("OpenID verification: Salt undefined - database may be corrupted");
console.log("Salt undefined");
return undefined;
}
const givenHash = myScryptService
.getSubjectIdentifierVerificationHash(subjectIdentifier, salt)
.getSubjectIdentifierVerificationHash(subjectIdentifier)
?.toString("base64");
if (givenHash === undefined) {
console.log("OpenID verification: Failed to generate hash for subject identifier");
console.log("Sub id hash undefined!");
return undefined;
}
@@ -84,13 +83,12 @@ function verifyOpenIDSubjectIdentifier(subjectIdentifier: string) {
"SELECT userIDVerificationHash FROM user_data"
);
if (savedHash === undefined) {
console.log("OpenID verification: No saved verification hash found");
console.log("verification hash undefined");
return undefined;
}
const matches = givenHash === savedHash;
console.log(`OpenID verification: Subject identifier match = ${matches}`);
return matches;
console.log("Matches: " + givenHash === savedHash);
return givenHash === savedHash;
}
function setDataKey(

View File

@@ -1,845 +0,0 @@
import { describe, it, expect, beforeEach, vi, type MockedFunction } from 'vitest';
import type { Request, Response } from 'express';
import type { Session } from 'express-openid-connect';
// Mock dependencies before imports
vi.mock('./cls.js');
vi.mock('./options.js');
vi.mock('./config.js');
vi.mock('./sql.js');
vi.mock('./sql_init.js');
vi.mock('./encryption/open_id_encryption.js');
vi.mock('./log.js', () => ({
default: {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn()
}
}));
// Import modules after mocking
import openID from './open_id.js';
import options from './options.js';
import config from './config.js';
import sql from './sql.js';
import sqlInit from './sql_init.js';
import openIDEncryption from './encryption/open_id_encryption.js';
// Type assertions for mocked functions
const mockGetOptionOrNull = options.getOptionOrNull as MockedFunction<typeof options.getOptionOrNull>;
const mockGetValue = sql.getValue as MockedFunction<typeof sql.getValue>;
const mockIsDbInitialized = sqlInit.isDbInitialized as MockedFunction<typeof sqlInit.isDbInitialized>;
const mockSaveUser = openIDEncryption.saveUser as MockedFunction<typeof openIDEncryption.saveUser>;
describe('OpenID Service', () => {
beforeEach(() => {
vi.clearAllMocks();
// Setup default config
config.MultiFactorAuthentication = {
oauthBaseUrl: 'https://trilium.example.com',
oauthClientId: 'test-client-id',
oauthClientSecret: 'test-client-secret',
oauthIssuerBaseUrl: 'https://auth.example.com',
oauthIssuerName: 'TestAuth',
oauthIssuerIcon: 'https://auth.example.com/icon.png'
};
});
describe('isOpenIDEnabled', () => {
it('returns true when OAuth is properly configured and enabled', () => {
mockGetOptionOrNull.mockReturnValue('oauth');
const result = openID.isOpenIDEnabled();
expect(result).toBe(true);
});
it('returns false when MFA method is not OAuth', () => {
mockGetOptionOrNull.mockReturnValue('totp');
const result = openID.isOpenIDEnabled();
expect(result).toBe(false);
});
it('returns false when configuration is missing', () => {
config.MultiFactorAuthentication.oauthClientId = '';
mockGetOptionOrNull.mockReturnValue('oauth');
const result = openID.isOpenIDEnabled();
expect(result).toBe(false);
});
});
describe('generateOAuthConfig', () => {
beforeEach(() => {
mockIsDbInitialized.mockReturnValue(true);
mockGetValue.mockReturnValue('testuser');
});
it('generates valid OAuth configuration', () => {
const generatedConfig = openID.generateOAuthConfig();
expect(generatedConfig).toMatchObject({
baseURL: 'https://trilium.example.com',
clientID: 'test-client-id',
clientSecret: 'test-client-secret',
issuerBaseURL: 'https://auth.example.com',
secret: expect.any(String),
authorizationParams: {
response_type: 'code',
scope: 'openid profile email',
access_type: 'offline',
prompt: 'consent'
},
routes: {
callback: '/callback',
login: '/authenticate',
postLogoutRedirect: '/login',
logout: '/logout'
},
idpLogout: true
});
});
it('includes afterCallback handler', () => {
const generatedConfig = openID.generateOAuthConfig();
expect(generatedConfig.afterCallback).toBeDefined();
expect(typeof generatedConfig.afterCallback).toBe('function');
});
});
describe('afterCallback handler', () => {
let mockReq: Partial<Request>;
let mockRes: Partial<Response>;
let mockSession: Session;
let afterCallback: (req: Request, res: Response, session: Session) => Promise<Session>;
beforeEach(() => {
mockReq = {
oidc: {
user: undefined
},
session: {
loggedIn: false,
lastAuthState: undefined
}
} as any;
mockRes = {} as Response;
mockSession = {
id_token: 'mock.id.token'
} as Session;
mockIsDbInitialized.mockReturnValue(true);
mockGetValue.mockReturnValue('testuser');
mockSaveUser.mockResolvedValue(undefined); // Reset to default behavior
const generatedConfig = openID.generateOAuthConfig();
afterCallback = generatedConfig.afterCallback!;
});
describe('with complete user claims', () => {
beforeEach(() => {
mockReq.oidc = {
user: {
sub: 'user123',
name: 'John Doe',
email: 'john@example.com',
email_verified: true
},
idTokenClaims: {
sub: 'user123',
name: 'John Doe',
email: 'john@example.com',
email_verified: true
}
} as any;
});
it('saves user and sets session for valid user data', async () => {
const result = await afterCallback(mockReq as Request, mockRes as Response, mockSession);
expect(mockSaveUser).toHaveBeenCalledWith(
'user123',
'John Doe',
'john@example.com'
);
expect(mockReq.session!.loggedIn).toBe(true);
expect(mockReq.session!.lastAuthState).toEqual({
totpEnabled: false,
ssoEnabled: true
});
expect(result).toBe(mockSession);
});
});
describe('with missing name claim (Authentik scenario)', () => {
it('uses given_name and family_name when name is missing', async () => {
mockReq.oidc = {
user: {
sub: 'auth123',
given_name: 'Jane',
family_name: 'Smith',
email: 'jane@example.com'
}
} as any;
await afterCallback(mockReq as Request, mockRes as Response, mockSession);
expect(mockSaveUser).toHaveBeenCalledWith(
'auth123',
'Jane Smith',
'jane@example.com'
);
});
it('uses preferred_username when name fields are missing', async () => {
mockReq.oidc = {
user: {
sub: 'auth456',
preferred_username: 'jdoe',
email: 'jdoe@example.com'
}
} as any;
await afterCallback(mockReq as Request, mockRes as Response, mockSession);
expect(mockSaveUser).toHaveBeenCalledWith(
'auth456',
'jdoe',
'jdoe@example.com'
);
});
it('extracts name from email when other fields are missing', async () => {
mockReq.oidc = {
user: {
sub: 'auth789',
email: 'johndoe@example.com'
}
} as any;
await afterCallback(mockReq as Request, mockRes as Response, mockSession);
expect(mockSaveUser).toHaveBeenCalledWith(
'auth789',
'johndoe',
'johndoe@example.com'
);
});
it('generates fallback name when all name sources are missing', async () => {
mockReq.oidc = {
user: {
sub: 'auth000xyz'
}
} as any;
await afterCallback(mockReq as Request, mockRes as Response, mockSession);
expect(mockSaveUser).toHaveBeenCalledWith(
'auth000xyz',
'User-auth000x',
'auth000xyz@oauth.local'
);
});
});
describe('with missing email claim', () => {
it('generates placeholder email when email is missing', async () => {
mockReq.oidc = {
user: {
sub: 'nomail123',
name: 'No Email User'
}
} as any;
await afterCallback(mockReq as Request, mockRes as Response, mockSession);
expect(mockSaveUser).toHaveBeenCalledWith(
'nomail123',
'No Email User',
'nomail123@oauth.local'
);
});
});
describe('error handling', () => {
it('returns session when database is not initialized', async () => {
mockIsDbInitialized.mockReturnValue(false);
mockReq.oidc = {
user: {
sub: 'user123',
name: 'John Doe',
email: 'john@example.com'
}
} as any;
const result = await afterCallback(mockReq as Request, mockRes as Response, mockSession);
expect(mockSaveUser).not.toHaveBeenCalled();
expect(result).toBe(mockSession);
});
it('throws error when no user object is provided', async () => {
mockReq.oidc = {
user: undefined
} as any;
await expect(afterCallback(mockReq as Request, mockRes as Response, mockSession)).rejects.toThrow('OAuth authentication failed');
expect(mockSaveUser).not.toHaveBeenCalled();
});
it('returns session when subject identifier is missing', async () => {
mockReq.oidc = {
user: {
name: 'No Sub User',
email: 'nosub@example.com'
}
} as any;
const result = await afterCallback(mockReq as Request, mockRes as Response, mockSession);
expect(mockSaveUser).not.toHaveBeenCalled();
expect(result).toBe(mockSession);
});
const invalidSubjects = [
['null', null, false],
['undefined', undefined, false],
['empty string', '', false],
['whitespace', ' ', false],
['empty object', {}, true, '{}'], // Objects get stringified
['array', [], true, '[]'], // Arrays get stringified
['zero', 0, true, '0'], // Numbers get stringified
['false', false, true, 'false'] // Booleans get stringified
];
invalidSubjects.forEach((testCase) => {
const [description, value, shouldCallSaveUser, expectedSub] = testCase;
it(`handles ${description} subject identifier`, async () => {
mockReq.oidc = {
user: {
sub: value,
name: 'Test User',
email: 'test@example.com'
}
} as any;
const result = await afterCallback(mockReq as Request, mockRes as Response, mockSession);
if (shouldCallSaveUser) {
// The implementation converts these to strings, which is a bug
// but we test the actual behavior
expect(mockSaveUser).toHaveBeenCalledWith(
expectedSub,
'Test User',
'test@example.com'
);
} else {
expect(mockSaveUser).not.toHaveBeenCalled();
}
expect(result).toBe(mockSession);
vi.clearAllMocks();
});
});
it('throws error and sets loggedIn to false when saveUser fails', async () => {
mockReq.oidc = {
user: {
sub: 'user123',
name: 'John Doe',
email: 'john@example.com'
}
} as any;
mockSaveUser.mockImplementation(() => {
throw new Error('Database error');
});
await expect(afterCallback(mockReq as Request, mockRes as Response, mockSession)).rejects.toThrow('Database error');
expect(mockReq.session!.loggedIn).toBe(false);
});
});
describe('edge cases', () => {
it('handles numeric subject identifiers', async () => {
mockReq.oidc = {
user: {
sub: 12345,
name: 'Numeric Sub',
email: 'numeric@example.com'
}
} as any;
await afterCallback(mockReq as Request, mockRes as Response, mockSession);
expect(mockSaveUser).toHaveBeenCalledWith(
'12345',
'Numeric Sub',
'numeric@example.com'
);
});
it('handles very long names gracefully', async () => {
const longName = 'A'.repeat(1000);
mockReq.oidc = {
user: {
sub: 'longname123',
name: longName,
email: 'long@example.com'
}
} as any;
await afterCallback(mockReq as Request, mockRes as Response, mockSession);
expect(mockSaveUser).toHaveBeenCalledWith(
'longname123',
longName,
'long@example.com'
);
});
it('handles special characters in claims', async () => {
mockReq.oidc = {
user: {
sub: 'special!@#$%',
name: 'Name with émojis 🎉',
email: 'special+tag@example.com'
}
} as any;
await afterCallback(mockReq as Request, mockRes as Response, mockSession);
expect(mockSaveUser).toHaveBeenCalledWith(
'special!@#$%',
'Name with émojis 🎉',
'special+tag@example.com'
);
});
});
});
describe('getOAuthStatus', () => {
it('returns OAuth status with user information', () => {
mockGetValue
.mockReturnValueOnce('johndoe') // username
.mockReturnValueOnce('john@example.com'); // email
mockGetOptionOrNull.mockReturnValue('oauth');
const status = openID.getOAuthStatus();
expect(status).toEqual({
success: true,
name: 'johndoe',
email: 'john@example.com',
enabled: true,
missingVars: []
});
});
it('includes missing configuration variables', () => {
config.MultiFactorAuthentication.oauthClientId = '';
config.MultiFactorAuthentication.oauthClientSecret = '';
mockGetValue
.mockReturnValueOnce('johndoe')
.mockReturnValueOnce('john@example.com');
mockGetOptionOrNull.mockReturnValue('oauth');
const status = openID.getOAuthStatus();
expect(status.missingVars).toContain('oauthClientId');
expect(status.missingVars).toContain('oauthClientSecret');
expect(status.enabled).toBe(false);
});
});
describe('getSSOIssuerName', () => {
it('returns configured issuer name', () => {
config.MultiFactorAuthentication.oauthIssuerName = 'CustomAuth';
const name = openID.getSSOIssuerName();
expect(name).toBe('CustomAuth');
});
});
describe('getSSOIssuerIcon', () => {
it('returns configured issuer icon', () => {
config.MultiFactorAuthentication.oauthIssuerIcon = 'https://example.com/icon.png';
const icon = openID.getSSOIssuerIcon();
expect(icon).toBe('https://example.com/icon.png');
});
});
describe('Configuration validation', () => {
it('detects missing oauthBaseUrl', () => {
config.MultiFactorAuthentication.oauthBaseUrl = '';
mockGetOptionOrNull.mockReturnValue('oauth');
const result = openID.isOpenIDEnabled();
expect(result).toBe(false);
});
it('does not detect missing oauthIssuerBaseUrl (not validated)', () => {
// Note: The implementation doesn't actually validate oauthIssuerBaseUrl
// This is a potential bug - the issuer URL should be validated
config.MultiFactorAuthentication.oauthIssuerBaseUrl = '';
mockGetOptionOrNull.mockReturnValue('oauth');
const result = openID.isOpenIDEnabled();
// The implementation returns true even with missing issuerBaseUrl
expect(result).toBe(true);
});
it('handles all configuration fields being empty', () => {
config.MultiFactorAuthentication = {
oauthBaseUrl: '',
oauthClientId: '',
oauthClientSecret: '',
oauthIssuerBaseUrl: '',
oauthIssuerName: '',
oauthIssuerIcon: ''
};
mockGetOptionOrNull.mockReturnValue('oauth');
const result = openID.isOpenIDEnabled();
expect(result).toBe(false);
});
});
describe('Provider compatibility tests', () => {
let afterCallback: (req: Request, res: Response, session: Session) => Promise<Session>;
beforeEach(() => {
mockIsDbInitialized.mockReturnValue(true);
const generatedConfig = openID.generateOAuthConfig();
afterCallback = generatedConfig.afterCallback!;
});
const providerTestCases = [
{
provider: 'Google OAuth',
user: {
sub: 'google-oauth2|123456789',
name: 'Google User',
given_name: 'Google',
family_name: 'User',
email: 'user@gmail.com',
email_verified: true,
picture: 'https://lh3.googleusercontent.com/...'
},
expected: {
sub: 'google-oauth2|123456789',
name: 'Google User',
email: 'user@gmail.com'
}
},
{
provider: 'Authentik (minimal claims)',
user: {
sub: 'ak-user-123',
preferred_username: 'authentik_user'
},
expected: {
sub: 'ak-user-123',
name: 'authentik_user',
email: 'ak-user-123@oauth.local'
}
},
{
provider: 'Keycloak',
user: {
sub: 'f:123e4567-e89b-12d3-a456-426614174000:keycloak',
preferred_username: 'keycloak.user',
given_name: 'Keycloak',
family_name: 'User',
email: 'user@keycloak.local'
},
expected: {
sub: 'f:123e4567-e89b-12d3-a456-426614174000:keycloak',
name: 'Keycloak User',
email: 'user@keycloak.local'
}
},
{
provider: 'Auth0',
user: {
sub: 'auth0|507f1f77bcf86cd799439011',
nickname: 'auth0user',
name: 'Auth0 User',
email: 'user@auth0.com',
email_verified: true
},
expected: {
sub: 'auth0|507f1f77bcf86cd799439011',
name: 'Auth0 User',
email: 'user@auth0.com'
}
}
];
providerTestCases.forEach(({ provider, user, expected }) => {
it(`handles ${provider} user claims format`, async () => {
const mockReq = {
oidc: { user },
session: {}
} as any;
await afterCallback(mockReq, {} as Response, {} as Session);
expect(mockSaveUser).toHaveBeenCalledWith(
expected.sub,
expected.name,
expected.email
);
});
});
});
describe('verifyOpenIDSubjectIdentifier with encryption', () => {
it('correctly verifies matching subject identifier', () => {
// Setup: User is saved
mockGetValue.mockImplementation((query: string) => {
if (query.includes('isSetup')) return 'true';
if (query.includes('salt')) return 'test-salt-value';
if (query.includes('userIDVerificationHash')) return 'dGVzdC1oYXNoLXZhbHVl'; // base64 encoded
return undefined;
});
mockIsDbInitialized.mockReturnValue(true);
// Mock the verification to return true for matching
vi.mocked(openIDEncryption.verifyOpenIDSubjectIdentifier).mockReturnValue(true);
const result = openIDEncryption.verifyOpenIDSubjectIdentifier('test-subject-id');
expect(result).toBe(true);
expect(openIDEncryption.verifyOpenIDSubjectIdentifier).toHaveBeenCalledWith('test-subject-id');
});
it('correctly rejects non-matching subject identifier', () => {
// Setup: User is saved with different subject
mockGetValue.mockImplementation((query: string) => {
if (query.includes('isSetup')) return 'true';
if (query.includes('salt')) return 'test-salt-value';
if (query.includes('userIDVerificationHash')) return 'ZGlmZmVyZW50LWhhc2g='; // different hash
return undefined;
});
mockIsDbInitialized.mockReturnValue(true);
// Mock the verification to return false for non-matching
vi.mocked(openIDEncryption.verifyOpenIDSubjectIdentifier).mockReturnValue(false);
const result = openIDEncryption.verifyOpenIDSubjectIdentifier('wrong-subject-id');
expect(result).toBe(false);
expect(openIDEncryption.verifyOpenIDSubjectIdentifier).toHaveBeenCalledWith('wrong-subject-id');
});
it('returns undefined when salt is missing', () => {
// Setup: Salt is missing
mockGetValue.mockImplementation((query: string) => {
if (query.includes('isSetup')) return 'true';
if (query.includes('salt')) return undefined;
return undefined;
});
mockIsDbInitialized.mockReturnValue(true);
// Mock the verification to return undefined for missing salt
vi.mocked(openIDEncryption.verifyOpenIDSubjectIdentifier).mockReturnValue(undefined);
const result = openIDEncryption.verifyOpenIDSubjectIdentifier('test-subject-id');
expect(result).toBe(undefined);
});
it('returns undefined when verification hash is missing', () => {
// Setup: Hash is missing
mockGetValue.mockImplementation((query: string) => {
if (query.includes('isSetup')) return 'true';
if (query.includes('salt')) return 'test-salt-value';
if (query.includes('userIDVerificationHash')) return undefined;
return undefined;
});
mockIsDbInitialized.mockReturnValue(true);
// Mock the verification to return undefined for missing hash
vi.mocked(openIDEncryption.verifyOpenIDSubjectIdentifier).mockReturnValue(undefined);
const result = openIDEncryption.verifyOpenIDSubjectIdentifier('test-subject-id');
expect(result).toBe(undefined);
});
it('handles empty subject identifier gracefully', () => {
mockGetValue.mockImplementation((query: string) => {
if (query.includes('isSetup')) return 'true';
if (query.includes('salt')) return 'test-salt-value';
if (query.includes('userIDVerificationHash')) return 'dGVzdC1oYXNoLXZhbHVl';
return undefined;
});
mockIsDbInitialized.mockReturnValue(true);
// Mock the verification to return false for empty identifier
vi.mocked(openIDEncryption.verifyOpenIDSubjectIdentifier).mockReturnValue(false);
const result = openIDEncryption.verifyOpenIDSubjectIdentifier('');
expect(result).toBe(false);
expect(openIDEncryption.verifyOpenIDSubjectIdentifier).toHaveBeenCalledWith('');
});
it('correctly uses salt parameter when provided during save', () => {
mockIsDbInitialized.mockReturnValue(true);
// Mock that no user is saved yet
mockGetValue.mockImplementation((query: string) => {
if (query.includes('isSetup')) return undefined;
return undefined;
});
// Mock successful save with salt
vi.mocked(openIDEncryption.saveUser).mockReturnValue(true);
const result = openIDEncryption.saveUser(
'new-subject-id',
'Test User',
'test@example.com'
);
expect(result).toBe(true);
expect(openIDEncryption.saveUser).toHaveBeenCalledWith(
'new-subject-id',
'Test User',
'test@example.com'
);
});
it('handles special characters in subject identifier', () => {
const specialSubjectId = 'user@example.com/+special=chars&test';
mockGetValue.mockImplementation((query: string) => {
if (query.includes('isSetup')) return 'true';
if (query.includes('salt')) return 'test-salt-value';
if (query.includes('userIDVerificationHash')) return 'c3BlY2lhbC1oYXNo'; // special hash
return undefined;
});
mockIsDbInitialized.mockReturnValue(true);
// Mock the verification to handle special characters
vi.mocked(openIDEncryption.verifyOpenIDSubjectIdentifier).mockReturnValue(true);
const result = openIDEncryption.verifyOpenIDSubjectIdentifier(specialSubjectId);
expect(result).toBe(true);
expect(openIDEncryption.verifyOpenIDSubjectIdentifier).toHaveBeenCalledWith(specialSubjectId);
});
it('handles very long subject identifiers', () => {
const longSubjectId = 'a'.repeat(500); // 500 character subject ID
mockGetValue.mockImplementation((query: string) => {
if (query.includes('isSetup')) return 'true';
if (query.includes('salt')) return 'test-salt-value';
if (query.includes('userIDVerificationHash')) return 'bG9uZy1oYXNo'; // long hash
return undefined;
});
mockIsDbInitialized.mockReturnValue(true);
// Mock the verification to handle long identifiers
vi.mocked(openIDEncryption.verifyOpenIDSubjectIdentifier).mockReturnValue(true);
const result = openIDEncryption.verifyOpenIDSubjectIdentifier(longSubjectId);
expect(result).toBe(true);
expect(openIDEncryption.verifyOpenIDSubjectIdentifier).toHaveBeenCalledWith(longSubjectId);
});
it('verifies case sensitivity of subject identifier', () => {
mockGetValue.mockImplementation((query: string) => {
if (query.includes('isSetup')) return 'true';
if (query.includes('salt')) return 'test-salt-value';
if (query.includes('userIDVerificationHash')) return 'Y2FzZS1zZW5zaXRpdmU=';
return undefined;
});
mockIsDbInitialized.mockReturnValue(true);
// Mock: lowercase should match
vi.mocked(openIDEncryption.verifyOpenIDSubjectIdentifier)
.mockReturnValueOnce(true) // lowercase matches
.mockReturnValueOnce(false); // uppercase doesn't match
const result1 = openIDEncryption.verifyOpenIDSubjectIdentifier('user-id-lowercase');
expect(result1).toBe(true);
const result2 = openIDEncryption.verifyOpenIDSubjectIdentifier('USER-ID-LOWERCASE');
expect(result2).toBe(false);
});
it('handles database not initialized error', () => {
mockIsDbInitialized.mockReturnValue(false);
// When DB is not initialized, the open_id_encryption throws an error
// We'll mock it to throw an error
vi.mocked(openIDEncryption.verifyOpenIDSubjectIdentifier).mockImplementation(() => {
throw new Error('Database not initialized!');
});
expect(() => {
openIDEncryption.verifyOpenIDSubjectIdentifier('test-subject-id');
}).toThrow('Database not initialized!');
});
it('correctly handles salt with special characters', () => {
const saltWithSpecialChars = 'salt+with/special=chars&symbols';
mockGetValue.mockImplementation((query: string) => {
if (query.includes('isSetup')) return 'true';
if (query.includes('salt')) return saltWithSpecialChars;
if (query.includes('userIDVerificationHash')) return 'c3BlY2lhbC1zYWx0LWhhc2g=';
return undefined;
});
mockIsDbInitialized.mockReturnValue(true);
vi.mocked(openIDEncryption.verifyOpenIDSubjectIdentifier).mockReturnValue(true);
const result = openIDEncryption.verifyOpenIDSubjectIdentifier('test-subject-id');
expect(result).toBe(true);
});
it('handles concurrent verification attempts correctly', () => {
mockGetValue.mockImplementation((query: string) => {
if (query.includes('isSetup')) return 'true';
if (query.includes('salt')) return 'test-salt-value';
if (query.includes('userIDVerificationHash')) return 'Y29uY3VycmVudC1oYXNo';
return undefined;
});
mockIsDbInitialized.mockReturnValue(true);
vi.mocked(openIDEncryption.verifyOpenIDSubjectIdentifier).mockReturnValue(true);
// Simulate concurrent verification attempts
const results = [
openIDEncryption.verifyOpenIDSubjectIdentifier('subject-1'),
openIDEncryption.verifyOpenIDSubjectIdentifier('subject-1'),
openIDEncryption.verifyOpenIDSubjectIdentifier('subject-1')
];
expect(results).toEqual([true, true, true]);
expect(openIDEncryption.verifyOpenIDSubjectIdentifier).toHaveBeenCalledTimes(3);
});
});
});

View File

@@ -5,382 +5,7 @@ import options from "./options.js";
import type { Session } from "express-openid-connect";
import sql from "./sql.js";
import config from "./config.js";
import log from "./log.js";
/**
* Enhanced error types for OAuth/OIDC errors
*/
interface OAuthError extends Error {
error?: string;
error_description?: string;
error_uri?: string;
error_hint?: string;
state?: string;
scope?: string;
code?: string;
errno?: string;
syscall?: string;
cause?: any;
statusCode?: number;
headers?: Record<string, string>;
}
/**
* OPError type - errors from the OpenID Provider
*/
interface OPError extends OAuthError {
name: 'OPError';
response?: {
body?: any;
statusCode?: number;
headers?: Record<string, string>;
};
}
/**
* RPError type - errors from the Relying Party (client-side)
*/
interface RPError extends OAuthError {
name: 'RPError';
response?: {
body?: any;
statusCode?: number;
};
checks?: Record<string, any>;
}
/**
* Type definition for OIDC user claims
* These may not all be present depending on the provider configuration
*/
interface OIDCUserClaims {
sub?: string | number | undefined; // Subject identifier (required in OIDC spec but may be missing)
name?: string | undefined; // Full name
given_name?: string | undefined; // First name
family_name?: string | undefined; // Last name
preferred_username?: string | undefined; // Username
email?: string | undefined; // Email address
email_verified?: boolean | undefined;
[key: string]: unknown; // Allow additional claims
}
/**
* Type guard to check if a value is a non-empty string
*/
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
/**
* Safely converts a value to string with fallback
*/
function safeToString(value: unknown, fallback = ''): string {
if (value === null || value === undefined) {
return fallback;
}
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}
if (typeof value === 'object') {
try {
return JSON.stringify(value);
} catch {
return fallback;
}
}
return fallback;
}
/**
* Type guard for OPError
*/
function isOPError(error: any): error is OPError {
return error?.name === 'OPError' ||
(error?.constructor?.name === 'OPError') ||
(error?.error && typeof error?.error === 'string');
}
/**
* Type guard for RPError
*/
function isRPError(error: any): error is RPError {
return error?.name === 'RPError' ||
(error?.constructor?.name === 'RPError') ||
(error?.checks && typeof error?.checks === 'object');
}
/**
* Extract detailed error information from various error types
*/
function extractErrorDetails(error: any): Record<string, any> {
const details: Record<string, any> = {};
// Basic error properties
if (error?.message) details.message = error.message;
if (error?.name) details.errorType = error.name;
if (error?.code) details.code = error.code;
if (error?.statusCode) details.statusCode = error.statusCode;
// OAuth-specific error properties
if (error?.error) details.error = error.error;
if (error?.error_description) details.error_description = error.error_description;
if (error?.error_uri) details.error_uri = error.error_uri;
if (error?.error_hint) details.error_hint = error.error_hint;
if (error?.state) details.state = error.state;
if (error?.scope) details.scope = error.scope;
// System error properties
if (error?.errno) details.errno = error.errno;
if (error?.syscall) details.syscall = error.syscall;
// Response information for OPError/RPError
if (error?.response) {
details.response = {
statusCode: error.response.statusCode,
body: error.response.body
};
// Don't log full headers to avoid sensitive data, just important ones
if (error.response.headers) {
details.response.headers = {
'content-type': error.response.headers['content-type'],
'www-authenticate': error.response.headers['www-authenticate']
};
}
}
// RPError specific checks
if (error?.checks) {
details.checks = error.checks;
}
// Nested cause error
if (error?.cause) {
details.cause = extractErrorDetails(error.cause);
}
return details;
}
/**
* Log comprehensive error details with actionable guidance
*/
function logOAuthError(context: string, error: any, req?: Request): void {
const errorDetails = extractErrorDetails(error);
// Always log the full error details
log.error(`OAuth ${context}: ${JSON.stringify(errorDetails, null, 2)}`);
// Provide specific guidance based on error type
if (isOPError(error)) {
log.error(`OAuth ${context}: OpenID Provider Error detected`);
// Handle specific OPError types
switch (error.error) {
case 'invalid_request':
log.error('Action: Check that all required parameters are being sent in the authorization request');
break;
case 'invalid_client':
log.error('Action: Verify OAuth client ID and client secret are correct');
log.error(`Current client ID: ${config.MultiFactorAuthentication.oauthClientId?.substring(0, 10)}...`);
break;
case 'invalid_grant':
log.error('Action: Authorization code may be expired or already used. User should try logging in again');
if (req?.session) {
log.error(`Session ID: ${req.session.id?.substring(0, 10)}...`);
}
break;
case 'unauthorized_client':
log.error('Action: Client is not authorized for this grant type. Check OAuth provider configuration');
break;
case 'unsupported_grant_type':
log.error('Action: Provider does not support authorization_code grant type. Check provider documentation');
break;
case 'invalid_scope':
log.error('Action: Requested scopes are invalid. Current scopes: openid profile email');
break;
case 'access_denied':
log.error('Action: User denied the authorization request or provider blocked access');
break;
case 'temporarily_unavailable':
log.error('Action: OAuth provider is temporarily unavailable. Try again later');
break;
case 'server_error':
log.error('Action: OAuth provider encountered an error. Check provider logs if available');
break;
case 'interaction_required':
log.error('Action: User interaction is required but prompt=none was requested');
break;
default:
if (error.error_description) {
log.error(`Provider guidance: ${error.error_description}`);
}
}
} else if (isRPError(error)) {
log.error(`OAuth ${context}: Relying Party (Client) Error detected`);
// Handle specific RPError types
if (error.checks) {
log.error('Failed validation checks:');
Object.entries(error.checks).forEach(([check, value]) => {
log.error(` - ${check}: ${JSON.stringify(value)}`);
});
}
if (error.message?.includes('state mismatch')) {
log.error('Action: State parameter mismatch. This can happen due to:');
log.error(' 1. Multiple login attempts in different tabs');
log.error(' 2. Session expired during login');
log.error(' 3. CSRF attack attempt (unlikely)');
log.error('Solution: Clear cookies and try logging in again');
} else if (error.message?.includes('nonce mismatch')) {
log.error('Action: Nonce mismatch detected. Similar to state mismatch');
log.error('Solution: Clear session and retry authentication');
} else if (error.message?.includes('JWT')) {
log.error('Action: JWT validation failed. Check:');
log.error(' 1. Clock synchronization between client and provider');
log.error(' 2. JWT signature algorithm configuration');
log.error(' 3. Issuer URL consistency');
}
} else if (error?.message?.includes('getaddrinfo') || error?.code === 'ENOTFOUND') {
log.error(`OAuth ${context}: DNS resolution failed`);
log.error(`Action: Cannot resolve host: ${config.MultiFactorAuthentication.oauthIssuerBaseUrl}`);
log.error('Solutions:');
log.error(' 1. Verify the OAuth issuer URL is correct');
log.error(' 2. Check DNS configuration (especially in Docker)');
log.error(' 3. Try using IP address instead of hostname');
log.error(' 4. Check network connectivity');
} else if (error?.code === 'ECONNREFUSED') {
log.error(`OAuth ${context}: Connection refused`);
log.error(`Target: ${config.MultiFactorAuthentication.oauthIssuerBaseUrl}`);
log.error('Solutions:');
log.error(' 1. Verify the OAuth provider is running');
log.error(' 2. Check firewall rules');
log.error(' 3. In Docker, ensure services are on the same network');
log.error(' 4. Verify port numbers are correct');
} else if (error?.code === 'ETIMEDOUT' || error?.code === 'ESOCKETTIMEDOUT') {
log.error(`OAuth ${context}: Request timeout`);
log.error('Solutions:');
log.error(' 1. Check network latency to OAuth provider');
log.error(' 2. Increase timeout values if possible');
log.error(' 3. Check for network congestion or packet loss');
} else if (error?.message?.includes('certificate')) {
log.error(`OAuth ${context}: SSL/TLS certificate issue`);
log.error('Solutions:');
log.error(' 1. For self-signed certificates, configure NODE_TLS_REJECT_UNAUTHORIZED=0 (dev only)');
log.error(' 2. Add CA certificate to trusted store');
log.error(' 3. Verify certificate validity and expiration');
} else if (error?.message?.includes('Unexpected token')) {
log.error(`OAuth ${context}: Invalid response format`);
log.error('Likely causes:');
log.error(' 1. Provider returned HTML error page instead of JSON');
log.error(' 2. Proxy or firewall intercepting requests');
log.error(' 3. Wrong endpoint URL configured');
}
// Log request context if available
if (req) {
const urlPath = req.originalUrl ? req.originalUrl.split('?')[0] : req.url;
if (urlPath) {
log.error(`Request path: ${urlPath}`);
}
if (req.method) {
log.error(`Request method: ${req.method}`);
}
// Log session state for debugging
if (req.session?.id) {
log.error(`Session ID (first 10 chars): ${req.session.id.substring(0, 10)}...`);
}
}
// Always log stack trace for debugging
if (error?.stack) {
const stackLines = error.stack.split('\n').slice(0, 5);
log.error('Stack trace (first 5 lines):');
stackLines.forEach((line: string) => log.error(` ${line.trim()}`));
}
}
/**
* Extracts and validates user information from OIDC claims
*/
function extractUserInfo(user: OIDCUserClaims): {
sub: string;
name: string;
email: string;
} | null {
// Extract subject identifier (required by OIDC spec)
const sub = safeToString(user.sub);
if (!isNonEmptyString(sub)) {
log.error('OAuth: CRITICAL - Missing or invalid subject identifier (sub) in user claims!');
log.error('The "sub" claim is REQUIRED by the OpenID Connect specification.');
log.error(`Received claims: ${JSON.stringify(user, null, 2)}`);
log.error('Possible causes:');
log.error(' 1. OAuth provider is not OIDC-compliant');
log.error(' 2. Provider configuration is incorrect');
log.error(' 3. Token parsing failed');
log.error(' 4. Using OAuth2 instead of OpenID Connect');
return null;
}
// Validate subject identifier quality
if (sub.length < 1) {
log.error(`OAuth: Subject identifier too short (length=${sub.length}): "${sub}"`);
log.error('This may indicate a configuration problem with the OAuth provider');
return null;
}
// Warn about suspicious subject identifiers
if (sub === 'undefined' || sub === 'null' || sub === '[object Object]') {
log.error(`OAuth: Subject identifier appears to be a stringified error value: "${sub}"`);
log.error('This indicates a serious problem with the OAuth provider or token parsing');
return null;
}
// Extract name with multiple fallback strategies
let name = '';
// Try direct name field
if (isNonEmptyString(user.name)) {
name = user.name;
}
// Try concatenating given_name and family_name
else if (isNonEmptyString(user.given_name) || isNonEmptyString(user.family_name)) {
const parts: string[] = [];
if (isNonEmptyString(user.given_name)) parts.push(user.given_name);
if (isNonEmptyString(user.family_name)) parts.push(user.family_name);
name = parts.join(' ');
}
// Try preferred_username
else if (isNonEmptyString(user.preferred_username)) {
name = user.preferred_username;
}
// Try email username part
else if (isNonEmptyString(user.email)) {
const emailParts = user.email.split('@');
if (emailParts.length > 0 && emailParts[0]) {
name = emailParts[0];
}
}
// Final fallback to subject identifier
if (!isNonEmptyString(name)) {
name = `User-${sub.substring(0, 8)}`;
}
// Extract email with fallback
let email = '';
if (isNonEmptyString(user.email)) {
email = user.email;
} else {
// Generate a placeholder email if none provided
email = `${sub}@oauth.local`;
}
return { sub, name, email };
}
function checkOpenIDConfig() {
const missingVars: string[] = []
@@ -483,13 +108,6 @@ function generateOAuthConfig() {
const logoutParams = {
};
// No need to log configuration details - users can check their environment variables
// The connectivity test will verify if everything is working
// Log what we're configuring
log.info(`OAuth config: baseURL=${config.MultiFactorAuthentication.oauthBaseUrl}, issuerBaseURL=${config.MultiFactorAuthentication.oauthIssuerBaseUrl}`);
log.info(`OAuth config: redirect URI will be: ${config.MultiFactorAuthentication.oauthBaseUrl}/callback`);
const authConfig = {
authRequired: false,
auth0Logout: false,
@@ -508,456 +126,32 @@ function generateOAuthConfig() {
routes: authRoutes,
idpLogout: true,
logoutParams: logoutParams,
// Add error handling for required auth failures
errorOnRequiredAuth: true,
// Enable detailed error messages
enableTelemetry: false,
// Explicitly configure to get user info
getLoginState: (req: Request) => {
// This ensures user info is fetched
return {
returnTo: req.originalUrl || '/'
};
},
// afterCallback is called only on successful token exchange
afterCallback: async (req: Request, res: Response, session: Session) => {
try {
log.info('OAuth afterCallback: Token exchange successful, processing user information');
if (!sqlInit.isDbInitialized()) return session;
// Check if database is initialized
if (!sqlInit.isDbInitialized()) {
log.info('OAuth afterCallback: Database not initialized, skipping user save');
if (!req.oidc.user) {
console.log("user invalid!");
return session;
}
// Check for callback errors in query parameters first
if (req.query?.error) {
log.error(`OAuth afterCallback: Provider returned error: ${req.query.error}`);
if (req.query.error_description) {
log.error(`OAuth afterCallback: Error description: ${req.query.error_description}`);
}
// Still try to set session to avoid breaking the flow
req.session.loggedIn = false;
return session;
}
// Log detailed OIDC state and session info
log.info(`OAuth afterCallback: Session has idToken=${!!session.id_token}, hasAccessToken=${!!session.access_token}, hasRefreshToken=${!!session.refresh_token}`);
log.info(`OAuth afterCallback: OIDC state - hasOidc=${!!req.oidc}, hasIdTokenClaims=${!!req.oidc?.idTokenClaims}`);
// Log comprehensive OAuth context for debugging
if (req.oidc) {
const isAuth = typeof req.oidc.isAuthenticated === 'function' ? req.oidc.isAuthenticated() : 'N/A';
log.info(`OAuth afterCallback: Context details - isAuthenticated=${isAuth}, ` +
`hasIdToken=${!!req.oidc.idToken}, hasAccessToken=${!!req.oidc.accessToken}, ` +
`hasRefreshToken=${!!req.oidc.refreshToken}, hasUser=${!!req.oidc.user}`);
}
// Parse and log ID token payload (safely) for debugging
if (session.id_token) {
try {
const parts = session.id_token.split('.');
if (parts.length === 3) {
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
// Log token payload for debugging (trusted logging environment)
const safePayload = {
iss: payload.iss,
aud: payload.aud,
exp: payload.exp ? new Date(payload.exp * 1000).toISOString() : undefined,
iat: payload.iat ? new Date(payload.iat * 1000).toISOString() : undefined,
sub: payload.sub,
email: payload.email,
name: payload.name,
given_name: payload.given_name,
family_name: payload.family_name,
preferred_username: payload.preferred_username,
nickname: payload.nickname,
groups: payload.groups ? `[${payload.groups.length} groups]` : undefined,
all_claims: Object.keys(payload).sort().join(', ')
};
log.info(`OAuth afterCallback: ID Token payload (masked): ${JSON.stringify(safePayload, null, 2)}`);
}
} catch (tokenError) {
log.error(`OAuth afterCallback: Failed to parse ID token for logging: ${tokenError}`);
}
}
// According to express-openid-connect v2 best practices, idTokenClaims is most reliable in afterCallback
// The session parameter contains the verified tokens
let user: OIDCUserClaims | undefined;
// Primary source: idTokenClaims from verified ID token
if (req.oidc?.idTokenClaims) {
log.info('OAuth afterCallback: Using idTokenClaims from verified ID token');
user = req.oidc.idTokenClaims as OIDCUserClaims;
// Log the actual claims structure for debugging
const claimKeys = Object.keys(req.oidc.idTokenClaims);
log.info(`OAuth afterCallback: idTokenClaims has ${claimKeys.length} properties: [${claimKeys.sort().join(', ')}]`);
// Log claim values for debugging (trusted logging environment)
const claimValues: any = {};
for (const key of claimKeys) {
const value = (req.oidc.idTokenClaims as any)[key];
if (typeof value === 'string' && value.length > 200) {
claimValues[key] = `${value.substring(0, 200)}...[truncated, length: ${value.length}]`;
} else if (Array.isArray(value)) {
claimValues[key] = `[Array with ${value.length} items: ${JSON.stringify(value.slice(0, 5))}${value.length > 5 ? '...' : ''}]`;
} else if (typeof value === 'object' && value !== null) {
claimValues[key] = `[Object with keys: ${Object.keys(value).join(', ')}]`;
} else {
claimValues[key] = value;
}
}
log.info(`OAuth afterCallback: idTokenClaims content: ${JSON.stringify(claimValues, null, 2)}`);
}
// Fallback: req.oidc.user (may be available in some configurations)
else if (req.oidc?.user) {
log.info('OAuth afterCallback: idTokenClaims not available, using req.oidc.user');
user = req.oidc.user as OIDCUserClaims;
const userKeys = Object.keys(req.oidc.user);
log.info(`OAuth afterCallback: req.oidc.user has ${userKeys.length} properties: [${userKeys.sort().join(', ')}]`);
}
// Log what we have for debugging
else {
log.error('OAuth afterCallback: No user claims available in req.oidc');
log.error(`Session has id_token: ${!!session.id_token}, access_token: ${!!session.access_token}`);
}
// Fallback: Parse ID token directly if req.oidc is not populated
// This handles cases where express-openid-connect doesn't properly populate req.oidc
if (!user && session.id_token) {
log.info('OAuth afterCallback: Attempting to parse ID token directly from session');
try {
const parts = session.id_token.split('.');
if (parts.length === 3) {
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
user = payload as OIDCUserClaims;
log.info('OAuth afterCallback: Successfully parsed ID token from session');
log.info(`OAuth afterCallback: Parsed claims: sub="${payload.sub}", name="${payload.name}", email="${payload.email}"`);
} else {
log.error('OAuth afterCallback: Invalid ID token format (expected 3 parts)');
}
} catch (parseError) {
log.error(`OAuth afterCallback: Failed to parse ID token from session: ${parseError}`);
}
}
// Fallback: Call UserInfo endpoint if we have an access token but no user info
if (!user && session.access_token) {
log.info('OAuth afterCallback: No user info from ID token, attempting UserInfo endpoint');
try {
// Get the issuer URL from config
const issuerURL = config.MultiFactorAuthentication.oauthIssuerBaseUrl;
if (issuerURL) {
// Construct UserInfo endpoint URL
// Try to determine the correct URL format based on the issuer
let userinfoUrl: string;
// For Keycloak/Authentik style URLs (ends with /realms/xxx or similar)
if (issuerURL.includes('/realms/') || issuerURL.includes('/application/o/')) {
userinfoUrl = issuerURL.endsWith('/')
? `${issuerURL}protocol/openid-connect/userinfo`
: `${issuerURL}/protocol/openid-connect/userinfo`;
}
// For standard OIDC providers (Auth0, Okta, etc.)
else {
userinfoUrl = issuerURL.endsWith('/')
? `${issuerURL}userinfo`
: `${issuerURL}/userinfo`;
}
log.info(`OAuth afterCallback: Calling UserInfo endpoint at ${userinfoUrl}`);
// Make the UserInfo request
const response = await fetch(userinfoUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${session.access_token}`,
'Accept': 'application/json'
}
});
if (response.ok) {
const userInfo = await response.json();
user = userInfo as OIDCUserClaims;
log.info('OAuth afterCallback: Successfully retrieved user info from UserInfo endpoint');
log.info(`OAuth afterCallback: UserInfo claims: sub="${userInfo.sub}", name="${userInfo.name}", email="${userInfo.email}"`);
} else {
const errorText = await response.text();
log.error(`OAuth afterCallback: UserInfo endpoint returned error: ${response.status} ${response.statusText}`);
log.error(`OAuth afterCallback: UserInfo error response: ${errorText}`);
}
} else {
log.error('OAuth afterCallback: Cannot call UserInfo endpoint - issuer URL not configured');
}
} catch (userinfoError) {
log.error(`OAuth afterCallback: Failed to fetch UserInfo: ${userinfoError}`);
}
}
// Check if user object exists after all attempts
if (!user) {
log.error('OAuth afterCallback: No user object received after all fallback attempts');
log.error('Attempted:');
log.error(' 1. req.oidc.idTokenClaims');
log.error(' 2. req.oidc.user');
log.error(' 3. Direct ID token parsing from session');
log.error(' 4. UserInfo endpoint (if access token available)');
log.error('This can happen when:');
log.error(' 1. ID token does not contain user claims (only sub)');
log.error(' 2. OAuth provider not configured to include claims in ID token');
log.error(' 3. Token validation failed');
log.error(' 4. UserInfo endpoint is not accessible or returns no data');
log.error('Consider checking your OAuth provider configuration for "openid profile email" scopes');
// Log raw OAuth context for maximum debugging
if (req.oidc) {
const isAuth = typeof req.oidc.isAuthenticated === 'function' ? req.oidc.isAuthenticated() : 'N/A';
log.error(`OAuth afterCallback: Raw oidc state: ${JSON.stringify({
isAuthenticated: isAuth,
hasIdToken: !!req.oidc.idToken,
hasAccessToken: !!req.oidc.accessToken,
hasIdTokenClaims: !!req.oidc.idTokenClaims,
hasUser: !!req.oidc.user,
idTokenClaimsKeys: req.oidc.idTokenClaims ? Object.keys(req.oidc.idTokenClaims) : [],
userKeys: req.oidc.user ? Object.keys(req.oidc.user) : []
}, null, 2)}`);
}
// DO NOT allow login without proper authentication data
req.session.loggedIn = false;
// Throw error to prevent authentication without user info
throw new Error('OAuth authentication failed: Unable to retrieve user information from any source');
}
const userClaims = user as OIDCUserClaims;
// Log available claims for debugging (trusted logging environment)
log.info(`OAuth afterCallback: User claims received - sub="${userClaims.sub}", name="${userClaims.name}", email="${userClaims.email}", given_name="${userClaims.given_name}", family_name="${userClaims.family_name}", preferred_username="${userClaims.preferred_username}", claimKeys=[${Object.keys(userClaims).join(', ')}]`);
// Extract and validate user information
const userInfo = extractUserInfo(userClaims);
if (!userInfo) {
log.error('OAuth afterCallback: Failed to extract valid user information from claims');
log.error(`Raw claims: ${JSON.stringify(userClaims, null, 2)}`);
// Still return session to avoid breaking the auth flow
return session;
}
log.info(`OAuth afterCallback: User info extracted successfully - sub="${userInfo.sub}", name="${userInfo.name}", email="${userInfo.email}"`);
// Check if a user already exists and verify subject identifier matches
if (isUserSaved()) {
// User exists, verify the subject identifier matches
const isValidUser = openIDEncryption.verifyOpenIDSubjectIdentifier(userInfo.sub);
if (isValidUser === false) {
log.error('OAuth afterCallback: CRITICAL - Subject identifier mismatch!');
log.error('A different user is already configured in Trilium.');
log.error(`Current login sub: ${userInfo.sub}`);
log.error('This is a single-user system. To use a different OAuth account:');
log.error(' 1. Clear the existing user data');
log.error(' 2. Restart Trilium');
log.error(' 3. Login with the new account');
// Don't allow login with mismatched subject
// We can't return a Response here, so we throw an error
// The error will be handled by the Express error handler
throw new Error('OAuth: User mismatch - a different user is already configured');
} else if (isValidUser === undefined) {
log.error('OAuth afterCallback: Unable to verify subject identifier');
log.error('This might indicate database corruption or configuration issues');
} else {
log.info('OAuth afterCallback: Existing user verified successfully');
}
} else {
// No existing user, save the new one
const saved = openIDEncryption.saveUser(
userInfo.sub,
userInfo.name,
userInfo.email
openIDEncryption.saveUser(
req.oidc.user.sub.toString(),
req.oidc.user.name.toString(),
req.oidc.user.email.toString()
);
if (saved === false) {
log.error('OAuth afterCallback: Failed to save user - a user may already exist');
log.error('This can happen in a race condition with concurrent logins');
} else if (saved === undefined) {
log.error('OAuth afterCallback: Critical error saving user - check logs');
} else {
log.info('OAuth afterCallback: New user saved successfully');
}
}
// Set session variables for successful authentication
req.session.loggedIn = true;
req.session.lastAuthState = {
totpEnabled: false,
ssoEnabled: true
};
log.info('OAuth afterCallback: Authentication completed successfully');
} catch (error) {
// Log comprehensive error details
logOAuthError('AfterCallback Processing Error', error, req);
// DO NOT set loggedIn = true on errors - this is a security risk
try {
req.session.loggedIn = false;
log.error('OAuth afterCallback: Authentication failed due to error');
} catch (sessionError) {
logOAuthError('AfterCallback Session Error', sessionError, req);
}
// Re-throw the error to ensure authentication fails
throw error;
}
return session;
},
};
return authConfig;
}
/**
* Enhanced middleware to log OAuth errors with comprehensive details
*/
function oauthErrorLogger(err: any, req: Request, res: Response, next: NextFunction) {
if (err) {
// Use the comprehensive error logging function
logOAuthError('Middleware Error', err, req);
// Additional middleware-specific handling
if (err.name === 'InternalOAuthError') {
// InternalOAuthError is a wrapper used by express-openid-connect
log.error('OAuth Middleware: InternalOAuthError detected - this usually wraps the actual error');
if (err.cause) {
log.error('OAuth Middleware: Examining wrapped error...');
logOAuthError('Wrapped Error', err.cause, req);
}
}
// Check for specific middleware states
if (req.oidc) {
const isAuth = typeof req.oidc.isAuthenticated === 'function' ? req.oidc.isAuthenticated() : 'N/A';
log.error(`OAuth Middleware: OIDC state - isAuthenticated=${isAuth}, hasUser=${!!req.oidc.user}, hasIdToken=${!!req.oidc.idToken}, hasAccessToken=${!!req.oidc.accessToken}`);
}
// Log response headers that might contain error information
const wwwAuth = res.getHeader('WWW-Authenticate');
if (wwwAuth) {
log.error(`OAuth Middleware: WWW-Authenticate header: ${wwwAuth}`);
}
// Check for redirect_uri mismatch errors specifically
if (err.message?.includes('redirect_uri_mismatch') ||
err.error_description?.includes('redirect_uri') ||
err.error_description?.includes('redirect URI') ||
err.error_description?.includes('Redirect URI')) {
log.error('');
log.error('OAuth Error: redirect_uri mismatch detected!');
log.error('');
log.error('This error means the redirect URI sent to the OAuth provider does not match what was configured.');
log.error('');
log.error('POSSIBLE CAUSES:');
log.error(' 1. If behind a reverse proxy: trustedReverseProxy may not be set');
log.error(' - The provider expects HTTPS but Trilium might be sending HTTP');
log.error(' 2. The OAuth provider redirect URI configuration is incorrect');
log.error(' 3. The baseURL in Trilium config does not match the actual URL');
log.error('');
log.error('Check the diagnostic logs above to see what protocol was detected.');
log.error('');
log.error('IF behind a reverse proxy, try setting:');
log.error(' In config.ini: trustedReverseProxy=true');
log.error(' Or environment: TRILIUM_NETWORK_TRUSTEDREVERSEPROXY=true');
log.error('');
}
// For other token exchange failures, provide general guidance
else if (err.message?.includes('Failed to obtain access token') ||
err.message?.includes('Token request failed') ||
err.error === 'invalid_grant') {
log.error('OAuth Middleware: Token exchange failure detected');
log.error('Common solutions:');
log.error(' 1. Verify client secret is correct and matches provider configuration');
log.error(' 2. Check if authorization code expired (typically valid for 10 minutes)');
log.error(' 3. Ensure redirect URI matches exactly what is configured in provider');
log.error(' 4. Verify clock synchronization between client and provider (for JWT validation)');
log.error(' 5. Check if the authorization code was already used (codes are single-use)');
// Log timing information if available
if (req.session) {
const now = Date.now();
log.error(`Current time: ${new Date(now).toISOString()}`);
}
}
// For state mismatch errors, provide detailed debugging
if (err.message?.includes('state') || err.checks?.state === false) {
log.error('OAuth Middleware: State parameter mismatch');
log.error('Debugging information:');
if (req.query.state) {
log.error(` Received state (first 10 chars): ${String(req.query.state).substring(0, 10)}...`);
}
if (req.session?.id) {
log.error(` Session ID (first 10 chars): ${req.session.id.substring(0, 10)}...`);
}
log.error('This can happen when:');
log.error(' - User has multiple login tabs open');
log.error(' - Session expired during login flow');
log.error(' - Cookies are blocked or not properly configured');
log.error(' - Load balancer without sticky sessions');
}
}
// Pass the error to the next error handler
next(err);
}
/**
* Helper function to test OAuth connectivity
* Useful for debugging network issues between containers
*/
async function testOAuthConnectivity(): Promise<{success: boolean, error?: string}> {
const issuerUrl = config.MultiFactorAuthentication.oauthIssuerBaseUrl;
if (!issuerUrl) {
return { success: false, error: 'No issuer URL configured' };
}
try {
log.info(`Testing OAuth connectivity to: ${issuerUrl}`);
// Try to fetch the OpenID configuration
const configUrl = issuerUrl.endsWith('/')
? `${issuerUrl}.well-known/openid-configuration`
: `${issuerUrl}/.well-known/openid-configuration`;
const response = await fetch(configUrl);
if (response.ok) {
log.info('OAuth connectivity test successful');
const config = await response.json();
log.info(`OAuth provider endpoints discovered: token=${config.token_endpoint ? 'yes' : 'no'}, userinfo=${config.userinfo_endpoint ? 'yes' : 'no'}`);
return { success: true };
} else {
const error = `OAuth provider returned status ${response.status}`;
log.error(`OAuth connectivity test failed: ${error}`);
return { success: false, error };
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
log.error(`OAuth connectivity test failed: ${errorMsg}`);
return { success: false, error: errorMsg };
}
}
export default {
generateOAuthConfig,
getOAuthStatus,
@@ -967,6 +161,4 @@ export default {
clearSavedUser,
isTokenValid,
isUserSaved,
oauthErrorLogger,
testOAuthConnectivity,
};

View File

@@ -56,7 +56,7 @@
"jsonc-eslint-parser": "^2.1.0",
"nx": "21.3.11",
"react-refresh": "^0.17.0",
"rollup-plugin-webpack-stats": "2.1.4",
"rollup-plugin-webpack-stats": "2.1.3",
"tslib": "^2.3.0",
"tsx": "4.20.4",
"typescript": "~5.9.0",

138
pnpm-lock.yaml generated
View File

@@ -65,7 +65,7 @@ importers:
version: 21.3.11(@babel/traverse@7.28.0)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17))(@types/node@22.17.2)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.33.0(jiti@2.5.1))(nx@21.3.11(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17)))(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.17))(@types/node@22.17.2)(typescript@5.9.2))(typescript@5.9.2)
'@nx/playwright':
specifier: 21.3.11
version: 21.3.11(@babel/traverse@7.28.0)(@playwright/test@1.55.0)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.33.0(jiti@2.5.1))(nx@21.3.11(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17)))(typescript@5.9.2)
version: 21.3.11(@babel/traverse@7.28.0)(@playwright/test@1.54.2)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.33.0(jiti@2.5.1))(nx@21.3.11(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17)))(typescript@5.9.2)
'@nx/vite':
specifier: 21.3.11
version: 21.3.11(@babel/traverse@7.28.0)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17))(nx@21.3.11(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17)))(typescript@5.9.2)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)
@@ -74,7 +74,7 @@ importers:
version: 21.3.11(@babel/traverse@7.28.0)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17))(nx@21.3.11(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17)))
'@playwright/test':
specifier: ^1.36.0
version: 1.55.0
version: 1.54.2
'@triliumnext/server':
specifier: workspace:*
version: link:apps/server
@@ -130,8 +130,8 @@ importers:
specifier: ^0.17.0
version: 0.17.0
rollup-plugin-webpack-stats:
specifier: 2.1.4
version: 2.1.4(rolldown@1.0.0-beta.29)(rollup@4.46.3)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))
specifier: 2.1.3
version: 2.1.3(rolldown@1.0.0-beta.29)(rollup@4.46.3)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))
tslib:
specifier: ^2.3.0
version: 2.8.1
@@ -238,8 +238,8 @@ importers:
specifier: 16.3.0
version: 16.3.0
i18next:
specifier: 25.4.0
version: 25.4.0(typescript@5.9.2)
specifier: 25.3.6
version: 25.3.6(typescript@5.9.2)
i18next-http-backend:
specifier: 3.0.2
version: 3.0.2(encoding@0.1.13)
@@ -286,8 +286,8 @@ importers:
specifier: 10.27.1
version: 10.27.1
react-i18next:
specifier: 15.7.0
version: 15.7.0(i18next@25.4.0(typescript@5.9.2))(react-dom@19.1.0(react@16.14.0))(react@16.14.0)(typescript@5.9.2)
specifier: 15.6.1
version: 15.6.1(i18next@25.3.6(typescript@5.9.2))(react-dom@19.1.0(react@16.14.0))(react@16.14.0)(typescript@5.9.2)
split.js:
specifier: 1.6.5
version: 1.6.5
@@ -690,8 +690,8 @@ importers:
specifier: 7.0.6
version: 7.0.6
i18next:
specifier: 25.4.0
version: 25.4.0(typescript@5.9.2)
specifier: 25.3.6
version: 25.3.6(typescript@5.9.2)
i18next-fs-backend:
specifier: 2.6.0
version: 2.6.0
@@ -812,10 +812,10 @@ importers:
version: 9.33.0
'@sveltejs/adapter-auto':
specifier: ^6.0.0
version: 6.1.0(@sveltejs/kit@2.36.1(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)))(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)))
version: 6.1.0(@sveltejs/kit@2.34.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)))(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)))
'@sveltejs/kit':
specifier: ^2.16.0
version: 2.36.1(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)))(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))
version: 2.34.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)))(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))
'@sveltejs/vite-plugin-svelte':
specifier: ^6.0.0
version: 6.1.3(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))
@@ -903,7 +903,7 @@ importers:
version: 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
'@vitest/browser':
specifier: ^3.0.5
version: 3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.55.0)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
version: 3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.54.2)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
'@vitest/coverage-istanbul':
specifier: ^3.0.5
version: 3.2.4(vitest@3.2.4)
@@ -963,7 +963,7 @@ importers:
version: 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
'@vitest/browser':
specifier: ^3.0.5
version: 3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.55.0)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
version: 3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.54.2)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
'@vitest/coverage-istanbul':
specifier: ^3.0.5
version: 3.2.4(vitest@3.2.4)
@@ -1023,7 +1023,7 @@ importers:
version: 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
'@vitest/browser':
specifier: ^3.0.5
version: 3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.55.0)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
version: 3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.54.2)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
'@vitest/coverage-istanbul':
specifier: ^3.0.5
version: 3.2.4(vitest@3.2.4)
@@ -1090,7 +1090,7 @@ importers:
version: 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
'@vitest/browser':
specifier: ^3.0.5
version: 3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.55.0)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
version: 3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.54.2)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
'@vitest/coverage-istanbul':
specifier: ^3.0.5
version: 3.2.4(vitest@3.2.4)
@@ -1157,7 +1157,7 @@ importers:
version: 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
'@vitest/browser':
specifier: ^3.0.5
version: 3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.55.0)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
version: 3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.54.2)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
'@vitest/coverage-istanbul':
specifier: ^3.0.5
version: 3.2.4(vitest@3.2.4)
@@ -4171,8 +4171,8 @@ packages:
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
'@playwright/test@1.55.0':
resolution: {integrity: sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==}
'@playwright/test@1.54.2':
resolution: {integrity: sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==}
engines: {node: '>=18'}
hasBin: true
@@ -5198,8 +5198,8 @@ packages:
peerDependencies:
'@sveltejs/kit': ^2.0.0
'@sveltejs/kit@2.36.1':
resolution: {integrity: sha512-dldNCtSIpaGxQMEfHaUxSPH/k3uU28pTZwtKzfkn8fqpOjWufKlMBeIL7FJ/s93dOrhEq41zaQYkXh+XTgEgVw==}
'@sveltejs/kit@2.34.0':
resolution: {integrity: sha512-xSwh4x6SkKqDKK2lx7VY+JBtovHBcZNN8benzPGQn9cJRpWmVZ872mDsi/kJ6w8+c7lBobbiAf2W4VeSvzKvHA==}
engines: {node: '>=18.13'}
hasBin: true
peerDependencies:
@@ -9587,8 +9587,8 @@ packages:
i18next-http-backend@3.0.2:
resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==}
i18next@25.4.0:
resolution: {integrity: sha512-UH5aiamXsO3cfrZFurCHiB6YSs3C+s+XY9UaJllMMSbmaoXILxFgqDEZu4NbfzJFjmUo3BNMa++Rjkr3ofjfLw==}
i18next@25.3.6:
resolution: {integrity: sha512-dThZ0CTCM3sUG/qS0ZtQYZQcUI6DtBN8yBHK+SKEqihPcEYmjVWh/YJ4luic73Iq6Uxhp6q7LJJntRK5+1t7jQ==}
peerDependencies:
typescript: ^5
peerDependenciesMeta:
@@ -11840,13 +11840,13 @@ packages:
pkg-types@2.1.0:
resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==}
playwright-core@1.55.0:
resolution: {integrity: sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==}
playwright-core@1.54.2:
resolution: {integrity: sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==}
engines: {node: '>=18'}
hasBin: true
playwright@1.55.0:
resolution: {integrity: sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==}
playwright@1.54.2:
resolution: {integrity: sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==}
engines: {node: '>=18'}
hasBin: true
@@ -12884,8 +12884,8 @@ packages:
peerDependencies:
react: ^19.1.0
react-i18next@15.7.0:
resolution: {integrity: sha512-hogS6K+7hJnGN1k5hpxcY0x3SnQ30K2Cj9PMKSwP8lqyhfbj7DEdjFhc7tXh9+z+npDQaxvPCGnpkRmCnRNCcQ==}
react-i18next@15.6.1:
resolution: {integrity: sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg==}
peerDependencies:
i18next: '>= 23.2.3'
react: '>= 16.8.0'
@@ -13215,8 +13215,8 @@ packages:
peerDependencies:
rollup: ^3.0.0||^4.0.0
rollup-plugin-webpack-stats@2.1.4:
resolution: {integrity: sha512-kv9W0rK9Qy1Q8/vkxEPGOwJvq7zVERnHD13At5Jv15FPPOXwxm1LblXtpuASeoM1ALhOPD0sJSDz64qgtSgh7g==}
rollup-plugin-webpack-stats@2.1.3:
resolution: {integrity: sha512-OOhpuwwoxW8J5pVd+RdokSbaVa21/4/mV1EsBBLfmcmc2hjL5VMFkytN0YTFGfPWcluWBCxrpA+8SP7P3xvloQ==}
engines: {node: '>=18'}
peerDependencies:
rolldown: ^1.0.0-beta.0
@@ -16800,6 +16800,8 @@ snapshots:
'@ckeditor/ckeditor5-core': 46.0.2
'@ckeditor/ckeditor5-upload': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-ai@46.0.2':
dependencies:
@@ -16866,6 +16868,8 @@ snapshots:
'@ckeditor/ckeditor5-ui': 46.0.2
'@ckeditor/ckeditor5-utils': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-bookmark@46.0.2':
dependencies:
@@ -16928,6 +16932,8 @@ snapshots:
'@ckeditor/ckeditor5-core': 46.0.2
'@ckeditor/ckeditor5-utils': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-code-block@46.0.2(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
dependencies:
@@ -17134,6 +17140,8 @@ snapshots:
'@ckeditor/ckeditor5-widget': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-easy-image@46.0.2':
dependencies:
@@ -17153,6 +17161,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-classic@46.0.2':
dependencies:
@@ -17162,6 +17172,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-decoupled@46.0.2':
dependencies:
@@ -17171,6 +17183,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-inline@46.0.2':
dependencies:
@@ -17231,6 +17245,8 @@ snapshots:
'@ckeditor/ckeditor5-core': 46.0.2
'@ckeditor/ckeditor5-engine': 46.0.2
'@ckeditor/ckeditor5-utils': 46.0.2
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-essentials@46.0.2':
dependencies:
@@ -17288,6 +17304,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-font@46.0.2':
dependencies:
@@ -17297,8 +17315,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 46.0.2
'@ckeditor/ckeditor5-utils': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-format-painter@46.0.2':
dependencies:
@@ -17351,6 +17367,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 46.0.2
'@ckeditor/ckeditor5-widget': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-html-embed@46.0.2':
dependencies:
@@ -17459,8 +17477,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-list-multi-level@46.0.2':
dependencies:
@@ -17523,8 +17539,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 46.0.2
'@ckeditor/ckeditor5-widget': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-mention@46.0.2(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)':
dependencies:
@@ -17612,8 +17626,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 46.0.2
'@ckeditor/ckeditor5-widget': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-pagination@46.0.2':
dependencies:
@@ -17677,8 +17689,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 46.0.2
'@ckeditor/ckeditor5-utils': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-restricted-editing@46.0.2':
dependencies:
@@ -17722,8 +17732,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 46.0.2
'@ckeditor/ckeditor5-utils': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-slash-command@46.0.2':
dependencies:
@@ -17799,8 +17807,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-template@46.0.2':
dependencies:
@@ -17875,8 +17881,6 @@ snapshots:
'@ckeditor/ckeditor5-icons': 46.0.2
'@ckeditor/ckeditor5-ui': 46.0.2
'@ckeditor/ckeditor5-utils': 46.0.2
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-upload@46.0.2':
dependencies:
@@ -17913,8 +17917,6 @@ snapshots:
'@ckeditor/ckeditor5-engine': 46.0.2
'@ckeditor/ckeditor5-utils': 46.0.2
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-widget@46.0.2':
dependencies:
@@ -17934,8 +17936,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 46.0.2
ckeditor5: 46.0.2(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@codemirror/autocomplete@6.18.6':
dependencies:
@@ -20050,7 +20050,7 @@ snapshots:
'@nx/nx-win32-x64-msvc@21.3.11':
optional: true
'@nx/playwright@21.3.11(@babel/traverse@7.28.0)(@playwright/test@1.55.0)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.33.0(jiti@2.5.1))(nx@21.3.11(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17)))(typescript@5.9.2)':
'@nx/playwright@21.3.11(@babel/traverse@7.28.0)(@playwright/test@1.54.2)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.33.0(jiti@2.5.1))(nx@21.3.11(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17)))(typescript@5.9.2)':
dependencies:
'@nx/devkit': 21.3.11(nx@21.3.11(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17)))
'@nx/eslint': 21.3.11(@babel/traverse@7.28.0)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.33.0(jiti@2.5.1))(nx@21.3.11(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.17)))
@@ -20059,7 +20059,7 @@ snapshots:
minimatch: 9.0.3
tslib: 2.8.1
optionalDependencies:
'@playwright/test': 1.55.0
'@playwright/test': 1.54.2
transitivePeerDependencies:
- '@babel/traverse'
- '@swc-node/register'
@@ -20261,9 +20261,9 @@ snapshots:
'@pkgr/core@0.2.9': {}
'@playwright/test@1.55.0':
'@playwright/test@1.54.2':
dependencies:
playwright: 1.55.0
playwright: 1.54.2
'@polka/url@1.0.0-next.29': {}
@@ -21319,11 +21319,11 @@ snapshots:
dependencies:
acorn: 8.15.0
'@sveltejs/adapter-auto@6.1.0(@sveltejs/kit@2.36.1(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)))(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)))':
'@sveltejs/adapter-auto@6.1.0(@sveltejs/kit@2.34.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)))(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)))':
dependencies:
'@sveltejs/kit': 2.36.1(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)))(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))
'@sveltejs/kit': 2.34.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)))(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))
'@sveltejs/kit@2.36.1(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)))(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))':
'@sveltejs/kit@2.34.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)))(svelte@5.38.2)(vite@7.1.3(@types/node@24.2.1)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))':
dependencies:
'@standard-schema/spec': 1.0.0
'@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0)
@@ -22488,7 +22488,7 @@ snapshots:
- bufferutil
- utf-8-validate
'@vitest/browser@3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.55.0)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))':
'@vitest/browser@3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.54.2)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))':
dependencies:
'@testing-library/dom': 10.4.0
'@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0)
@@ -22500,7 +22500,7 @@ snapshots:
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.30.1)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)
ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)
optionalDependencies:
playwright: 1.55.0
playwright: 1.54.2
webdriverio: 9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5)
transitivePeerDependencies:
- bufferutil
@@ -22541,7 +22541,7 @@ snapshots:
tinyrainbow: 2.0.0
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.30.1)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)
optionalDependencies:
'@vitest/browser': 3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.55.0)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
'@vitest/browser': 3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.54.2)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
transitivePeerDependencies:
- supports-color
@@ -26775,7 +26775,7 @@ snapshots:
transitivePeerDependencies:
- encoding
i18next@25.4.0(typescript@5.9.2):
i18next@25.3.6(typescript@5.9.2):
dependencies:
'@babel/runtime': 7.27.6
optionalDependencies:
@@ -29568,11 +29568,11 @@ snapshots:
exsolve: 1.0.5
pathe: 2.0.3
playwright-core@1.55.0: {}
playwright-core@1.54.2: {}
playwright@1.55.0:
playwright@1.54.2:
dependencies:
playwright-core: 1.55.0
playwright-core: 1.54.2
optionalDependencies:
fsevents: 2.3.2
@@ -30622,11 +30622,11 @@ snapshots:
react: 16.14.0
scheduler: 0.26.0
react-i18next@15.7.0(i18next@25.4.0(typescript@5.9.2))(react-dom@19.1.0(react@16.14.0))(react@16.14.0)(typescript@5.9.2):
react-i18next@15.6.1(i18next@25.3.6(typescript@5.9.2))(react-dom@19.1.0(react@16.14.0))(react@16.14.0)(typescript@5.9.2):
dependencies:
'@babel/runtime': 7.27.6
html-parse-stringify: 3.0.1
i18next: 25.4.0(typescript@5.9.2)
i18next: 25.3.6(typescript@5.9.2)
react: 16.14.0
optionalDependencies:
react-dom: 19.1.0(react@16.14.0)
@@ -31034,7 +31034,7 @@ snapshots:
'@rollup/pluginutils': 5.1.4(rollup@4.40.0)
rollup: 4.40.0
rollup-plugin-webpack-stats@2.1.4(rolldown@1.0.0-beta.29)(rollup@4.46.3)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)):
rollup-plugin-webpack-stats@2.1.3(rolldown@1.0.0-beta.29)(rollup@4.46.3)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)):
dependencies:
rolldown: 1.0.0-beta.29
rollup-plugin-stats: 1.5.0(rolldown@1.0.0-beta.29)(rollup@4.46.3)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))
@@ -33215,7 +33215,7 @@ snapshots:
optionalDependencies:
'@types/debug': 4.1.12
'@types/node': 22.17.2
'@vitest/browser': 3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.55.0)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
'@vitest/browser': 3.2.4(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.17.2)(typescript@5.9.2))(playwright@1.54.2)(utf-8-validate@6.0.5)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.87.0)(sass@1.87.0)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(vitest@3.2.4)(webdriverio@9.19.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
'@vitest/ui': 3.2.4(vitest@3.2.4)
happy-dom: 18.0.1
jsdom: 26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)