meta-sovereign 0.50.0 → 0.51.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.hi.md +3 -0
  2. package/README.md +3 -0
  3. package/README.ru.md +3 -0
  4. package/README.zh.md +3 -0
  5. package/js/src/cli/automation-commands.js +2 -1
  6. package/js/src/cli/contact-commands.js +2 -1
  7. package/js/src/cli/cv-commands.js +5 -4
  8. package/js/src/cli/greetings-commands.js +2 -1
  9. package/js/src/cli/index.js +44 -24
  10. package/js/src/cli/migrate-commands.js +2 -1
  11. package/js/src/cli/trust-commands.js +3 -2
  12. package/js/src/cv/browser.js +42 -2
  13. package/js/src/server/cors.js +11 -0
  14. package/js/src/server/index.js +4 -1
  15. package/js/src/server/log.js +2 -1
  16. package/js/src/server/routes-telegram.js +2 -13
  17. package/js/src/server/telegram-route-error.js +29 -0
  18. package/js/src/sources/telegram-account.js +10 -1
  19. package/js/src/sources/telegram-backfill.js +49 -25
  20. package/js/src/sources/telegram-catchup.js +24 -10
  21. package/js/src/sources/telegram-channels.js +13 -4
  22. package/js/src/sources/telegram-history.js +12 -0
  23. package/js/src/sources/telegram-pages.js +37 -10
  24. package/js/src/storage/app-directory.js +78 -0
  25. package/js/src/web/app.css +112 -1
  26. package/js/src/web/app.min.js +87 -87
  27. package/js/src/web/chat-calls.js +23 -9
  28. package/js/src/web/chat-folder-model.js +3 -2
  29. package/js/src/web/chat-folders.js +16 -10
  30. package/js/src/web/chat-format-bar.js +2 -1
  31. package/js/src/web/chat-history-load.js +80 -0
  32. package/js/src/web/chat-history-status.js +94 -0
  33. package/js/src/web/chat-profile.js +8 -3
  34. package/js/src/web/chat-sidebar.js +61 -9
  35. package/js/src/web/chat-state.js +37 -32
  36. package/js/src/web/chat-tab-scroll.js +40 -0
  37. package/js/src/web/chat-view.js +46 -16
  38. package/js/src/web/chat-window.js +95 -74
  39. package/js/src/web/client-log.js +8 -2
  40. package/js/src/web/connection-guides.js +8 -0
  41. package/js/src/web/icons.js +8 -0
  42. package/js/src/web/locales/en-chat.js +14 -1
  43. package/js/src/web/locales/en.js +9 -0
  44. package/js/src/web/locales/hi-chat.js +14 -1
  45. package/js/src/web/locales/hi.js +10 -0
  46. package/js/src/web/locales/ru-chat.js +13 -1
  47. package/js/src/web/locales/ru.js +10 -0
  48. package/js/src/web/locales/zh-chat.js +11 -1
  49. package/js/src/web/locales/zh.js +9 -0
  50. package/js/src/web/nav-items.js +1 -0
  51. package/js/src/web/panel-resize.js +95 -0
  52. package/js/src/web/peer-avatar.js +9 -2
  53. package/js/src/web/person-panel.js +11 -0
  54. package/js/src/web/settings-view.js +19 -0
  55. package/js/src/web/shell/app-shell.js +12 -1
  56. package/js/src/web/views.js +28 -1
  57. package/package.json +3 -1
package/README.hi.md CHANGED
@@ -61,6 +61,8 @@ Default UI hosted app है: `serve` उसका पता छापता ह
61
61
 
62
62
  macOS पर interactive `serve` पूरा setup करता है: इस installation की TLS key को Secure Enclave से protect करता है, certificate trust जाँचता है, जरूरत पर ही trust माँगता है और GitHub Pages app खोलता है। Headless launch: `node js/bin/meta-sovereign.js serve --trust=true --open=true`। `--trust=false` और `--open=false` से opt out करें। Swift helper को macOS Command Line Tools चाहिए; unsupported Mac पर `--hardware-key=false` स्पष्ट रूप से file key चुनता है। Hardware protection disk की key पर लागू है; Node key को memory में उपयोग करता है। नियम और सीमाएँ: [local server setup और security](docs/LOCAL-SERVER-SECURITY.md)।
63
63
 
64
+ खुली चैट पहले लोड होती है और लोडिंग या पुनःप्रयास की स्थिति दिखाती है। सभी / निजी / समूह / चैनल फ़िल्टर हर Telegram फ़ोल्डर में उपलब्ध हैं। कॉलम की चौड़ाई ऊर्ध्वाधर डिवाइडर खींचकर बदलें; आकार याद रखे जाते हैं। कॉल मुख्य मेनू में चैट के बाद है और Telegram खाता संपादन प्रोफ़ाइल में है। डार्क थीम में नियंत्रण सफ़ेद रेखा आइकन इस्तेमाल करते हैं। JS सर्वर OS उपयोगकर्ता की ऐप फ़ोल्डर में डेटा रखता है: macOS में `~/Library/Application Support/Meta Sovereign/store`, Windows में `%APPDATA%/Meta Sovereign/store` और Linux में `${XDG_CONFIG_HOME:-~/.config}/Meta Sovereign/store`। पुराने checkout से `serve` चलाने पर `.meta-sovereign` एक बार स्थानांतरित होता है; संदेश, संपर्क, सत्र, मीडिया और प्रमाणपत्र सुरक्षित रहते हैं तथा लिंक पुराने पथ बनाए रखता है। रीस्टार्ट और अलग कार्य निर्देशिका में वही स्टोर इस्तेमाल होता है। `--store` या `META_SOVEREIGN_STORE_DIR` से बदलें। [चैट और डेटा संग्रहण](docs/CHAT-READINESS.md) देखें।
65
+
64
66
  ## SPA को server से जोड़ें
65
67
 
66
68
  SPA server को इस क्रम में चुनता है ([`js/src/web/discover.js`](js/src/web/discover.js)):
@@ -81,6 +83,7 @@ SPA server को इस क्रम में चुनता है ([`js/src
81
83
  - **Telegram के अपने ऐप जैसी चैट**: हर संदेश एक बार भेजा जाता है, डिलीवरी टिक, लाइव अपडेट और अंतराल के बाद catch-up, पढ़ा-चिह्न, cloud ड्राफ़्ट, reload के बाद भी बचे रहने वाले retry ([`docs/CHAT-REFERENCES.hi.md`](docs/CHAT-REFERENCES.hi.md))।
82
84
  - **CRM**: किसी संपर्क के बारे में सब कुछ एक दृश्य में, अलग-अलग नेटवर्क पर वही व्यक्ति, communities और सदस्यताएँ, खोज, नियम से ऑडियंस (`friends:vk & chat:vk:… - list:blocked`)।
83
85
  - **संपर्क आयात**: macOS Contacts (सिंक किए गए iCloud खातों सहित) या किसी भी प्लेटफ़ॉर्म के vCard निर्यात का पूर्वावलोकन करके आयात करें। दोबारा आयात करने पर रिकॉर्ड की पहचान बनी रहती है। निर्देशिका सिंक के दौरान अपडेट होती है और बिना संदेश वाले लोग भी दिखाती है। [संपर्क आयात](docs/CONTACTS-IMPORT.md) देखें।
86
+
84
87
  - **Outreach, broadcast और ऑटोमेशन**: किसे, कब, किन शर्तों पर और किस पाठ से लिखना है; कौन क्या पाएगा इसका preview; डिफ़ॉल्ट रूप से सुरक्षित ड्राफ़्ट, या quiet hours, हर चैट का cooldown, दैनिक सीमा और opt-out सूची के साथ धीमी गति से स्वचालित भेजना। हर स्वचालित भेजना एक ही outbox से जाता है: अधिक से अधिक एक बार, और अस्पष्ट परिणाम अपने आप दोहराया नहीं जाता ([`docs/USER-GUIDE.hi.md`](docs/USER-GUIDE.hi.md), §11)।
85
88
  - **Operator कतार और उत्तर**: अपठित संदेश चैट-दर-चैट, आपके पिछले उत्तरों से सुझाव (Tab एक चुनता है), पैटर्न और उत्तर के रूप, मिलते संदेश पर auto-reply।
86
89
  - **व्यवसाय और community खाते**: VK community token Bots Long Poll चलाता है; Telegram Business के quick replies, स्वागत और अनुपस्थिति संदेश, कार्य-समय और chatbots।
package/README.md CHANGED
@@ -61,6 +61,8 @@ The hosted app is the default UI: `serve` prints its address, <https://link-foun
61
61
 
62
62
  On macOS, interactive `serve` now handles the full setup: it protects this installation’s TLS key with Secure Enclave, detects existing certificate trust, requests trust only when needed, and opens the GitHub Pages app. For a headless launch use `node js/bin/meta-sovereign.js serve --trust=true --open=true`. `--trust=false` and `--open=false` opt out. The Swift helper needs the macOS Command Line Tools; `--hardware-key=false` explicitly opts into a file key on unsupported Macs. Hardware protection applies to the key on disk; Node uses it in memory. See [local server setup and security](docs/LOCAL-SERVER-SECURITY.md) for the exact access rules and limits.
63
63
 
64
+ Open conversations load first and show loading or retry status until their history is ready. All / Private / Groups / Channels filters remain available inside every Telegram folder. Drag the vertical dividers to resize columns; widths are remembered. Calls is in the main menu after Chat, and Telegram account editing is under Profile. The default dark controls use white line icons. The JS server keeps its store under your OS user: `~/Library/Application Support/Meta Sovereign/store` on macOS, `%APPDATA%/Meta Sovereign/store` on Windows, and `${XDG_CONFIG_HOME:-~/.config}/Meta Sovereign/store` on Linux. Starting `serve` from an older checkout migrates its `.meta-sovereign` store once, preserving messages, contacts, sessions, media and certificate; a compatibility link preserves old paths. Restarts and a different working directory reuse the same store. `--store` or `META_SOVEREIGN_STORE_DIR` override it. See [chat readiness and data storage](docs/CHAT-READINESS.md).
65
+
64
66
  ## Connect the SPA to your server
65
67
 
66
68
  The SPA picks a server in this order ([`js/src/web/discover.js`](js/src/web/discover.js)):
@@ -81,6 +83,7 @@ The full user-facing flow — install nothing, install Rust server, install JS s
81
83
  - **Chat that behaves like Telegram's own apps**: each message sent once, delivery ticks, live updates and catch-up after a gap, read marking, cloud drafts, retries that survive a reload. How the open-source Telegram, Signal and Element clients do it, pinned to the commits we read, is in [`docs/CHAT-REFERENCES.md`](docs/CHAT-REFERENCES.md).
82
84
  - **CRM**: everything known about a contact in one view, the same person across networks, communities and memberships, search, audiences by rule (`friends:vk & chat:vk:… - list:blocked`).
83
85
  - **Contact imports**: preview and import macOS Contacts (including synced iCloud accounts) or vCard exports from any platform; repeated imports keep stable identities. The directory updates during sync and includes people without messages. See [contact imports](docs/CONTACTS-IMPORT.md).
86
+
84
87
  - **Outreach, broadcasts and automations**: who to message, when, on what conditions and with which text; a preview of who gets what; safe drafts by default or paced auto-send with quiet hours, a per-chat cooldown, daily caps and an opt-out list. Every automated send goes through one outbox, so it happens at most once and an unclear result is never retried by itself ([`docs/USER-GUIDE.md` §11](docs/USER-GUIDE.md#11-automations-message-people-by-rule-safely)).
85
88
  - **Operator queue and replies**: walk unread messages chat by chat, reply suggestions from your own past answers (Tab takes one), patterns and reply variations, auto-replies on a matching message.
86
89
  - **Business and community accounts**: a VK community token runs the Bots Long Poll; Telegram Business quick replies, greeting and away messages, hours and chatbots.
package/README.ru.md CHANGED
@@ -61,6 +61,8 @@ node js/bin/meta-sovereign.js serve
61
61
 
62
62
  В macOS интерактивный `serve` выполняет настройку целиком: защищает TLS-ключ этой установки через Secure Enclave, проверяет доверие сертификату, запрашивает его только при необходимости и открывает приложение на GitHub Pages. Без интерактивного терминала: `node js/bin/meta-sovereign.js serve --trust=true --open=true`. `--trust=false` и `--open=false` отключают эти действия. Для Swift-помощника нужны macOS Command Line Tools; `--hardware-key=false` явно выбирает файловый ключ на неподдерживаемом Mac. Аппаратная защита действует на диске; Node использует ключ в памяти. Правила и ограничения: [настройка и безопасность локального сервера](docs/LOCAL-SERVER-SECURITY.md).
63
63
 
64
+ Открытый чат загружается первым и показывает загрузку или ожидание повторной попытки. Фильтры Все / Личные / Группы / Каналы доступны внутри каждой папки Telegram. Вертикальные разделители меняют ширину столбцов; размеры запоминаются. Звонки находятся в главном меню после Чата, редактор аккаунта Telegram — в Профиле. В тёмной теме элементы управления используют белые контурные значки. JS-сервер хранит данные в папке пользователя ОС: `~/Library/Application Support/Meta Sovereign/store` в macOS, `%APPDATA%/Meta Sovereign/store` в Windows и `${XDG_CONFIG_HOME:-~/.config}/Meta Sovereign/store` в Linux. Запуск `serve` из старого checkout однократно переносит `.meta-sovereign`, сохраняя сообщения, контакты, сессии, медиа и сертификат; ссылка сохраняет старые пути. Перезапуск и другой рабочий каталог используют то же хранилище. Переопределение: `--store` или `META_SOVEREIGN_STORE_DIR`. См. [чаты и хранение данных](docs/CHAT-READINESS.md).
65
+
64
66
  ## Подключение SPA к серверу
65
67
 
66
68
  SPA выбирает сервер в таком порядке ([`js/src/web/discover.js`](js/src/web/discover.js)):
@@ -81,6 +83,7 @@ SPA выбирает сервер в таком порядке ([`js/src/web/dis
81
83
  - **Чат, который ведёт себя как приложения Telegram**: каждое сообщение отправляется один раз, галочки доставки, живые обновления и догрузка после разрыва, отметки о прочтении, облачные черновики, повторы, переживающие перезагрузку ([`docs/CHAT-REFERENCES.ru.md`](docs/CHAT-REFERENCES.ru.md)).
82
84
  - **CRM**: всё о контакте в одном окне, один и тот же человек в разных сетях, сообщества и членства, поиск, аудитории по правилам (`friends:vk & chat:vk:… - list:blocked`).
83
85
  - **Импорт контактов**: предпросмотр и импорт из Контактов macOS (включая синхронизированные учётные записи iCloud) или файлов vCard на любой платформе; повторный импорт сохраняет идентичность записей. Список обновляется при синхронизации и включает людей без сообщений. См. [импорт контактов](docs/CONTACTS-IMPORT.md).
86
+
84
87
  - **Рассылки, broadcast и автоматизации**: кому писать, когда, при каких условиях и каким текстом; предпросмотр, кто что получит; по умолчанию безопасные черновики или отправка с паузами, тихими часами, перерывом для каждого чата, дневными лимитами и списком отписавшихся. Каждая автоматическая отправка идёт через один outbox: не больше одного раза, а неясный результат сам не повторяется ([`docs/USER-GUIDE.ru.md`](docs/USER-GUIDE.ru.md), §11).
85
88
  - **Очередь оператора и ответы**: непрочитанные сообщения чат за чатом, подсказки ответов из ваших прошлых ответов (Tab берёт подсказку), паттерны и варианты ответов, автоответы на подходящее сообщение.
86
89
  - **Аккаунты бизнеса и сообществ**: токен сообщества VK запускает Bots Long Poll; Telegram Business: быстрые ответы, приветствие и сообщение об отсутствии, часы работы, чат-боты.
package/README.zh.md CHANGED
@@ -61,6 +61,8 @@ node js/bin/meta-sovereign.js serve
61
61
 
62
62
  在 macOS 交互式终端中,`serve` 会完成整个设置:使用 Secure Enclave 保护本次安装的 TLS 私钥,检测证书是否已受信任,仅在需要时请求信任,并打开 GitHub Pages 应用。无交互终端时可运行 `node js/bin/meta-sovereign.js serve --trust=true --open=true`。`--trust=false` 和 `--open=false` 可关闭相应操作。Swift 辅助程序需要 macOS Command Line Tools;不支持的 Mac 可显式使用 `--hardware-key=false` 选择文件密钥。硬件保护针对磁盘上的密钥;Node 在内存中使用密钥。详见[本地服务器设置与安全](docs/LOCAL-SERVER-SECURITY.md)。
63
63
 
64
+ 打开的聊天优先加载,并显示加载或重试状态。全部 / 私聊 / 群组 / 频道筛选在每个 Telegram 文件夹内都可用。拖动竖直分隔线调整列宽,尺寸会保存。通话位于主菜单聊天之后,Telegram 账户编辑移到个人资料。默认深色控件使用白色线条图标。 JS 服务器将数据保存在操作系统用户的应用文件夹:macOS 为 `~/Library/Application Support/Meta Sovereign/store`,Windows 为 `%APPDATA%/Meta Sovereign/store`,Linux 为 `${XDG_CONFIG_HOME:-~/.config}/Meta Sovereign/store`。从旧 checkout 启动 `serve` 会一次性迁移 `.meta-sovereign`,保留消息、联系人、会话、媒体和证书,并用兼容目录链接保留旧路径。重启或改变工作目录仍使用同一存储。可通过 `--store` 或 `META_SOVEREIGN_STORE_DIR` 覆盖。参见[聊天就绪与数据存储](docs/CHAT-READINESS.md)。
65
+
64
66
  ## 将 SPA 连接到服务器
65
67
 
66
68
  SPA 按以下顺序选择服务器([`js/src/web/discover.js`](js/src/web/discover.js)):
@@ -81,6 +83,7 @@ SPA 按以下顺序选择服务器([`js/src/web/discover.js`](js/src/web/disco
81
83
  - **像 Telegram 官方应用一样的聊天**:每条消息只发送一次,送达对勾,实时更新与断线后补齐,已读标记,云端草稿,刷新后仍保留的重试([`docs/CHAT-REFERENCES.zh.md`](docs/CHAT-REFERENCES.zh.md))。
82
84
  - **CRM**:一个视图看到联系人的全部信息,跨网络识别同一个人,社区与成员关系,搜索,按规则定义受众(`friends:vk & chat:vk:… - list:blocked`)。
83
85
  - **导入联系人**:预览并导入 macOS 通讯录(包括已同步的 iCloud 账户)或任何平台导出的 vCard;重复导入保留记录身份。目录在同步期间自动更新,也包含没有消息的联系人。参见[联系人导入](docs/CONTACTS-IMPORT.md)。
86
+
84
87
  - **外联、群发与自动化**:给谁发、何时发、在什么条件下发、发什么内容;预览谁会收到什么;默认生成安全草稿,或按节奏自动发送,并有安静时段、每个聊天的冷却期、每日上限和退订名单。每次自动发送都经过同一个发件箱:最多发送一次,结果不明时不会自行重试([`docs/USER-GUIDE.zh.md`](docs/USER-GUIDE.zh.md) 第 11 节)。
85
88
  - **Operator 队列与回复**:逐个聊天处理未读消息,根据您以往的回答给出回复建议(按 Tab 采用),模式与回复变体,对匹配消息自动回复。
86
89
  - **企业与社区账号**:VK 社区令牌运行 Bots Long Poll;Telegram Business 的快捷回复、问候与离开消息、营业时间和聊天机器人。
@@ -19,6 +19,7 @@
19
19
  * the chat menu does); with `--test` the conditions are ignored there.
20
20
  */
21
21
 
22
+ import { defaultStoreDir } from '../storage/app-directory.js';
22
23
  import { promises as fs } from 'node:fs';
23
24
  import { createInterface } from 'node:readline/promises';
24
25
  import { getSource } from '../sources/index.js';
@@ -383,7 +384,7 @@ const automationsCmd = async (args, log, deps) => {
383
384
  log(`unknown subcommand "${sub}"\n${AUTOMATION_HELP}`);
384
385
  return 1;
385
386
  }
386
- const store = await deps.openStore(args.store ?? '.meta-sovereign');
387
+ const store = await deps.openStore(args.store ?? defaultStoreDir());
387
388
  const { runner, close } = runnerFor(store, args, deps);
388
389
  try {
389
390
  return await handler({
@@ -1,4 +1,5 @@
1
1
  // Local address-book imports, with the same parser and plan as the app.
2
+ import { defaultStoreDir } from '../storage/app-directory.js';
2
3
  import { promises as fs } from 'node:fs';
3
4
  import { decodeVcard, parseVcard } from '../sources/vcard.js';
4
5
  import { readSystemContacts } from '../sources/system-contacts.js';
@@ -17,7 +18,7 @@ export const createContactCommands = ({ openStore }) => ({
17
18
  log('choose --file=<contacts.vcf> or --system');
18
19
  return 1;
19
20
  }
20
- const storeDir = args.store ?? '.meta-sovereign';
21
+ const storeDir = args.store ?? defaultStoreDir();
21
22
  const store = await openStore(storeDir);
22
23
  try {
23
24
  const rows = system
@@ -11,6 +11,7 @@
11
11
  * this module importing the entry point back.
12
12
  */
13
13
 
14
+ import { defaultStoreDir } from '../storage/app-directory.js';
14
15
  import {
15
16
  compareAllCvs,
16
17
  cvPlatformCatalogue,
@@ -147,7 +148,7 @@ const planCmd = async (args, log) => {
147
148
  };
148
149
 
149
150
  const readCmd = async (args, log, openStore, injected) => {
150
- const store = await openStore(args.store ?? '.meta-sovereign');
151
+ const store = await openStore(args.store ?? defaultStoreDir());
151
152
  const ids = parsePlatforms(args.platforms);
152
153
  const { entries, failures } = await readAllCvs(
153
154
  ids,
@@ -168,7 +169,7 @@ const readCmd = async (args, log, openStore, injected) => {
168
169
  };
169
170
 
170
171
  const diffCmd = async (args, log, openStore) => {
171
- const store = await openStore(args.store ?? '.meta-sovereign');
172
+ const store = await openStore(args.store ?? defaultStoreDir());
172
173
  const entries = await loadStoredCvs(store, parsePlatforms(args.platforms));
173
174
  const comparison = compareAllCvs(entries, { prefer: args.prefer ?? null });
174
175
  const diffs = diffAllCvs(entries, null);
@@ -213,7 +214,7 @@ const applyConfirmed = async (preview, { args, log, ask, run }) => {
213
214
  };
214
215
 
215
216
  const syncCmd = async (args, log, openStore, injected) => {
216
- const store = await openStore(args.store ?? '.meta-sovereign');
217
+ const store = await openStore(args.store ?? defaultStoreDir());
217
218
  const ids = parsePlatforms(args.platforms);
218
219
  const run = (dryRun, confirmed = null) =>
219
220
  syncCvAcross(ids, {
@@ -256,7 +257,7 @@ const syncCmd = async (args, log, openStore, injected) => {
256
257
  };
257
258
 
258
259
  const telemetryCmd = async (args, log, openStore) => {
259
- const store = await openStore(args.store ?? '.meta-sovereign');
260
+ const store = await openStore(args.store ?? defaultStoreDir());
260
261
  const events = await loadCvTelemetry(store, {
261
262
  runId: args.run ?? null,
262
263
  platform: args.platform ?? null,
@@ -11,6 +11,7 @@
11
11
  * its own `openStore`, and the tests inject fake source adapters.
12
12
  */
13
13
 
14
+ import { defaultStoreDir } from '../storage/app-directory.js';
14
15
  import { parseList } from './cv-commands.js';
15
16
  import { getSource } from '../sources/index.js';
16
17
  import {
@@ -40,7 +41,7 @@ const optionalNumber = (value) =>
40
41
 
41
42
  const asJson = (value) => JSON.stringify(value, null, 2);
42
43
 
43
- const storeOf = (args, openStore) => openStore(args.store ?? '.meta-sovereign');
44
+ const storeOf = (args, openStore) => openStore(args.store ?? defaultStoreDir());
44
45
 
45
46
  /**
46
47
  * `--stickers=vk:21,facebook:369239263222822` → selection entries. The
@@ -13,6 +13,7 @@
13
13
  * entry point unit-testable.
14
14
  */
15
15
 
16
+ import { defaultStoreDir, resolveUserStore } from '../storage/app-directory.js';
16
17
  import path from 'node:path';
17
18
  import { maybeOpenHostedApp } from './browser-open.js';
18
19
  import { CONTACTS_HELP, createContactCommands } from './contact-commands.js';
@@ -144,14 +145,14 @@ const importCmd = async (args, log) => {
144
145
  const file = args.file;
145
146
  const archive = await fs.readFile(file, 'utf8');
146
147
  const parsed = file.endsWith('.json') ? JSON.parse(archive) : archive;
147
- const store = await openStore(args.store ?? '.meta-sovereign');
148
+ const store = await openStore(args.store ?? defaultStoreDir());
148
149
  const n = await importInto(store, args.source, parsed);
149
150
  log(`imported ${n} messages from ${args.source}`);
150
151
  return 0;
151
152
  };
152
153
 
153
154
  const exportCmd = async (args, log) => {
154
- const store = await openReadOnly(args.store ?? '.meta-sovereign');
155
+ const store = await openReadOnly(args.store ?? defaultStoreDir());
155
156
  const links = await store.query();
156
157
  await fs.writeFile(args.file, JSON.stringify(links, null, 2));
157
158
  log(`exported ${links.length} links to ${args.file}`);
@@ -159,7 +160,7 @@ const exportCmd = async (args, log) => {
159
160
  };
160
161
 
161
162
  const backupCmd = async (args, log) => {
162
- const store = await openReadOnly(args.store ?? '.meta-sovereign');
163
+ const store = await openReadOnly(args.store ?? defaultStoreDir());
163
164
  const file = await createBackup(store, { archiveDir: args.archive });
164
165
  if (args.keep) {
165
166
  await pruneBackups({ archiveDir: args.archive, keep: Number(args.keep) });
@@ -175,7 +176,7 @@ const passcodeCmd = async (args, log) => {
175
176
  log('usage: meta-sovereign passcode --clear [--store=<dir>]');
176
177
  return 1;
177
178
  }
178
- const cleared = clearPasscode(args.store ?? '.meta-sovereign');
179
+ const cleared = clearPasscode(args.store ?? defaultStoreDir());
179
180
  log(cleared ? 'local passcode cleared' : 'no local passcode was set');
180
181
  return 0;
181
182
  };
@@ -185,7 +186,7 @@ const restoreCmd = async (args, log) => {
185
186
  // Swap the whole log in one step; the store must not be in use (R-Z16).
186
187
  const n = await restoreBackupToDir(
187
188
  args.file,
188
- args.store ?? '.meta-sovereign',
189
+ args.store ?? defaultStoreDir(),
189
190
  {
190
191
  replace: true,
191
192
  }
@@ -193,7 +194,7 @@ const restoreCmd = async (args, log) => {
193
194
  log(`replaced the store with ${n} records from ${args.file}`);
194
195
  return 0;
195
196
  }
196
- const store = await openStore(args.store ?? '.meta-sovereign');
197
+ const store = await openStore(args.store ?? defaultStoreDir());
197
198
  const n = await restoreBackup(store, args.file);
198
199
  log(`restored ${n} links from ${args.file}`);
199
200
  return 0;
@@ -235,7 +236,7 @@ const logAddresses = (handle, log) => {
235
236
  };
236
237
 
237
238
  const serveCmd = async (args, log) => {
238
- const storeDir = args.store ?? '.meta-sovereign';
239
+ const storeDir = args.store ?? defaultStoreDir();
239
240
  // Verbose logging is on by default: every request, event and error is
240
241
  // written to a file in the app folder (and echoed here), so the root
241
242
  // cause of any issue is always on disk. `--quiet-logs` opts out.
@@ -313,7 +314,7 @@ const rawEmailTransport = (args, protocol) =>
313
314
  : null;
314
315
 
315
316
  const sourcePullCmd = async (args, log) => {
316
- const store = await openStore(args.store ?? '.meta-sovereign');
317
+ const store = await openStore(args.store ?? defaultStoreDir());
317
318
  const protocol = args.protocol;
318
319
  const result = await pullLiveInto(store, args.source, {
319
320
  protocol,
@@ -384,7 +385,7 @@ const githubLiveFromArgs = (args) =>
384
385
  });
385
386
 
386
387
  const githubCloneCmd = async (args, log) => {
387
- const store = await openStore(args.store ?? '.meta-sovereign');
388
+ const store = await openStore(args.store ?? defaultStoreDir());
388
389
  const live = githubLiveFromArgs(args);
389
390
  const result = await live.cloneRepo({
390
391
  owner: args.owner,
@@ -490,7 +491,7 @@ const peoplePerHourMessageCmd = boardMessageCmd(
490
491
  );
491
492
 
492
493
  const audienceCmd = async (args, log) => {
493
- const store = await openReadOnly(args.store ?? '.meta-sovereign');
494
+ const store = await openReadOnly(args.store ?? defaultStoreDir());
494
495
  const all = await store.query();
495
496
  const matched = evalAudience(all, args.query ?? '');
496
497
  log(JSON.stringify(matched, null, 2));
@@ -498,7 +499,7 @@ const audienceCmd = async (args, log) => {
498
499
  };
499
500
 
500
501
  const factsCmd = async (args, log) => {
501
- const store = await openReadOnly(args.store ?? '.meta-sovereign');
502
+ const store = await openReadOnly(args.store ?? defaultStoreDir());
502
503
  const all = await store.query();
503
504
  const messages = all.filter((l) => l.id?.startsWith('msg:'));
504
505
  const patterns = all
@@ -515,7 +516,7 @@ const factsCmd = async (args, log) => {
515
516
  };
516
517
 
517
518
  const searchCmd = async (args, log) => {
518
- const store = await openReadOnly(args.store ?? '.meta-sovereign');
519
+ const store = await openReadOnly(args.store ?? defaultStoreDir());
519
520
  const results = await localSearch(store, {
520
521
  query: args.query ?? '',
521
522
  min: Number(args.min ?? 0.2),
@@ -525,7 +526,7 @@ const searchCmd = async (args, log) => {
525
526
  };
526
527
 
527
528
  const broadcastCmd = async (args, log) => {
528
- const store = await openStore(args.store ?? '.meta-sovereign');
529
+ const store = await openStore(args.store ?? defaultStoreDir());
529
530
  const networks = (args.networks?.split(',') ?? listSources()).filter((n) =>
530
531
  listSources().includes(n)
531
532
  );
@@ -543,7 +544,7 @@ const broadcastCmd = async (args, log) => {
543
544
  };
544
545
 
545
546
  const listByPrefixCmd = (prefix) => async (args, log) => {
546
- const store = await openReadOnly(args.store ?? '.meta-sovereign');
547
+ const store = await openReadOnly(args.store ?? defaultStoreDir());
547
548
  const all = await store.query();
548
549
  log(
549
550
  JSON.stringify(
@@ -566,7 +567,7 @@ const patternsInferCmd = async (args, log) => {
566
567
  };
567
568
 
568
569
  const graphsRunCmd = async (args, log) => {
569
- const store = await openStore(args.store ?? '.meta-sovereign');
570
+ const store = await openStore(args.store ?? defaultStoreDir());
570
571
  const persisted = await store.get(args.id);
571
572
  if (!persisted) {
572
573
  log(`graph ${args.id} not found`);
@@ -589,7 +590,7 @@ const graphsRunCmd = async (args, log) => {
589
590
  };
590
591
 
591
592
  const profileCmd = async (args, log) => {
592
- const store = await openStore(args.store ?? '.meta-sovereign');
593
+ const store = await openStore(args.store ?? defaultStoreDir());
593
594
  if (args.name || args.bio) {
594
595
  const profile = {
595
596
  id: 'profile:me',
@@ -619,7 +620,7 @@ const profileCmd = async (args, log) => {
619
620
  };
620
621
 
621
622
  const resumeCmd = async (args, log) => {
622
- const store = await openStore(args.store ?? '.meta-sovereign');
623
+ const store = await openStore(args.store ?? defaultStoreDir());
623
624
  if (args.title || args.body) {
624
625
  const resume = {
625
626
  id: 'resume:me',
@@ -648,7 +649,7 @@ const resumeCmd = async (args, log) => {
648
649
  };
649
650
 
650
651
  const syncListenCmd = async (args, log) => {
651
- const store = await openStore(args.store ?? '.meta-sovereign');
652
+ const store = await openStore(args.store ?? defaultStoreDir());
652
653
  const peer = createPeer(store, {
653
654
  node: 'cli-listener',
654
655
  schemaVersion: CURRENT_SCHEMA,
@@ -662,7 +663,7 @@ const syncListenCmd = async (args, log) => {
662
663
  };
663
664
 
664
665
  const syncConnectCmd = async (args, log) => {
665
- const store = await openStore(args.store ?? '.meta-sovereign');
666
+ const store = await openStore(args.store ?? defaultStoreDir());
666
667
  const peer = createPeer(store, {
667
668
  node: 'cli-client',
668
669
  schemaVersion: CURRENT_SCHEMA,
@@ -680,7 +681,7 @@ const syncConnectCmd = async (args, log) => {
680
681
  };
681
682
 
682
683
  const outreachCmd = async (args, log) => {
683
- const store = await openStore(args.store ?? '.meta-sovereign');
684
+ const store = await openStore(args.store ?? defaultStoreDir());
684
685
  const all = await store.query();
685
686
  const matched = evalAudience(all, args.query ?? '');
686
687
  const replyGroup = args.reply
@@ -702,7 +703,7 @@ const exportEncryptedCmd = async (args, log) => {
702
703
  log('export-encrypted: --passphrase is required (R-K13)');
703
704
  return 1;
704
705
  }
705
- const store = await openReadOnly(args.store ?? '.meta-sovereign');
706
+ const store = await openReadOnly(args.store ?? defaultStoreDir());
706
707
  const file = await writeEncryptedExport(store, args.file, {
707
708
  passphrase: args.passphrase,
708
709
  });
@@ -715,7 +716,7 @@ const purgeTombstonesCmd = async (args, log) => {
715
716
  log('purge-tombstones: refusing without --confirm=true (R-K4)');
716
717
  return 1;
717
718
  }
718
- const store = await openStore(args.store ?? '.meta-sovereign');
719
+ const store = await openStore(args.store ?? defaultStoreDir());
719
720
  const idPrefix = args['id-prefix'] ?? null;
720
721
  const olderThan = args['older-than']
721
722
  ? new Date(args['older-than']).toISOString()
@@ -741,7 +742,7 @@ const purgeTombstonesCmd = async (args, log) => {
741
742
  };
742
743
 
743
744
  const vaultFile = (args) =>
744
- args.file ?? path.join(args.store ?? '.meta-sovereign', 'vault.json');
745
+ args.file ?? path.join(args.store ?? defaultStoreDir(), 'vault.json');
745
746
 
746
747
  const vaultInitCmd = async (args, log) => {
747
748
  const file = vaultFile(args);
@@ -860,7 +861,26 @@ export const runCli = async (argv, { log = console.log } = {}) => {
860
861
  return 1;
861
862
  }
862
863
  try {
863
- return await handler(parseArgs(rest), log);
864
+ const args = parseArgs(rest);
865
+ if (
866
+ args.store === undefined &&
867
+ ![
868
+ 'help',
869
+ '--help',
870
+ '-h',
871
+ 'version',
872
+ '--version',
873
+ '-v',
874
+ undefined,
875
+ ].includes(cmd)
876
+ ) {
877
+ args.store =
878
+ ['serve', 'trust'].includes(cmd) &&
879
+ !process.env.META_SOVEREIGN_STORE_DIR
880
+ ? await resolveUserStore({ log })
881
+ : defaultStoreDir();
882
+ }
883
+ return await handler(args, log);
864
884
  } catch (error) {
865
885
  if (error?.code === 'STORE_LOCKED' || SCHEMA_ERROR_CODES.has(error?.code)) {
866
886
  log(error.message); // another process owns the store (R-Z2); the schema (R-Z17)
@@ -10,6 +10,7 @@
10
10
  * `serve`; this command is for doing it on purpose, or looking first.
11
11
  */
12
12
 
13
+ import { defaultStoreDir } from '../storage/app-directory.js';
13
14
  import { openDualStore } from '../storage/dual-store.js';
14
15
  import {
15
16
  MIGRATIONS,
@@ -100,7 +101,7 @@ const migrateCmd = async (dir, log) => {
100
101
 
101
102
  export const createMigrateCommands = () => ({
102
103
  migrate: async (args, log) => {
103
- const dir = args.store ?? '.meta-sovereign';
104
+ const dir = args.store ?? defaultStoreDir();
104
105
  if (args.status) {
105
106
  return statusCmd(dir, log);
106
107
  }
@@ -11,6 +11,7 @@
11
11
  * The runner is injected so the tests never touch a real keychain.
12
12
  */
13
13
 
14
+ import { defaultStoreDir } from '../storage/app-directory.js';
14
15
  import { spawnSync } from 'node:child_process';
15
16
  import os from 'node:os';
16
17
  import path from 'node:path';
@@ -62,7 +63,7 @@ export const setupLocalCertificate = async (args, log, options = {}) => {
62
63
  if (!enabled(args.tls) || process.env.MS_TLS === 'off' || !supported) {
63
64
  return null;
64
65
  }
65
- const storeDir = args.store ?? '.meta-sovereign';
66
+ const storeDir = args.store ?? defaultStoreDir();
66
67
  const protector =
67
68
  enabled(args['hardware-key']) && platform === 'darwin'
68
69
  ? await deviceProtector(storeDir)
@@ -128,7 +129,7 @@ export const createTrustCommands = ({
128
129
  } = {}) => ({
129
130
  trust: async (args, log) => {
130
131
  const remove = String(args.remove ?? 'false') !== 'false';
131
- const { certPath } = await loadLocalTls(args.store ?? '.meta-sovereign');
132
+ const { certPath } = await loadLocalTls(args.store ?? defaultStoreDir());
132
133
  if (platform !== 'darwin') {
133
134
  log(manualSteps(certPath));
134
135
  return 0;
@@ -15,13 +15,53 @@
15
15
 
16
16
  import os from 'node:os';
17
17
  import path from 'node:path';
18
+ import { promises as fs } from 'node:fs';
19
+ import { defaultStoreDir } from '../storage/app-directory.js';
18
20
 
19
21
  /** Packages a live run needs, in the order we report them. */
20
22
  export const BROWSER_PACKAGES = ['browser-commander', 'playwright'];
21
23
 
22
24
  /** Default persistent profile directory: sessions must survive runs. */
23
25
  export const defaultProfileDir = (platform = 'shared') =>
24
- path.join(os.homedir(), '.meta-sovereign', 'browser-profiles', platform);
26
+ path.join(defaultStoreDir(), 'browser-profiles', platform);
27
+
28
+ const persistentProfile = async (platform) => {
29
+ const target = defaultProfileDir(platform);
30
+ const legacy = path.join(
31
+ os.homedir(),
32
+ '.meta-sovereign',
33
+ 'browser-profiles',
34
+ platform
35
+ );
36
+ const exists = (file) =>
37
+ fs.lstat(file).then(
38
+ () => true,
39
+ () => false
40
+ );
41
+ if (!(await exists(target)) && (await exists(legacy))) {
42
+ if (await exists(path.join(legacy, 'SingletonLock'))) {
43
+ throw Object.assign(
44
+ new Error(
45
+ 'Close the existing CV browser before migrating its profile.'
46
+ ),
47
+ { code: 'browser-profile-in-use' }
48
+ );
49
+ }
50
+ await fs.mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
51
+ await fs.rename(legacy, target);
52
+ try {
53
+ await fs.symlink(
54
+ target,
55
+ legacy,
56
+ process.platform === 'win32' ? 'junction' : 'dir'
57
+ );
58
+ } catch (error) {
59
+ await fs.rename(target, legacy);
60
+ throw error;
61
+ }
62
+ }
63
+ return target;
64
+ };
25
65
 
26
66
  const defaultImporters = {
27
67
  'browser-commander': () => import('browser-commander'),
@@ -137,7 +177,7 @@ export const openBrowserSession = async ({
137
177
  return launchWithCommander({
138
178
  bc,
139
179
  headless,
140
- profileDir: profileDir ?? defaultProfileDir(platform),
180
+ profileDir: profileDir ?? (await persistentProfile(platform)),
141
181
  slowMo,
142
182
  locale,
143
183
  verbose,
@@ -140,6 +140,17 @@ export const classifyOrigin = (req, allowed) => {
140
140
  return allowed.includes(origin) ? 'allowed' : 'denied';
141
141
  };
142
142
 
143
+ /** Safe metadata for refused requests, without URLs, cookies or tokens. */
144
+ export const corsContext = (req) => ({
145
+ origin: toOrigin(req.headers?.origin) ?? null,
146
+ transport: req.socket?.encrypted ? 'https' : 'http',
147
+ fetchSite: ['same-origin', 'same-site', 'cross-site', 'none'].includes(
148
+ req.headers?.['sec-fetch-site']
149
+ )
150
+ ? req.headers['sec-fetch-site']
151
+ : null,
152
+ });
153
+
143
154
  const writeEmpty = (res, status) => {
144
155
  res.writeHead(status, { 'content-length': '0' });
145
156
  res.end();
@@ -119,6 +119,7 @@ import { createLearnedRoute } from './routes-learned.js';
119
119
  import { createGitHubCheck } from '../sources/github-check.js';
120
120
  import { handleMetrics } from './metrics.js';
121
121
  import { jsonLog } from './log.js';
122
+ import { defaultStoreDir } from '../storage/app-directory.js';
122
123
  import { listenLocal } from './local-tls.js';
123
124
  import { handlePullRoutes, pullContext } from './routes-pull.js';
124
125
  import { createPeer, attachSyncWebSocket } from '../sync/index.js';
@@ -126,6 +127,7 @@ import { attachSignaling } from '../sync/webrtc-signaling.js';
126
127
  import { registerDefaultHandlers } from './handlers-bootstrap.js';
127
128
  import {
128
129
  classifyOrigin,
130
+ corsContext,
129
131
  handleCors,
130
132
  hostAllowed,
131
133
  parseAllowedOrigins,
@@ -710,6 +712,7 @@ const createHttpServer = (store, ctx, log) =>
710
712
  method: req.method,
711
713
  path: (req.url ?? '').split('?')[0],
712
714
  status: res.statusCode,
715
+ ...(res.statusCode === 403 ? corsContext(req) : {}),
713
716
  duration_ms: Date.now() - start,
714
717
  });
715
718
  });
@@ -813,7 +816,7 @@ const automationFor = (store, automations, log, telegramAccount, later) =>
813
816
 
814
817
  export const startServer = async ({
815
818
  port = 0,
816
- storeDir = '.meta-sovereign',
819
+ storeDir = defaultStoreDir(),
817
820
  archiveDir,
818
821
  store: providedStore,
819
822
  enableHandlers = true,
@@ -19,6 +19,7 @@
19
19
  import { createWriteStream, openSync, statSync, unlinkSync } from 'node:fs';
20
20
  import { chmod, mkdir } from 'node:fs/promises';
21
21
  import path from 'node:path';
22
+ import { defaultStoreDir } from '../storage/app-directory.js';
22
23
  import { redact } from './redact.js';
23
24
  import { renameDurableSync } from '../storage/durable.js';
24
25
 
@@ -138,7 +139,7 @@ export const createServerLog = async ({
138
139
  keep = KEEP_FILES,
139
140
  stdout = (line) => process.stdout.write(line),
140
141
  } = {}) => {
141
- const dir = path.join(storeDir ?? '.meta-sovereign', 'logs');
142
+ const dir = path.join(storeDir ?? defaultStoreDir(), 'logs');
142
143
  await mkdir(dir, { recursive: true, mode: 0o700 });
143
144
  await chmod(dir, 0o700).catch(() => {});
144
145
  const file = path.join(dir, 'meta-sovereign.log');
@@ -194,6 +194,7 @@
194
194
  */
195
195
 
196
196
  import { classifyOrigin } from './cors.js';
197
+ import { telegramRouteError } from './telegram-route-error.js';
197
198
  import { bodyFailure, json, readBody } from './util.js';
198
199
  import { isTrustedMutation } from './routes-auto-import.js';
199
200
  import { DIALOG_ACTIONS } from '../sources/telegram-dialogs.js';
@@ -1482,18 +1483,6 @@ export const handleTelegramRoutes = async (req, res, p, ctx) => {
1482
1483
  if (bodyFailure(error)) {
1483
1484
  throw error; // a bad request body: 400/413, not a 502 (R-Z1)
1484
1485
  }
1485
- ctx.log?.({
1486
- level: 'error',
1487
- event: 'telegram_route_failed',
1488
- path: p,
1489
- error: error.message,
1490
- });
1491
- return (
1492
- json(res, 502, {
1493
- error: error.message,
1494
- code: error.code,
1495
- ...(error.retryAfter ? { retryAfter: error.retryAfter } : {}),
1496
- }) ?? true
1497
- );
1486
+ return telegramRouteError(res, p, error, ctx.log);
1498
1487
  }
1499
1488
  };