From c7d44f5b7bc875f52dd14c7e76dd59617f5a4f6a Mon Sep 17 00:00:00 2001 From: lundog <8041501+lundog@users.noreply.github.com> Date: Thu, 2 Jul 2026 03:53:50 -0600 Subject: [PATCH 1/6] Bundle simplex-chat v6.5.5 and rework file exchange (0.3.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump the bundled image to lundog/simplex-chat:6.5.5, which raises websocat's WebSocket message-size limit so large SimpleX events (e.g. a new contact connecting) are no longer split into invalid frames — this was causing JSON parse errors on the client during connect. - Rework the file-exchange mounts for the new image layout. Received files and tmp now live under /data/.simplex (covered by the single /data mount), so the inbound contract is the .simplex/files subpath: name-only reporting, mounted read-only, resolved by the consumer against its own path. Outbound keeps a neutral verbatim mount, now .simplex/outbound -> /tmp/simplex-outbound. - Update the README file-exchange contract to match the new layout. - Version 0.3.0:0. No data migration: the SimpleX profile DB and API keys keep their paths under .simplex/; any files left in the old .simplex/media tree are orphaned but harmless. --- README.md | 31 +++++++++++----------- startos/manifest/index.ts | 2 +- startos/utils.ts | 39 +++++++++++++++------------- startos/versions/current.ts | 51 ++++++++++++++++++------------------- startos/versions/index.ts | 3 ++- startos/versions/v0_2_0.ts | 51 +++++++++++++++++++++++++++++++++++++ 6 files changed, 115 insertions(+), 62 deletions(-) create mode 100644 startos/versions/v0_2_0.ts diff --git a/README.md b/README.md index 9d1065e..12ec51f 100644 --- a/README.md +++ b/README.md @@ -45,28 +45,27 @@ The image entrypoint starts `simplex-chat` in headless server/bot mode on a loca ## Volume and Data Layout -| Volume | Mount Point | Purpose | -| ------ | ----------- | ------------------------------------------------------------------- | -| `main` | `/data` | HOME — the `.simplex` profile database, plus `store.json` (API keys) | -| `main` | `/simplex` | The `.simplex/media` subtree, re-mounted for the file-exchange contract | +| Volume | Mount Point | Purpose | +| ------ | ------------------ | ----------------------------------------------------------------------------- | +| `main` | `/data` | HOME — the `.simplex` profile database and file dirs, plus `store.json` (keys) | +| `main` | `/tmp/simplex-outbound`| The `.simplex/outbound` subpath, re-mounted at a neutral path for outbound sends | -The SimpleX client's own profile (identity, contacts, chat history) is the source of truth for chat state; there is no separate package-level chat config. A small `store.json` at the volume root holds the bridge's API keys. +The SimpleX client's own profile (identity, contacts, chat history) is the source of truth for chat state; there is no separate package-level chat config. A small `store.json` at the volume root holds the bridge's API keys. All SimpleX file dirs live under `/data/.simplex` — `files` (received, `--files-folder`), `tmp` (`--temp-folder`), and `outbound` (consumer-written) — as siblings on one filesystem, so simplex-chat's atomic `tmp` → `files` rename can't fail with `EXDEV`. ### File exchange contract (for consumer packages) -The Websocket carries only a small inline preview for image/video messages — actual file bytes (documents, voice, full-resolution media) live on disk. So other StartOS packages exchange files with the bridge by mounting subpaths of this package's `main` volume via dependency mounts (`mountDependency`), declared as an **optional** dependency so it stays opt-in. The volume's `.simplex/media` subtree is published at a neutral `/simplex` prefix as a **single mount** containing: +The Websocket carries only a small inline preview for image/video messages — actual file bytes (documents, voice, full-resolution media) live on disk. So other StartOS packages exchange files with the bridge by mounting subpaths of this package's `main` volume via dependency mounts (`mountDependency`), declared as an **optional** dependency so it stays opt-in. The two directions are handled differently: -| Volume subpath | Container path | Access for consumers | Purpose | -| ------------------------ | ------------------ | -------------------- | --------------------------------------------- | -| `.simplex/media/inbound` | `/simplex/inbound` | read-only | Files received by the bridge (`--files-folder`) | -| `.simplex/media/tmp` | `/simplex/tmp` | read-only (optional) | In-progress transfers (`--temp-folder`) | -| `.simplex/media/outbound`| `/simplex/outbound`| read-write | Consumer-written files for the bridge to send | +| Direction | Volume subpath | Consumer mounts at | Access | Purpose | +| --------- | -------------- | ------------------ | ------ | ------- | +| Inbound | `.simplex/files` | *any path* (its own choice) | read-only | Files received by the bridge (`--files-folder`) | +| Outbound | `.simplex/outbound` | `/tmp/simplex-outbound` (verbatim) | read-write | Consumer-written files for the bridge to send | -**Why a single mount:** `simplex-chat` finishes a download with an atomic `rename(2)` from `tmp` into `inbound`. Separate bind mounts would make that rename cross filesystems and fail with `EXDEV`, stranding the payload — so on the bridge side they must be siblings within one mount. Consumers are unaffected and may mount `inbound` and `outbound` individually. +**Inbound** is loose: the Websocket API reports a received file by *name only*, which the consumer resolves against its own view of the `files` dir — so the two sides only need to share that one host directory; the mountpoint can be anything the consumer likes. -**Paths:** a consumer that mounts these subpaths at the *same mountpoints* can use the paths verbatim. This is strictly required only for **outbound** — that path travels over the Websocket and is resolved inside the bridge's container, so it must be valid there. **Inbound** is looser: the Websocket API reports only a filename, which the consumer resolves against its own inbound directory, so the two sides need only share the same host directory. The neutral `/simplex` prefix (rather than `/data/...`) lets consumers mount at identical paths without colliding with their own `/data` volume. +**Outbound** requires a matching path: on send the consumer passes a file path that simplex-chat resolves inside *this* container, so it must be valid here. The bridge re-mounts the `.simplex/outbound` subpath at a neutral `/tmp/simplex-outbound`; a consumer mounting the same subpath at `/tmp/simplex-outbound` can then pass `/tmp/simplex-outbound/...` paths verbatim, with no `/data` collision (the neutral prefix avoids clashing with the consumer's own `/data` volume). This is the same neutral path the standalone Docker image documents, so a consumer's outbound folder is identical in both deployments. `outbound` is not renamed across filesystems, so it needs no co-location with `tmp`. -**Security:** consumers mount only the `media/*` subpaths — never the whole `main` volume or `.simplex/` itself, which hold the SimpleX profile database and keys. `inbound` is read-only so consumers can't alter received files; write access is limited to `outbound`. +**Security:** consumers mount only the `.simplex/files` and `.simplex/outbound` subpaths — never the whole `main` volume, `.simplex/` itself, or the profile database and keys it holds. `files` is read-only so consumers can't alter received files; write access is limited to `outbound`. --- @@ -168,8 +167,8 @@ ports: ws: 5225 auth: bearer token (Authorization: Bearer ); same-box dependents bypass via container bridge IP file_exchange: - inbound: /simplex/inbound (read-only for consumers) - outbound: /simplex/outbound (read-write for consumers) + inbound: mount subpath .simplex/files read-only at any path; WS reports file name only + outbound: mount subpath .simplex/outbound read-write at /tmp/simplex-outbound (pass paths verbatim) health_checks: - websocket dependencies: none diff --git a/startos/manifest/index.ts b/startos/manifest/index.ts index 39466b7..0746511 100644 --- a/startos/manifest/index.ts +++ b/startos/manifest/index.ts @@ -19,7 +19,7 @@ export const manifest = setupManifest({ // Bump this tag deliberately when the upstream SimpleX version changes. simplex: { source: { - dockerTag: 'lundog/simplex-chat:6.5.4', + dockerTag: 'lundog/simplex-chat:6.5.5', }, arch: ['x86_64', 'aarch64'], }, diff --git a/startos/utils.ts b/startos/utils.ts index 86721f9..308b622 100644 --- a/startos/utils.ts +++ b/startos/utils.ts @@ -3,24 +3,27 @@ import { sdk } from './sdk' export const port = 5225 /** - * The `main` volume is mounted wholly at /data (HOME, profile database). + * The `main` volume is mounted wholly at /data (HOME). Everything SimpleX lives + * under /data/.simplex: the profile database and `store.json`, plus the image's + * file dirs — `files` (received, `--files-folder`), `tmp` (`--temp-folder`), and + * `outbound` (consumer-written, for the bridge to send). They are all siblings + * on one filesystem, so simplex-chat's atomic tmp->files rename can't hit EXDEV. * - * The file exchange contract (see README) re-mounts - * the volume's `.simplex/media` subpath at a neutral /simplex prefix so that - * consumer packages can mount its subdirectories at identical mountpoints - * and use file paths from WS messages verbatim: + * The file exchange contract (see README) is asymmetric: * - * /simplex/inbound (received files, --files-folder) - * /simplex/tmp (in-progress transfers, --temp-folder) - * /simplex/outbound (consumer-written files for outbound sends) - * - * The tree lives under the bot's profile dir (.simplex/media) rather than a - * separate top-level dir, so everything SimpleX-related sits under .simplex/. - * - * This must be a SINGLE mount (not one mount per subdirectory): simplex-chat - * moves completed downloads from the temp folder to the files folder with an - * atomic rename, which fails with EXDEV ("Invalid cross-device link") if the - * two directories are separate bind mounts. + * inbound — the WS reports received files by name only, which a consumer + * resolves against its own view of `.simplex/files`, so no shared + * path is required; the consumer mounts that subpath read-only. + * outbound — on send the consumer passes a path the bridge resolves *here*, so + * it must be valid in this container. We expose only the + * `.simplex/outbound` subpath at a neutral `/tmp/simplex-outbound`, + * so a consumer mounting the same path can pass + * `/tmp/simplex-outbound/...` verbatim without colliding with its + * own /data. The same neutral path is used by the standalone Docker + * image's docs, so a consumer's `outboundFolder` is identical in + * both deployments. The physical bytes still live at the + * `.simplex/outbound` subpath (backed up); `/tmp` is only the + * shared mountpoint, and suits transient staged sends. */ export const mainMounts = sdk.Mounts.of() .mountVolume({ @@ -31,7 +34,7 @@ export const mainMounts = sdk.Mounts.of() }) .mountVolume({ volumeId: 'main', - subpath: '.simplex/media', - mountpoint: '/simplex', + subpath: '.simplex/outbound', + mountpoint: '/tmp/simplex-outbound', readonly: false, }) diff --git a/startos/versions/current.ts b/startos/versions/current.ts index c453471..0c42d69 100644 --- a/startos/versions/current.ts +++ b/startos/versions/current.ts @@ -1,50 +1,49 @@ import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const current = VersionInfo.of({ - version: '0.2.0:0', + version: '0.3.0:0', releaseNotes: { en_US: [ - 'SimpleX Websocket Bridge runs SimpleX Chat headless and exposes the SimpleX network over a token-authenticated Websocket API, so bots, AI agents, scripts, and other StartOS services can send and receive SimpleX messages and files programmatically.', + 'Bundles simplex-chat v6.5.5 and improves connection reliability and the file-exchange contract.', '', - '- Outside access is gated by per-client bearer tokens, managed in the API Keys action; same-box services connect directly.', - '- Actions to configure the SimpleX profile, create one-time invitation links, and reset the identity.', - '- Shared-volume file exchange for consumer packages via /simplex/inbound and /simplex/outbound.', - '- Bundles simplex-chat v6.5.4.', + '- Bundles simplex-chat v6.5.5 (previously v6.5.4).', + '- More reliable connections: large SimpleX events (for example, when a new contact connects) are no longer split into invalid WebSocket frames.', + '- File exchange reworked for consumer packages: received files are shared via the .simplex/files subpath (mounted read-only, resolved by name) and outgoing files via a neutral /tmp/simplex-outbound mount. See the README.', ].join('\n'), es_ES: [ - 'SimpleX Websocket Bridge ejecuta SimpleX Chat sin interfaz y expone la red SimpleX a través de una API Websocket autenticada por token, para que bots, agentes de IA, scripts y otros servicios de StartOS puedan enviar y recibir mensajes y archivos de SimpleX de forma programática.', + 'Incluye simplex-chat v6.5.5 y mejora la fiabilidad de las conexiones y el contrato de intercambio de archivos.', '', - '- El acceso externo se protege con tokens bearer por cliente, gestionados en la acción API Keys; los servicios del mismo equipo se conectan directamente.', - '- Acciones para configurar el perfil de SimpleX, crear enlaces de invitación de un solo uso y restablecer la identidad.', - '- Intercambio de archivos mediante volumen compartido para paquetes consumidores a través de /simplex/inbound y /simplex/outbound.', - '- Incluye simplex-chat v6.5.4.', + '- Incluye simplex-chat v6.5.5 (antes v6.5.4).', + '- Conexiones más fiables: los eventos grandes de SimpleX (por ejemplo, cuando un nuevo contacto se conecta) ya no se dividen en tramas WebSocket no válidas.', + '- Intercambio de archivos rediseñado para paquetes consumidores: los archivos recibidos se comparten mediante la subruta .simplex/files (montada en solo lectura, resuelta por nombre) y los archivos salientes mediante un montaje neutral /tmp/simplex-outbound. Consulte el README.', ].join('\n'), de_DE: [ - 'SimpleX Websocket Bridge betreibt SimpleX Chat im Headless-Modus und stellt das SimpleX-Netzwerk über eine token-authentifizierte Websocket-API bereit, sodass Bots, KI-Agenten, Skripte und andere StartOS-Dienste SimpleX-Nachrichten und -Dateien programmatisch senden und empfangen können.', + 'Enthält simplex-chat v6.5.5 und verbessert die Verbindungszuverlässigkeit sowie den Dateiaustausch-Vertrag.', '', - '- Externer Zugriff wird durch clientspezifische Bearer-Token abgesichert, verwaltet in der Aktion „API Keys“; Dienste auf demselben Gerät verbinden sich direkt.', - '- Aktionen zum Konfigurieren des SimpleX-Profils, zum Erstellen einmaliger Einladungslinks und zum Zurücksetzen der Identität.', - '- Dateiaustausch über ein gemeinsames Volume für konsumierende Pakete via /simplex/inbound und /simplex/outbound.', - '- Enthält simplex-chat v6.5.4.', + '- Enthält simplex-chat v6.5.5 (zuvor v6.5.4).', + '- Zuverlässigere Verbindungen: Große SimpleX-Ereignisse (zum Beispiel, wenn ein neuer Kontakt eine Verbindung herstellt) werden nicht mehr in ungültige WebSocket-Frames aufgeteilt.', + '- Dateiaustausch für konsumierende Pakete überarbeitet: Empfangene Dateien werden über den Unterpfad .simplex/files (schreibgeschützt eingebunden, anhand des Namens aufgelöst) und ausgehende Dateien über eine neutrale Einbindung /tmp/simplex-outbound geteilt. Siehe README.', ].join('\n'), pl_PL: [ - 'SimpleX Websocket Bridge uruchamia SimpleX Chat w trybie bezgłowym i udostępnia sieć SimpleX poprzez uwierzytelniane tokenem API Websocket, dzięki czemu boty, agenci AI, skrypty i inne usługi StartOS mogą programowo wysyłać i odbierać wiadomości oraz pliki SimpleX.', + 'Zawiera simplex-chat v6.5.5 oraz poprawia niezawodność połączeń i kontrakt wymiany plików.', '', - '- Dostęp z zewnątrz jest chroniony tokenami bearer dla każdego klienta, zarządzanymi w akcji „API Keys”; usługi na tym samym urządzeniu łączą się bezpośrednio.', - '- Akcje do konfiguracji profilu SimpleX, tworzenia jednorazowych linków zaproszeń i resetowania tożsamości.', - '- Wymiana plików przez współdzielony wolumin dla pakietów konsumujących poprzez /simplex/inbound i /simplex/outbound.', - '- Zawiera simplex-chat v6.5.4.', + '- Zawiera simplex-chat v6.5.5 (poprzednio v6.5.4).', + '- Bardziej niezawodne połączenia: duże zdarzenia SimpleX (na przykład gdy łączy się nowy kontakt) nie są już dzielone na nieprawidłowe ramki WebSocket.', + '- Przebudowano wymianę plików dla pakietów konsumujących: pliki odebrane są udostępniane przez podścieżkę .simplex/files (montowaną tylko do odczytu, rozwiązywaną po nazwie), a pliki wychodzące przez neutralny montaż /tmp/simplex-outbound. Zobacz README.', ].join('\n'), fr_FR: [ - 'SimpleX Websocket Bridge exécute SimpleX Chat sans interface et expose le réseau SimpleX via une API Websocket authentifiée par jeton, afin que les bots, agents IA, scripts et autres services StartOS puissent envoyer et recevoir des messages et fichiers SimpleX de façon programmatique.', + "Inclut simplex-chat v6.5.5 et améliore la fiabilité des connexions ainsi que le contrat d'échange de fichiers.", '', - "- L'accès externe est protégé par des jetons bearer propres à chaque client, gérés dans l'action « API Keys » ; les services du même appareil se connectent directement.", - "- Des actions pour configurer le profil SimpleX, créer des liens d'invitation à usage unique et réinitialiser l'identité.", - '- Échange de fichiers via un volume partagé pour les paquets consommateurs à travers /simplex/inbound et /simplex/outbound.', - '- Inclut simplex-chat v6.5.4.', + '- Inclut simplex-chat v6.5.5 (auparavant v6.5.4).', + "- Connexions plus fiables : les événements SimpleX volumineux (par exemple lorsqu'un nouveau contact se connecte) ne sont plus fractionnés en trames WebSocket invalides.", + '- Échange de fichiers repensé pour les paquets consommateurs : les fichiers reçus sont partagés via le sous-chemin .simplex/files (monté en lecture seule, résolu par nom) et les fichiers sortants via un montage neutre /tmp/simplex-outbound. Voir le README.', ].join('\n'), }, migrations: { + // No data migration: the SimpleX profile DB and API keys keep their paths + // under .simplex/. Received files move from .simplex/media/inbound to + // .simplex/files (the new image default); any files left in the old + // .simplex/media tree are orphaned but harmless and can be removed manually. up: async ({ effects }) => {}, down: IMPOSSIBLE, }, diff --git a/startos/versions/index.ts b/startos/versions/index.ts index e596b0c..a2974d0 100644 --- a/startos/versions/index.ts +++ b/startos/versions/index.ts @@ -1,7 +1,8 @@ import { VersionGraph } from '@start9labs/start-sdk' import { current } from './current' +import { v0_2_0 } from './v0_2_0' export const versionGraph = VersionGraph.of({ current, - other: [], + other: [v0_2_0], }) diff --git a/startos/versions/v0_2_0.ts b/startos/versions/v0_2_0.ts new file mode 100644 index 0000000..75514a8 --- /dev/null +++ b/startos/versions/v0_2_0.ts @@ -0,0 +1,51 @@ +import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' + +export const v0_2_0 = VersionInfo.of({ + version: '0.2.0:0', + releaseNotes: { + en_US: [ + 'SimpleX Websocket Bridge runs SimpleX Chat headless and exposes the SimpleX network over a token-authenticated Websocket API, so bots, AI agents, scripts, and other StartOS services can send and receive SimpleX messages and files programmatically.', + '', + '- Outside access is gated by per-client bearer tokens, managed in the API Keys action; same-box services connect directly.', + '- Actions to configure the SimpleX profile, create one-time invitation links, and reset the identity.', + '- Shared-volume file exchange for consumer packages via /simplex/inbound and /simplex/outbound.', + '- Bundles simplex-chat v6.5.4.', + ].join('\n'), + es_ES: [ + 'SimpleX Websocket Bridge ejecuta SimpleX Chat sin interfaz y expone la red SimpleX a través de una API Websocket autenticada por token, para que bots, agentes de IA, scripts y otros servicios de StartOS puedan enviar y recibir mensajes y archivos de SimpleX de forma programática.', + '', + '- El acceso externo se protege con tokens bearer por cliente, gestionados en la acción API Keys; los servicios del mismo equipo se conectan directamente.', + '- Acciones para configurar el perfil de SimpleX, crear enlaces de invitación de un solo uso y restablecer la identidad.', + '- Intercambio de archivos mediante volumen compartido para paquetes consumidores a través de /simplex/inbound y /simplex/outbound.', + '- Incluye simplex-chat v6.5.4.', + ].join('\n'), + de_DE: [ + 'SimpleX Websocket Bridge betreibt SimpleX Chat im Headless-Modus und stellt das SimpleX-Netzwerk über eine token-authentifizierte Websocket-API bereit, sodass Bots, KI-Agenten, Skripte und andere StartOS-Dienste SimpleX-Nachrichten und -Dateien programmatisch senden und empfangen können.', + '', + '- Externer Zugriff wird durch clientspezifische Bearer-Token abgesichert, verwaltet in der Aktion „API Keys“; Dienste auf demselben Gerät verbinden sich direkt.', + '- Aktionen zum Konfigurieren des SimpleX-Profils, zum Erstellen einmaliger Einladungslinks und zum Zurücksetzen der Identität.', + '- Dateiaustausch über ein gemeinsames Volume für konsumierende Pakete via /simplex/inbound und /simplex/outbound.', + '- Enthält simplex-chat v6.5.4.', + ].join('\n'), + pl_PL: [ + 'SimpleX Websocket Bridge uruchamia SimpleX Chat w trybie bezgłowym i udostępnia sieć SimpleX poprzez uwierzytelniane tokenem API Websocket, dzięki czemu boty, agenci AI, skrypty i inne usługi StartOS mogą programowo wysyłać i odbierać wiadomości oraz pliki SimpleX.', + '', + '- Dostęp z zewnątrz jest chroniony tokenami bearer dla każdego klienta, zarządzanymi w akcji „API Keys”; usługi na tym samym urządzeniu łączą się bezpośrednio.', + '- Akcje do konfiguracji profilu SimpleX, tworzenia jednorazowych linków zaproszeń i resetowania tożsamości.', + '- Wymiana plików przez współdzielony wolumin dla pakietów konsumujących poprzez /simplex/inbound i /simplex/outbound.', + '- Zawiera simplex-chat v6.5.4.', + ].join('\n'), + fr_FR: [ + 'SimpleX Websocket Bridge exécute SimpleX Chat sans interface et expose le réseau SimpleX via une API Websocket authentifiée par jeton, afin que les bots, agents IA, scripts et autres services StartOS puissent envoyer et recevoir des messages et fichiers SimpleX de façon programmatique.', + '', + "- L'accès externe est protégé par des jetons bearer propres à chaque client, gérés dans l'action « API Keys » ; les services du même appareil se connectent directement.", + "- Des actions pour configurer le profil SimpleX, créer des liens d'invitation à usage unique et réinitialiser l'identité.", + '- Échange de fichiers via un volume partagé pour les paquets consommateurs à travers /simplex/inbound et /simplex/outbound.', + '- Inclut simplex-chat v6.5.4.', + ].join('\n'), + }, + migrations: { + up: async ({ effects }) => {}, + down: IMPOSSIBLE, + }, +}) From 35dad23cd44b22f6b45834708e82288547bd1516 Mon Sep 17 00:00:00 2001 From: lundog <8041501+lundog@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:50:31 -0600 Subject: [PATCH 2/6] Bump dockerTag to renamed image, clarify license of bundled simplex-chat is AGPL-3.0 --- README.md | 16 ++++++++++++++-- UPDATING.md | 12 ++++++------ startos/manifest/index.ts | 10 ++++++---- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 12ec51f..51f7035 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The bundled client is driven over its Websocket using the upstream [SimpleX bot/ | Property | Value | | ------------- | -------------------------------------------------------------- | -| Image | `lundog/simplex-chat` (built from `lundog/simplex-chat-docker`) | +| Image | `lundog/simplex-websocket-bridge` (built from `lundog/simplex-websocket-bridge-docker`) | | Architectures | x86_64, aarch64 | | Command | Image entrypoint — supervises `simplex-chat` and `websocat` | @@ -159,7 +159,7 @@ The bundled `simplex-chat` client behaves exactly as upstream: messages and file ```yaml package_id: simplex-websocket-bridge -image: lundog/simplex-chat +image: lundog/simplex-websocket-bridge architectures: [x86_64, aarch64] volumes: main: /data @@ -179,3 +179,15 @@ actions: - api-keys - reset-profile ``` + +--- + +## License + +This package's own code is **MIT**. The bundled container image runs +`simplex-chat`, which is **AGPL-3.0-only**, unmodified — so the distributed +package is an aggregate: `SPDX-License-Identifier: MIT AND AGPL-3.0-only`. The +AGPL applies to the simplex-chat component (including the network-use provision, +AGPL §13); its corresponding source is the upstream repository +() at the version pinned by the +image. The image also bundles `websocat` (MIT). diff --git a/UPDATING.md b/UPDATING.md index 49fb5cd..8630be4 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -2,8 +2,8 @@ SimpleX Websocket Bridge runs the [SimpleX Chat](https://github.com/simplex-chat/simplex-chat) client headless. The runtime is the standalone container image built from -[lundog/simplex-chat-docker](https://github.com/lundog/simplex-chat-docker) and -published to Docker Hub as `lundog/simplex-chat`. "Upstream" here means the +[lundog/simplex-websocket-bridge-docker](https://github.com/lundog/simplex-websocket-bridge-docker) and +published to Docker Hub as `lundog/simplex-websocket-bridge`. "Upstream" here means the SimpleX Chat release that image bundles; this repo consumes the image via `dockerTag` and does not build it. @@ -17,17 +17,17 @@ SimpleX Chat release that image bundles; this repo consumes the image via The current pin lives in `startos/manifest/index.ts` at `images.simplex.source.dockerTag` (the version after the `:` in - `lundog/simplex-chat:`). + `lundog/simplex-websocket-bridge:`). ## Applying the bump -1. In [lundog/simplex-chat-docker](https://github.com/lundog/simplex-chat-docker), +1. In [lundog/simplex-websocket-bridge-docker](https://github.com/lundog/simplex-websocket-bridge-docker), bump the pinned simplex-chat version, then build and publish a matching - multi-arch (amd64 + arm64) tag to `lundog/simplex-chat`. The Dockerfile, + multi-arch (amd64 + arm64) tag to `lundog/simplex-websocket-bridge`. The Dockerfile, entrypoint supervisor, and the `--files-folder`/`--temp-folder` flags all live there, not in this repo. 2. Bump `dockerTag` in `startos/manifest/index.ts` to - `lundog/simplex-chat:` (drop the leading `v` from the release tag). + `lundog/simplex-websocket-bridge:` (drop the leading `v` from the release tag). 3. Bump the package `version` and update `releaseNotes` in `startos/versions/current.ts`. Add a migration only if the new version needs one (see the [packaging guide on versions](https://docs.start9.com/packaging)). diff --git a/startos/manifest/index.ts b/startos/manifest/index.ts index 0746511..35aa77e 100644 --- a/startos/manifest/index.ts +++ b/startos/manifest/index.ts @@ -4,7 +4,9 @@ import { long, short } from './i18n' export const manifest = setupManifest({ id: 'simplex-websocket-bridge', title: 'SimpleX Websocket Bridge', - license: 'MIT', + // The package's own code is MIT; the bundled image ships simplex-chat + // (AGPL-3.0) unmodified, so the distributed package is an aggregate. + license: 'MIT AND AGPL-3.0-only', packageRepo: 'https://github.com/Start9-Community/simplex-websocket-bridge-startos', upstreamRepo: 'https://github.com/simplex-chat/simplex-chat', @@ -15,11 +17,11 @@ export const manifest = setupManifest({ volumes: ['main'], images: { // Consume the standalone container image published from - // github.com/lundog/simplex-chat-docker, rather than building locally. - // Bump this tag deliberately when the upstream SimpleX version changes. + // github.com/lundog/simplex-websocket-bridge-docker, rather than building + // locally. Pinned to an exact revision; bump deliberately. simplex: { source: { - dockerTag: 'lundog/simplex-chat:6.5.5', + dockerTag: 'lundog/simplex-websocket-bridge:6.5.5-2', }, arch: ['x86_64', 'aarch64'], }, From d37620c7f8213eef8352e6a3225d4787f39fdd51 Mon Sep 17 00:00:00 2001 From: lundog <8041501+lundog@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:06:56 -0600 Subject: [PATCH 3/6] Remove 2nd mountVolume now that the plugin can rewrite outbound file paths and outbound files can be referenced by their path in /data --- README.md | 17 ++++++------ startos/utils.ts | 54 +++++++++++++++---------------------- startos/versions/current.ts | 10 +++---- 3 files changed, 36 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 51f7035..b65c3e7 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,9 @@ The image entrypoint starts `simplex-chat` in headless server/bot mode on a loca ## Volume and Data Layout -| Volume | Mount Point | Purpose | -| ------ | ------------------ | ----------------------------------------------------------------------------- | -| `main` | `/data` | HOME — the `.simplex` profile database and file dirs, plus `store.json` (keys) | -| `main` | `/tmp/simplex-outbound`| The `.simplex/outbound` subpath, re-mounted at a neutral path for outbound sends | +| Volume | Mount Point | Purpose | +| ------ | ----------- | ------------------------------------------------------------------------------ | +| `main` | `/data` | HOME — the `.simplex` profile database and file dirs, plus `store.json` (keys) | The SimpleX client's own profile (identity, contacts, chat history) is the source of truth for chat state; there is no separate package-level chat config. A small `store.json` at the volume root holds the bridge's API keys. All SimpleX file dirs live under `/data/.simplex` — `files` (received, `--files-folder`), `tmp` (`--temp-folder`), and `outbound` (consumer-written) — as siblings on one filesystem, so simplex-chat's atomic `tmp` → `files` rename can't fail with `EXDEV`. @@ -59,11 +58,13 @@ The Websocket carries only a small inline preview for image/video messages — a | Direction | Volume subpath | Consumer mounts at | Access | Purpose | | --------- | -------------- | ------------------ | ------ | ------- | | Inbound | `.simplex/files` | *any path* (its own choice) | read-only | Files received by the bridge (`--files-folder`) | -| Outbound | `.simplex/outbound` | `/tmp/simplex-outbound` (verbatim) | read-write | Consumer-written files for the bridge to send | +| Outbound | `.simplex/outbound` | *any path* (its own choice) | read-write | Consumer-written files for the bridge to send | -**Inbound** is loose: the Websocket API reports a received file by *name only*, which the consumer resolves against its own view of the `files` dir — so the two sides only need to share that one host directory; the mountpoint can be anything the consumer likes. +Both directions mount a subpath at whatever path the consumer likes — the bridge exposes no special/neutral mountpoint of its own (its files all sit under the single `/data` mount at `/data/.simplex/{files,outbound}`). -**Outbound** requires a matching path: on send the consumer passes a file path that simplex-chat resolves inside *this* container, so it must be valid here. The bridge re-mounts the `.simplex/outbound` subpath at a neutral `/tmp/simplex-outbound`; a consumer mounting the same subpath at `/tmp/simplex-outbound` can then pass `/tmp/simplex-outbound/...` paths verbatim, with no `/data` collision (the neutral prefix avoids clashing with the consumer's own `/data` volume). This is the same neutral path the standalone Docker image documents, so a consumer's outbound folder is identical in both deployments. `outbound` is not renamed across filesystems, so it needs no co-location with `tmp`. +**Inbound** is loose: the Websocket API reports a received file by *name only*, which the consumer resolves against its own view of the `files` dir — so the two sides only need to share that one host directory. + +**Outbound**: on send the consumer passes a file path that simplex-chat resolves inside *this* container, so it must be valid here — namely `/data/.simplex/outbound/...`. The consumer stages the file into its own mount of the `.simplex/outbound` subpath, then rewrites the directory prefix to the bridge's path before sending. The openclaw-simplex plugin does this automatically via `connection.outboundFolder` (its mount) + `connection.outboundFolderOnClient` (`/data/.simplex/outbound`), so no shared or verbatim mountpoint is needed. `outbound` is not renamed across filesystems, so it needs no co-location with `tmp`. **Security:** consumers mount only the `.simplex/files` and `.simplex/outbound` subpaths — never the whole `main` volume, `.simplex/` itself, or the profile database and keys it holds. `files` is read-only so consumers can't alter received files; write access is limited to `outbound`. @@ -168,7 +169,7 @@ ports: auth: bearer token (Authorization: Bearer ); same-box dependents bypass via container bridge IP file_exchange: inbound: mount subpath .simplex/files read-only at any path; WS reports file name only - outbound: mount subpath .simplex/outbound read-write at /tmp/simplex-outbound (pass paths verbatim) + outbound: mount subpath .simplex/outbound read-write at any path; pass the bridge path /data/.simplex/outbound/ (translate prefix) health_checks: - websocket dependencies: none diff --git a/startos/utils.ts b/startos/utils.ts index 308b622..0e51cc6 100644 --- a/startos/utils.ts +++ b/startos/utils.ts @@ -3,38 +3,28 @@ import { sdk } from './sdk' export const port = 5225 /** - * The `main` volume is mounted wholly at /data (HOME). Everything SimpleX lives - * under /data/.simplex: the profile database and `store.json`, plus the image's - * file dirs — `files` (received, `--files-folder`), `tmp` (`--temp-folder`), and - * `outbound` (consumer-written, for the bridge to send). They are all siblings - * on one filesystem, so simplex-chat's atomic tmp->files rename can't hit EXDEV. + * A single mount: the `main` volume at /data (HOME). Everything SimpleX lives + * under /data/.simplex — the profile database and `store.json`, plus the image's + * file dirs `files` (received, `--files-folder`), `tmp` (`--temp-folder`), and + * `outbound` (consumer-written, for the bridge to send). All siblings on one + * filesystem, so simplex-chat's atomic tmp->files rename can't hit EXDEV. * - * The file exchange contract (see README) is asymmetric: + * The file exchange contract (see README) needs no second/neutral mount here. + * A consumer package mounts the specific subpaths it needs via `mountDependency` + * at whatever paths it likes: * - * inbound — the WS reports received files by name only, which a consumer - * resolves against its own view of `.simplex/files`, so no shared - * path is required; the consumer mounts that subpath read-only. - * outbound — on send the consumer passes a path the bridge resolves *here*, so - * it must be valid in this container. We expose only the - * `.simplex/outbound` subpath at a neutral `/tmp/simplex-outbound`, - * so a consumer mounting the same path can pass - * `/tmp/simplex-outbound/...` verbatim without colliding with its - * own /data. The same neutral path is used by the standalone Docker - * image's docs, so a consumer's `outboundFolder` is identical in - * both deployments. The physical bytes still live at the - * `.simplex/outbound` subpath (backed up); `/tmp` is only the - * shared mountpoint, and suits transient staged sends. + * inbound — mount `.simplex/files` read-only. The WS reports received files + * by name only, so the consumer resolves them against its own path. + * outbound — mount `.simplex/outbound` read-write. On send the consumer passes + * a path the bridge resolves here (`/data/.simplex/outbound/...`); + * the consumer stages into its own mount and rewrites the prefix to + * that container path (the openclaw-simplex plugin does this via + * connection.outboundFolder + outboundFolderOnClient), so no shared + * or verbatim mountpoint is required. */ -export const mainMounts = sdk.Mounts.of() - .mountVolume({ - volumeId: 'main', - subpath: null, - mountpoint: '/data', - readonly: false, - }) - .mountVolume({ - volumeId: 'main', - subpath: '.simplex/outbound', - mountpoint: '/tmp/simplex-outbound', - readonly: false, - }) +export const mainMounts = sdk.Mounts.of().mountVolume({ + volumeId: 'main', + subpath: null, + mountpoint: '/data', + readonly: false, +}) diff --git a/startos/versions/current.ts b/startos/versions/current.ts index 0c42d69..a28cce5 100644 --- a/startos/versions/current.ts +++ b/startos/versions/current.ts @@ -8,35 +8,35 @@ export const current = VersionInfo.of({ '', '- Bundles simplex-chat v6.5.5 (previously v6.5.4).', '- More reliable connections: large SimpleX events (for example, when a new contact connects) are no longer split into invalid WebSocket frames.', - '- File exchange reworked for consumer packages: received files are shared via the .simplex/files subpath (mounted read-only, resolved by name) and outgoing files via a neutral /tmp/simplex-outbound mount. See the README.', + '- File exchange reworked for consumer packages: received files are shared via the .simplex/files subpath (mounted read-only, resolved by name) and outgoing files via the .simplex/outbound subpath (the consumer stages the file and translates the path). See the README.', ].join('\n'), es_ES: [ 'Incluye simplex-chat v6.5.5 y mejora la fiabilidad de las conexiones y el contrato de intercambio de archivos.', '', '- Incluye simplex-chat v6.5.5 (antes v6.5.4).', '- Conexiones más fiables: los eventos grandes de SimpleX (por ejemplo, cuando un nuevo contacto se conecta) ya no se dividen en tramas WebSocket no válidas.', - '- Intercambio de archivos rediseñado para paquetes consumidores: los archivos recibidos se comparten mediante la subruta .simplex/files (montada en solo lectura, resuelta por nombre) y los archivos salientes mediante un montaje neutral /tmp/simplex-outbound. Consulte el README.', + '- Intercambio de archivos rediseñado para paquetes consumidores: los archivos recibidos se comparten mediante la subruta .simplex/files (montada en solo lectura, resuelta por nombre) y los archivos salientes mediante la subruta .simplex/outbound (el consumidor prepara el archivo y traduce la ruta). Consulte el README.', ].join('\n'), de_DE: [ 'Enthält simplex-chat v6.5.5 und verbessert die Verbindungszuverlässigkeit sowie den Dateiaustausch-Vertrag.', '', '- Enthält simplex-chat v6.5.5 (zuvor v6.5.4).', '- Zuverlässigere Verbindungen: Große SimpleX-Ereignisse (zum Beispiel, wenn ein neuer Kontakt eine Verbindung herstellt) werden nicht mehr in ungültige WebSocket-Frames aufgeteilt.', - '- Dateiaustausch für konsumierende Pakete überarbeitet: Empfangene Dateien werden über den Unterpfad .simplex/files (schreibgeschützt eingebunden, anhand des Namens aufgelöst) und ausgehende Dateien über eine neutrale Einbindung /tmp/simplex-outbound geteilt. Siehe README.', + '- Dateiaustausch für konsumierende Pakete überarbeitet: Empfangene Dateien werden über den Unterpfad .simplex/files (schreibgeschützt eingebunden, anhand des Namens aufgelöst) und ausgehende Dateien über den Unterpfad .simplex/outbound geteilt (der Konsument legt die Datei ab und übersetzt den Pfad). Siehe README.', ].join('\n'), pl_PL: [ 'Zawiera simplex-chat v6.5.5 oraz poprawia niezawodność połączeń i kontrakt wymiany plików.', '', '- Zawiera simplex-chat v6.5.5 (poprzednio v6.5.4).', '- Bardziej niezawodne połączenia: duże zdarzenia SimpleX (na przykład gdy łączy się nowy kontakt) nie są już dzielone na nieprawidłowe ramki WebSocket.', - '- Przebudowano wymianę plików dla pakietów konsumujących: pliki odebrane są udostępniane przez podścieżkę .simplex/files (montowaną tylko do odczytu, rozwiązywaną po nazwie), a pliki wychodzące przez neutralny montaż /tmp/simplex-outbound. Zobacz README.', + '- Przebudowano wymianę plików dla pakietów konsumujących: pliki odebrane są udostępniane przez podścieżkę .simplex/files (montowaną tylko do odczytu, rozwiązywaną po nazwie), a pliki wychodzące przez podścieżkę .simplex/outbound (konsument przygotowuje plik i tłumaczy ścieżkę). Zobacz README.', ].join('\n'), fr_FR: [ "Inclut simplex-chat v6.5.5 et améliore la fiabilité des connexions ainsi que le contrat d'échange de fichiers.", '', '- Inclut simplex-chat v6.5.5 (auparavant v6.5.4).', "- Connexions plus fiables : les événements SimpleX volumineux (par exemple lorsqu'un nouveau contact se connecte) ne sont plus fractionnés en trames WebSocket invalides.", - '- Échange de fichiers repensé pour les paquets consommateurs : les fichiers reçus sont partagés via le sous-chemin .simplex/files (monté en lecture seule, résolu par nom) et les fichiers sortants via un montage neutre /tmp/simplex-outbound. Voir le README.', + '- Échange de fichiers repensé pour les paquets consommateurs : les fichiers reçus sont partagés via le sous-chemin .simplex/files (monté en lecture seule, résolu par nom) et les fichiers sortants via le sous-chemin .simplex/outbound (le consommateur prépare le fichier et traduit le chemin). Voir le README.', ].join('\n'), }, migrations: { From 5b9c6a1497f51179217c60cce3e901306ae9882a Mon Sep 17 00:00:00 2001 From: lundog <8041501+lundog@users.noreply.github.com> Date: Fri, 3 Jul 2026 03:21:46 -0600 Subject: [PATCH 4/6] Add client configuration and address-management actions Turn the bridge from a bare Websocket endpoint into a configurable SimpleX client, driven entirely from the StartOS UI. All settings live in a new client-settings.json (read on start; can be set before first start); profile, address, and relay changes apply live over the Websocket, while file-retention (a container env/janitor setting) applies on restart. Actions: - Configure Client (replaces Configure Bot Profile; id configure-client): display name, full name, profile picture, peer type, auto-accept contact requests, business mode, welcome message, message relays, and received-file cleanup (days). allowedStatuses: any, so it can seed the identity before the first start. - View Address / Reset Address: show or replace the long-lived, reusable contact address (create if missing). Reset Address runs live (Danger Zone, no stop). Create Invitation still covers one-time links; shared link formatting factored into links.ts (connLinkMembers). - Reset Profile renamed to Reset Client (id reset-client), with an expanded warning covering the OpenClaw reused-contact-id caveat and a success message noting contacts must reconnect. Profile picture: accepts an image URL (http/https, fetched with a timeout and size cap), a data URL, or raw base64, then center-crops to a square and shrinks to a <=12KB JPEG (jimp) to fit SimpleX's avatar limit. New avatar.ts; adds jimp dependency. Message relays (public / self-hosted SimpleX Server / custom SMP+XFTP): - Applied over the client's operator-servers API (/_servers GET -> mutate -> SET), not via the container's --server flag. simplex-chat persists relay config in its database and only uses the presets when none are set, so the flag stuck and dropping it never reverted to public. The round-trip enables presets / disables+replaces custom per protocol, so switching (including back to public) is authoritative and reliable. Applied on every (re)start and live on relay change. - Local mode declares an optional dependency on the simplex (SimpleX Server) package and auto-pulls its SMP/XFTP addresses from that package's service interfaces, preferring clearnet domain > clearnet IP > Tor > .local. Reliability: - The start-time settings sync waits for the Websocket to actually answer (waitForBotReady) before syncing, instead of trusting daemon launch ordering, fixing an ECONNREFUSED race against websocat's bind. - main reads settings non-reactively (.once) so writing the settings file no longer re-triggers main and restarts the service mid-edit. Docs & i18n: - README, instructions.md, and the 0.3.0 release notes updated: new actions, the relay model, and a "Switching message relays" section explaining that a relay change affects only new connections (existing contacts/address stay on their original server) and the self-hosting reachability/decommission caveats. - i18n dictionaries updated across all five languages; unused keys pruned. No data migration: client-settings.json is created on demand and store.json (API keys) is unchanged. --- README.md | 62 +- instructions.md | 45 +- package-lock.json | 1007 ++++++++++++++++- package.json | 1 + startos/actions/configure.ts | 176 --- startos/actions/configureClient.ts | 306 +++++ startos/actions/create-invitation.ts | 37 +- startos/actions/index.ts | 12 +- startos/actions/reset-address.ts | 83 ++ .../{reset-profile.ts => reset-client.ts} | 14 +- startos/actions/view-address.ts | 88 ++ startos/avatar.ts | 112 ++ startos/bot-client.ts | 33 + startos/dependencies.ts | 28 +- startos/fileModels/clientSettings.json.ts | 101 ++ startos/i18n/dictionaries/default.ts | 74 +- startos/i18n/dictionaries/translations.ts | 256 ++++- startos/links.ts | 43 + startos/liveSync.ts | 235 ++++ startos/main.ts | 45 + startos/manifest/index.ts | 17 +- startos/serverConfig.ts | 135 +++ startos/versions/current.ts | 50 +- 23 files changed, 2608 insertions(+), 352 deletions(-) delete mode 100644 startos/actions/configure.ts create mode 100644 startos/actions/configureClient.ts create mode 100644 startos/actions/reset-address.ts rename startos/actions/{reset-profile.ts => reset-client.ts} (55%) create mode 100644 startos/actions/view-address.ts create mode 100644 startos/avatar.ts create mode 100644 startos/fileModels/clientSettings.json.ts create mode 100644 startos/links.ts create mode 100644 startos/liveSync.ts create mode 100644 startos/serverConfig.ts diff --git a/README.md b/README.md index b65c3e7..19bf4a9 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,11 @@ The bundled client is driven over its Websocket using the upstream [SimpleX bot/ ## Image and Container Runtime -| Property | Value | -| ------------- | -------------------------------------------------------------- | +| Property | Value | +| ------------- | --------------------------------------------------------------------------------------- | | Image | `lundog/simplex-websocket-bridge` (built from `lundog/simplex-websocket-bridge-docker`) | -| Architectures | x86_64, aarch64 | -| Command | Image entrypoint — supervises `simplex-chat` and `websocat` | +| Architectures | x86_64, aarch64 | +| Command | Image entrypoint — supervises `simplex-chat` and `websocat` | The image entrypoint starts `simplex-chat` in headless server/bot mode on a localhost-only control port, then runs [`websocat`](https://github.com/vi/websocat) as a bridge from a container-wide port to that control port so traffic from outside the container can reach it. The supervisor exits if either child dies, so StartOS restarts the container rather than leaving a half-running service. @@ -55,16 +55,16 @@ The SimpleX client's own profile (identity, contacts, chat history) is the sourc The Websocket carries only a small inline preview for image/video messages — actual file bytes (documents, voice, full-resolution media) live on disk. So other StartOS packages exchange files with the bridge by mounting subpaths of this package's `main` volume via dependency mounts (`mountDependency`), declared as an **optional** dependency so it stays opt-in. The two directions are handled differently: -| Direction | Volume subpath | Consumer mounts at | Access | Purpose | -| --------- | -------------- | ------------------ | ------ | ------- | -| Inbound | `.simplex/files` | *any path* (its own choice) | read-only | Files received by the bridge (`--files-folder`) | -| Outbound | `.simplex/outbound` | *any path* (its own choice) | read-write | Consumer-written files for the bridge to send | +| Direction | Volume subpath | Consumer mounts at | Access | Purpose | +| --------- | ------------------- | --------------------------- | ---------- | ----------------------------------------------- | +| Inbound | `.simplex/files` | _any path_ (its own choice) | read-only | Files received by the bridge (`--files-folder`) | +| Outbound | `.simplex/outbound` | _any path_ (its own choice) | read-write | Consumer-written files for the bridge to send | Both directions mount a subpath at whatever path the consumer likes — the bridge exposes no special/neutral mountpoint of its own (its files all sit under the single `/data` mount at `/data/.simplex/{files,outbound}`). -**Inbound** is loose: the Websocket API reports a received file by *name only*, which the consumer resolves against its own view of the `files` dir — so the two sides only need to share that one host directory. +**Inbound** is loose: the Websocket API reports a received file by _name only_, which the consumer resolves against its own view of the `files` dir — so the two sides only need to share that one host directory. -**Outbound**: on send the consumer passes a file path that simplex-chat resolves inside *this* container, so it must be valid here — namely `/data/.simplex/outbound/...`. The consumer stages the file into its own mount of the `.simplex/outbound` subpath, then rewrites the directory prefix to the bridge's path before sending. The openclaw-simplex plugin does this automatically via `connection.outboundFolder` (its mount) + `connection.outboundFolderOnClient` (`/data/.simplex/outbound`), so no shared or verbatim mountpoint is needed. `outbound` is not renamed across filesystems, so it needs no co-location with `tmp`. +**Outbound**: on send the consumer passes a file path that simplex-chat resolves inside _this_ container, so it must be valid here — namely `/data/.simplex/outbound/...`. The consumer stages the file into its own mount of the `.simplex/outbound` subpath, then rewrites the directory prefix to the bridge's path before sending. The openclaw-simplex plugin does this automatically via `connection.outboundFolder` (its mount) + `connection.outboundFolderOnClient` (`/data/.simplex/outbound`), so no shared or verbatim mountpoint is needed. `outbound` is not renamed across filesystems, so it needs no co-location with `tmp`. **Security:** consumers mount only the `.simplex/files` and `.simplex/outbound` subpaths — never the whole `main` volume, `.simplex/` itself, or the profile database and keys it holds. `files` is read-only so consumers can't alter received files; write access is limited to `outbound`. @@ -80,9 +80,11 @@ Both directions mount a subpath at whatever path the consumer likes — the brid ## Configuration Management -There is no separate StartOS-side config file — the SimpleX profile is the configuration. +Client settings are saved to `client-settings.json` by the **Configure Client** action and applied two ways depending on the field. -- **SimpleX profile** (display name, picture, file sharing) — edited live via the **Configure Bot Profile** action; changes are pushed to the running client over the Websocket with no restart. +- **Profile & address** (display name, full name, picture, peer type, auto-accept contact requests, business mode, welcome message) — pushed to the running client live over the Websocket, no restart. Run the action before first start to seed the identity. +- **Message relays** (public / self-hosted SimpleX Server / custom SMP+XFTP) — applied live over the client's API on save, no restart. SimpleX persists the relay choice in its database and only uses the public presets when none are set, so switching back to public actively resets to the presets rather than relying on removing a flag. Relays are also re-applied on every (re)start so the database stays authoritative. +- **Received-file retention** — a container/janitor setting read at launch, so changing it saves and then restarts the service. - **API keys** — stored in `store.json` and managed via the **API Keys** action. Editing keys re-binds the reverse proxy's accepted-token set with no restart. --- @@ -105,12 +107,14 @@ There is no separate StartOS-side config file — the SimpleX profile is the con ## Actions (StartOS UI) -| Action | Group | Purpose | -| ----------------------- | ----------- | ---------------------------------------------------------------------------------------- | -| **Configure Bot Profile** | General | View and edit the live SimpleX profile — display name, picture, and the file-sharing toggle. Pushed to the running client; no restart. | -| **Create Invitation** | General | Drive the running client to mint a fresh one-time SimpleX invitation link (with QR). One redemption per link. | -| **API Keys** | General | Manage the bearer tokens that gate outside access — add a labeled key per client, delete to revoke. | -| **Reset Profile** | Danger Zone | Wipe the SimpleX identity, contacts, and chat history. Service must be stopped. API keys are preserved. | +| Action | Group | Purpose | +| --------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Configure Client** | General | Set the client identity and behavior — display name, full name, picture, peer type, auto-accept contact requests, business mode, welcome message, message relays (public / self-hosted / custom), and received-file retention. Run before first start. Profile, address, and relay edits apply live; a received-file-retention change restarts the service. | +| **Create Invitation** | General | Drive the running client to mint a fresh one-time SimpleX invitation link (with QR). One redemption per link. | +| **View Address** | General | Show the client's long-lived, reusable SimpleX address (with QR), creating one if it doesn't exist yet. Unlike an invitation, the same address works for any number of contacts. | +| **API Keys** | General | Manage the bearer tokens that gate outside access — add a labeled key per client, delete to revoke. | +| **Reset Client** | Danger Zone | Wipe the SimpleX identity, contacts, and chat history. Service must be stopped. API keys are preserved. Contacts must reconnect afterward; if used with OpenClaw, purge the channel's per-contact state first (SimpleX reuses low contact ids). | +| **Reset Address** | Danger Zone | Replace the long-lived address with a new one (runs live, no stop needed). The old address link stops working; existing contacts are unaffected. | --- @@ -126,9 +130,9 @@ There is no separate StartOS-side config file — the SimpleX profile is the con ## Health Checks -| Check | Method | Messages | -| --------- | ---------------------- | ----------------------------------------------------------------- | -| Websocket | Port listening (5225) | Success: "Websocket is ready" / Error: "Websocket is not ready" | +| Check | Method | Messages | +| --------- | --------------------- | --------------------------------------------------------------- | +| Websocket | Port listening (5225) | Success: "Websocket is ready" / Error: "Websocket is not ready" | The standalone health check has the stable ID `websocket`, which dependent packages reference in a `kind: 'running'` dependency requirement. @@ -136,9 +140,11 @@ The standalone health check has the stable ID `websocket`, which dependent packa ## Dependencies -None. +Optional: **SimpleX Server** (`simplex`). -This package has no dependencies of its own. Other packages may declare an **optional** dependency on it to consume the Websocket API and the file-exchange contract above (referencing the `websocket` health check). Pointing the bridge at a self-hosted SimpleX Server for SMP/XFTP relaying is a planned enhancement, tracked upstream of this README. +The bridge has no hard dependencies. It declares one **optional** dependency on the `simplex` package: when you choose **My self-hosted SimpleX Server** for message relays in the Configure Client action, the bridge flips `simplex` to a required running dependency and auto-pulls its SMP and XFTP addresses (full URIs, fingerprint included) from that package's service interfaces — preferring a clearnet domain, then a clearnet IP, then Tor, then `.local`. Public and Custom relays need no dependency. + +Other packages may in turn declare an optional dependency on the bridge to consume the Websocket API and the file-exchange contract above (referencing the `websocket` health check). --- @@ -172,13 +178,15 @@ file_exchange: outbound: mount subpath .simplex/outbound read-write at any path; pass the bridge path /data/.simplex/outbound/ (translate prefix) health_checks: - websocket -dependencies: none -startos_managed_env_vars: none +dependencies: simplex (optional; required only when relays = self-hosted) +startos_managed_env_vars: PROFILE_DISPLAY_NAME, PROFILE_PEER_TYPE, INBOUND_RETENTION_HOURS (derived from client-settings.json at start; SMP/XFTP relays are applied over the client API on start, not via env) actions: - - configure + - configure-client - create-invitation + - view-address - api-keys - - reset-profile + - reset-client + - reset-address ``` --- diff --git a/instructions.md b/instructions.md index 0a05af0..c3a6597 100644 --- a/instructions.md +++ b/instructions.md @@ -11,7 +11,7 @@ This service has no human chat interface — it is driven entirely by your own s ## What you get on StartOS - A **Websocket API** to the SimpleX network, gated by an API key, that your bots, AI agents, scripts, and other StartOS services can drive. -- **Actions** to manage the SimpleX profile, mint one-time invitation links, and manage API keys. +- **Actions** to configure the client (identity, message relays, file retention), mint one-time invitation links, manage API keys, and reset the client. - A **shared-volume file-exchange contract** other StartOS packages can opt into to send and receive files through the bridge. ## Getting set up @@ -30,22 +30,43 @@ Outside access to the Websocket API is gated by a bearer token at the StartOS re Manage tokens in the **API Keys** action — each has a label (to identify the client) and a generated token. Add one per client; delete one to revoke its access. -## Configuring the profile +## Configuring the client -Open the **Configure Bot Profile** action to view and edit the live profile: +Run the **Configure Client** action to set the client's identity and behavior. It writes a settings file the service reads on start, so you can run it **before the first start** to seed the identity. -- **Display Name** — the name peers see when they connect. -- **Profile Picture** — a base64 data URL; leave empty to remove the current picture. Small images (< 64KB) render best in SimpleX clients. -- **Allow files & media** — whether contacts can exchange files and media with the bridge. +- **Display Name** / **Full Name** — how the client presents to contacts. +- **Profile Picture** — an image URL (http/https), a data URL, or base64; it is cropped to a square and shrunk automatically to fit SimpleX's avatar limit. Leave empty to remove it. +- **Peer Type** — Bot or Human (cosmetic; both transfer files and messages either way). +- **Auto-Accept Contact Requests** — automatically accept incoming requests to the client's address. +- **Business Mode** / **Welcome Message** — present a business address and/or send an auto-reply to new contacts. +- **Message Relays (SMP/XFTP)** — SimpleX's public servers (default), your own self-hosted SimpleX Server, or custom addresses. +- **Cleanup Received Files After (days)** — optionally delete old received files. -Submitting pushes the change to the running client immediately — no restart, and no impact on existing contacts or chats. +Profile, contact-request, and message-relay changes are pushed to the running client immediately, with no restart. Only a received-file-retention change restarts the service (it's read by the container at startup). + +## Switching message relays + +Choosing **SimpleX defaults (public)**, **self-hosted**, or **custom** relays is not a global on/off switch for all traffic — it only changes where **new** connections are created: + +- **New contacts and new invitation/address links** use the selected relays. +- **Existing contacts keep working** over the connections they already have. In SimpleX a message queue lives on the server the _recipient_ chose when the connection was made, and it doesn't move; selecting new relays disables the old ones for _new_ queues but doesn't delete or stop existing ones. Messages are never re-routed between your servers by this setting — a sender only adds its own forwarding relay to hide its IP, still delivering to the queue's original server. +- Your **existing published address** likewise stays on its original server — run **Reset Address** to mint a new one on the current relays. + +When moving to a self-hosted server, keep two things in mind: + +- If you later **shut the old server down**, any contact or address still hosted there stops working; reconnect them (and Reset Address) on the new relays first. +- A self-hosted relay must be **reachable by your contacts and by this client**. A LAN-only/private address only works for peers on that network; to reach outside contacts, publish the server on clearnet or Tor. ## Adding a contact -1. Run the **Create Invitation** action. The bridge mints a fresh one-time link and shows it with a QR code. -2. Send the link or QR to the person you want to invite; they paste it into their SimpleX client. +There are two ways to hand out a connection link: + +- **Create Invitation** mints a fresh one-time link (with QR). Each link can be redeemed by exactly one peer — run it again to invite another person. +- **View Address** shows the client's long-lived, reusable address (with QR), creating one if needed. The same address works for any number of contacts, so it's convenient to publish. To accept incoming requests automatically, enable **Auto-Accept Contact Requests** in Configure Client. -Each link can be redeemed by exactly one peer — run the action again to invite another person. +Send the link or QR to the person you want to connect; they paste it into their SimpleX client. + +If a published address is ever over-shared or abused, run **Reset Address** (Danger Zone) to replace it with a new one — the service keeps running, existing contacts are unaffected, and only the old link stops working. (See [Switching message relays](#switching-message-relays) for why you may also want to reset the address after changing servers.) ## Connecting programmatically @@ -62,4 +83,6 @@ Each reply is a JSON object with the same `corrId` and a `resp` field. The bridg ## Resetting -To start over with a fresh identity, run the **Reset Profile** action under Danger Zone (available when the service is stopped). It permanently deletes the SimpleX identity, contacts, and chat history; your API keys are kept. On the next start the bridge boots with a fresh profile. +To start over with a fresh identity, run the **Reset Client** action under Danger Zone (available when the service is stopped). It permanently deletes the SimpleX identity, contacts, and chat history; your API keys are kept, and a fresh identity is created on the next start. + +Existing contacts can no longer reach the client afterward and must reconnect with a new invitation or address. If this client is used with OpenClaw, purge the channel's per-contact state before anyone reconnects — SimpleX reuses low, sequential contact ids, so a brand-new contact could otherwise inherit an old contact's OpenClaw session history, pairing approval, and allowlist entry. diff --git a/package-lock.json b/package-lock.json index c1d3b2e..d9412e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,14 @@ { - "name": "simplex-gateway-startos", + "name": "simplex-websocket-bridge-startos", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "simplex-gateway-startos", + "name": "simplex-websocket-bridge-startos", "dependencies": { "@simplex-chat/types": "^0.7.0", "@start9labs/start-sdk": "1.5.3", + "jimp": "^0.22.12", "ws": "^8.18.0" }, "devDependencies": { @@ -24,6 +25,425 @@ "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", "license": "ISC" }, + "node_modules/@jimp/bmp": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.22.12.tgz", + "integrity": "sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "bmp-js": "^0.1.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/core": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.22.12.tgz", + "integrity": "sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "any-base": "^1.1.0", + "buffer": "^5.2.0", + "exif-parser": "^0.1.12", + "file-type": "^16.5.4", + "isomorphic-fetch": "^3.0.0", + "pixelmatch": "^4.0.2", + "tinycolor2": "^1.6.0" + } + }, + "node_modules/@jimp/custom": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz", + "integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==", + "license": "MIT", + "dependencies": { + "@jimp/core": "^0.22.12" + } + }, + "node_modules/@jimp/gif": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.22.12.tgz", + "integrity": "sha512-y6BFTJgch9mbor2H234VSjd9iwAhaNf/t3US5qpYIs0TSbAvM02Fbc28IaDETj9+4YB4676sz4RcN/zwhfu1pg==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "gifwrap": "^0.10.1", + "omggif": "^1.0.9" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/jpeg": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.22.12.tgz", + "integrity": "sha512-Rq26XC/uQWaQKyb/5lksCTCxXhtY01NJeBN+dQv5yNYedN0i7iYu+fXEoRsfaJ8xZzjoANH8sns7rVP4GE7d/Q==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "jpeg-js": "^0.4.4" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-blit": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz", + "integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-blur": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz", + "integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-circle": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-0.22.12.tgz", + "integrity": "sha512-SWVXx1yiuj5jZtMijqUfvVOJBwOifFn0918ou4ftoHgegc5aHWW5dZbYPjvC9fLpvz7oSlptNl2Sxr1zwofjTg==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-color": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz", + "integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "tinycolor2": "^1.6.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-contain": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.22.12.tgz", + "integrity": "sha512-Eo3DmfixJw3N79lWk8q/0SDYbqmKt1xSTJ69yy8XLYQj9svoBbyRpSnHR+n9hOw5pKXytHwUW6nU4u1wegHNoQ==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5", + "@jimp/plugin-scale": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-cover": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.22.12.tgz", + "integrity": "sha512-z0w/1xH/v/knZkpTNx+E8a7fnasQ2wHG5ze6y5oL2dhH1UufNua8gLQXlv8/W56+4nJ1brhSd233HBJCo01BXA==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-crop": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5", + "@jimp/plugin-scale": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-crop": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz", + "integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-displace": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.22.12.tgz", + "integrity": "sha512-qpRM8JRicxfK6aPPqKZA6+GzBwUIitiHaZw0QrJ64Ygd3+AsTc7BXr+37k2x7QcyCvmKXY4haUrSIsBug4S3CA==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-dither": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.22.12.tgz", + "integrity": "sha512-jYgGdSdSKl1UUEanX8A85v4+QUm+PE8vHFwlamaKk89s+PXQe7eVE3eNeSZX4inCq63EHL7cX580dMqkoC3ZLw==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-fisheye": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-0.22.12.tgz", + "integrity": "sha512-LGuUTsFg+fOp6KBKrmLkX4LfyCy8IIsROwoUvsUPKzutSqMJnsm3JGDW2eOmWIS/jJpPaeaishjlxvczjgII+Q==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-flip": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.22.12.tgz", + "integrity": "sha512-m251Rop7GN8W0Yo/rF9LWk6kNclngyjIJs/VXHToGQ6EGveOSTSQaX2Isi9f9lCDLxt+inBIb7nlaLLxnvHX8Q==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-rotate": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-gaussian": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.22.12.tgz", + "integrity": "sha512-sBfbzoOmJ6FczfG2PquiK84NtVGeScw97JsCC3rpQv1PHVWyW+uqWFF53+n3c8Y0P2HWlUjflEla2h/vWShvhg==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-invert": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.22.12.tgz", + "integrity": "sha512-N+6rwxdB+7OCR6PYijaA/iizXXodpxOGvT/smd/lxeXsZ/empHmFFFJ/FaXcYh19Tm04dGDaXcNF/dN5nm6+xQ==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-mask": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.22.12.tgz", + "integrity": "sha512-4AWZg+DomtpUA099jRV8IEZUfn1wLv6+nem4NRJC7L/82vxzLCgXKTxvNvBcNmJjT9yS1LAAmiJGdWKXG63/NA==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-normalize": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.22.12.tgz", + "integrity": "sha512-0So0rexQivnWgnhacX4cfkM2223YdExnJTTy6d06WbkfZk5alHUx8MM3yEzwoCN0ErO7oyqEWRnEkGC+As1FtA==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-print": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.22.12.tgz", + "integrity": "sha512-c7TnhHlxm87DJeSnwr/XOLjJU/whoiKYY7r21SbuJ5nuH+7a78EW1teOaj5gEr2wYEd7QtkFqGlmyGXY/YclyQ==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "load-bmfont": "^1.4.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-resize": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz", + "integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-rotate": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz", + "integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5", + "@jimp/plugin-crop": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-scale": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz", + "integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-shadow": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-shadow/-/plugin-shadow-0.22.12.tgz", + "integrity": "sha512-FX8mTJuCt7/3zXVoeD/qHlm4YH2bVqBuWQHXSuBK054e7wFRnRnbSLPUqAwSeYP3lWqpuQzJtgiiBxV3+WWwTg==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blur": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-threshold": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-0.22.12.tgz", + "integrity": "sha512-4x5GrQr1a/9L0paBC/MZZJjjgjxLYrqSmWd+e+QfAEPvmRxdRoQ5uKEuNgXnm9/weHQBTnQBQsOY2iFja+XGAw==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-color": ">=0.8.0", + "@jimp/plugin-resize": ">=0.8.0" + } + }, + "node_modules/@jimp/plugins": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.22.12.tgz", + "integrity": "sha512-yBJ8vQrDkBbTgQZLty9k4+KtUQdRjsIDJSPjuI21YdVeqZxYywifHl4/XWILoTZsjTUASQcGoH0TuC0N7xm3ww==", + "license": "MIT", + "dependencies": { + "@jimp/plugin-blit": "^0.22.12", + "@jimp/plugin-blur": "^0.22.12", + "@jimp/plugin-circle": "^0.22.12", + "@jimp/plugin-color": "^0.22.12", + "@jimp/plugin-contain": "^0.22.12", + "@jimp/plugin-cover": "^0.22.12", + "@jimp/plugin-crop": "^0.22.12", + "@jimp/plugin-displace": "^0.22.12", + "@jimp/plugin-dither": "^0.22.12", + "@jimp/plugin-fisheye": "^0.22.12", + "@jimp/plugin-flip": "^0.22.12", + "@jimp/plugin-gaussian": "^0.22.12", + "@jimp/plugin-invert": "^0.22.12", + "@jimp/plugin-mask": "^0.22.12", + "@jimp/plugin-normalize": "^0.22.12", + "@jimp/plugin-print": "^0.22.12", + "@jimp/plugin-resize": "^0.22.12", + "@jimp/plugin-rotate": "^0.22.12", + "@jimp/plugin-scale": "^0.22.12", + "@jimp/plugin-shadow": "^0.22.12", + "@jimp/plugin-threshold": "^0.22.12", + "timm": "^1.6.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/png": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.22.12.tgz", + "integrity": "sha512-Mrp6dr3UTn+aLK8ty/dSKELz+Otdz1v4aAXzV5q53UDD2rbB5joKVJ/ChY310B+eRzNxIovbUF1KVrUsYdE8Hg==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "pngjs": "^6.0.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/tiff": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.22.12.tgz", + "integrity": "sha512-E1LtMh4RyJsoCAfAkBRVSYyZDTtLq9p9LUiiYP0vPtXyxX4BiYBUYihTLSBlCQg5nF2e4OpQg7SPrLdJ66u7jg==", + "license": "MIT", + "dependencies": { + "utif2": "^4.0.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/types": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.22.12.tgz", + "integrity": "sha512-wwKYzRdElE1MBXFREvCto5s699izFHNVvALUv79GXNbsOVqlwlOxlWJ8DuyOGIXoLP4JW/m30YyuTtfUJgMRMA==", + "license": "MIT", + "dependencies": { + "@jimp/bmp": "^0.22.12", + "@jimp/gif": "^0.22.12", + "@jimp/jpeg": "^0.22.12", + "@jimp/png": "^0.22.12", + "@jimp/tiff": "^0.22.12", + "timm": "^1.6.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/utils": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.22.12.tgz", + "integrity": "sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.13.3" + } + }, "node_modules/@noble/curves": { "version": "1.9.7", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", @@ -92,6 +512,12 @@ "zod-deep-partial": "^1.2.0" } }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, "node_modules/@types/ini": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", @@ -128,6 +554,92 @@ "ncc": "dist/ncc/cli.js" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "license": "MIT" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", + "integrity": "sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/centra": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz", + "integrity": "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6" + } + }, "node_modules/deep-equality-data-structures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/deep-equality-data-structures/-/deep-equality-data-structures-2.0.0.tgz", @@ -137,6 +649,34 @@ "object-hash": "^3.0.0" } }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" + }, "node_modules/fast-xml-builder": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", @@ -174,6 +714,98 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "license": "MIT", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/gifwrap": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", + "integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==", + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } + }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/image-q": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", + "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", + "license": "MIT", + "dependencies": { + "@types/node": "16.9.1" + } + }, + "node_modules/image-q/node_modules/@types/node": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "license": "MIT" + }, "node_modules/ini": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", @@ -183,6 +815,12 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "license": "MIT" + }, "node_modules/isomorphic-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", @@ -193,6 +831,52 @@ "whatwg-fetch": "^3.4.1" } }, + "node_modules/jimp": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.22.12.tgz", + "integrity": "sha512-R5jZaYDnfkxKJy1dwLpj/7cvyjxiclxU3F4TrI/J4j2rS0niq6YDUMoPn5hs8GDpO+OZGo7Ky057CRtWesyhfg==", + "license": "MIT", + "dependencies": { + "@jimp/custom": "^0.22.12", + "@jimp/plugins": "^0.22.12", + "@jimp/types": "^0.22.12", + "regenerator-runtime": "^0.13.3" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, + "node_modules/load-bmfont": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz", + "integrity": "sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==", + "license": "MIT", + "dependencies": { + "buffer-equal": "0.0.1", + "mime": "^1.3.4", + "parse-bmfont-ascii": "^1.0.3", + "parse-bmfont-binary": "^1.0.5", + "parse-bmfont-xml": "^1.1.4", + "phin": "^3.7.1", + "xhr": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "node_modules/load-bmfont/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", @@ -208,6 +892,15 @@ "node": ">=16" } }, + "node_modules/min-document": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", + "license": "MIT", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -237,6 +930,46 @@ "node": ">= 6" } }, + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "license": "MIT" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, + "node_modules/parse-headers": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", + "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==", + "license": "MIT" + }, "node_modules/path-expression-matcher": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", @@ -252,6 +985,62 @@ "node": ">=14.0.0" } }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/phin": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/phin/-/phin-3.7.1.tgz", + "integrity": "sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "centra": "^2.7.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/pixelmatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", + "integrity": "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==", + "license": "ISC", + "dependencies": { + "pngjs": "^3.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, "node_modules/prettier": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", @@ -268,6 +1057,115 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/strnum": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", @@ -280,6 +1178,52 @@ ], "license": "MIT" }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/timm": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz", + "integrity": "sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==", + "license": "MIT" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -306,6 +1250,15 @@ "dev": true, "license": "MIT" }, + "node_modules/utif2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz", + "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.11" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -349,6 +1302,18 @@ } } }, + "node_modules/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", + "license": "MIT", + "dependencies": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, "node_modules/xml-naming": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", @@ -364,6 +1329,43 @@ "node": ">=16.0.0" } }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", @@ -384,7 +1386,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index f3cb626..2fdb78e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dependencies": { "@simplex-chat/types": "^0.7.0", "@start9labs/start-sdk": "1.5.3", + "jimp": "^0.22.12", "ws": "^8.18.0" }, "devDependencies": { diff --git a/startos/actions/configure.ts b/startos/actions/configure.ts deleted file mode 100644 index 522afe9..0000000 --- a/startos/actions/configure.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { T } from '@start9labs/start-sdk' -import { T as SX } from '@simplex-chat/types' -import { sdk } from '../sdk' -import { callBot, withBotSession } from '../bot-client' -import { i18n } from '../i18n' - -const { InputSpec, Value } = sdk - -/** - * Live editor for the bridge's SimpleX profile: display name, picture, and - * file/media sharing. - * - * Open: calls /user and prefills the form from the active user's current - * profile — including the live file-sharing preference, so submitting without - * touching the toggle can't silently flip it. - * - * Submit: re-fetches the live profile and sends /_profile - * with the edited fields applied *on top of* it, so peerType (the bot marker) - * and any other preferences are preserved, not clobbered. - * - * Schema reference: - * https://github.com/simplex-chat/simplex-chat/blob/stable/bots/api/COMMANDS.md#showactiveuser - * https://github.com/simplex-chat/simplex-chat/blob/stable/bots/api/COMMANDS.md#apiupdateprofile - */ - -const inputSpec = InputSpec.of({ - displayName: Value.text({ - name: i18n('Display Name'), - description: i18n( - 'The display name peers see when they connect to your bot.', - ), - required: true, - default: null, - masked: false, - placeholder: 'SimpleX Bot', - minLength: 1, - maxLength: 64, - patterns: [], - inputmode: 'text', - }), - image: Value.textarea({ - name: i18n('Profile Picture'), - description: i18n( - 'Profile picture as a base64 data URL (e.g. "data:image/jpg;base64,..."). Leave empty for no picture. Tip: small images (< 64KB) render best in SimpleX clients.', - ), - required: false, - default: null, - placeholder: 'data:image/jpg;base64,...', - minLength: null, - // ≈256KB of binary after base64 expansion — generous for a profile - // picture, but prevents accidental "I pasted the wrong file" submissions - // from bloating the bot profile and StartOS backups. - maxLength: 350_000, - patterns: [], - }), - allowFiles: Value.toggle({ - name: i18n('Allow files & media'), - description: i18n( - 'When on, contacts can send files and media to the bridge, and the bridge can send them. When off, file and media transfers are disabled.', - ), - default: true, - }), - // Hidden — round-tripped from getInput → run as fallbacks if the live - // re-fetch on submit fails. - _userId: Value.hidden(), - _fullName: Value.hidden(), -}) - -async function fetchActiveUser( - effects: T.Effects, -): Promise<{ userId: number; profile: SX.Profile }> { - const env = await callBot(effects, '/user') - if (env.resp?.type !== 'activeUser') { - throw new Error( - `Bot did not return an active user. Got: ${JSON.stringify(env.resp).slice(0, 500)}`, - ) - } - // env.resp narrows to CR.ActiveUser by the discriminated union — `user` - // and `user.profile` are guaranteed by the upstream type. - return { userId: env.resp.user.userId, profile: env.resp.user.profile } -} - -export const configure = sdk.Action.withInput( - 'configure', - async () => ({ - name: i18n('Configure Bot Profile'), - description: i18n( - 'Edit the bot profile — display name, picture, and file sharing.', - ), - warning: null, - allowedStatuses: 'only-running', - group: i18n('General'), - visibility: 'enabled', - }), - inputSpec, - async ({ effects }) => { - // Always fetch fresh — don't trust a stale prefill, since the profile can - // change between opens. - const { userId, profile } = await fetchActiveUser(effects) - return { - displayName: profile.displayName ?? '', - image: profile.image ?? '', - // Anything other than an explicit "no" is treated as enabled (the - // bridge's default), so an unset preference reads as on. - allowFiles: profile.preferences?.files?.allow !== SX.FeatureAllowed.No, - _userId: userId, - _fullName: profile.fullName ?? '', - } - }, - async ({ effects, input }) => { - // Re-resolve the live profile at submit time: if the bot was reset or the - // active user swapped between open and submit, the hidden fields are stale. - const live = await fetchActiveUser(effects).catch(() => null) - const userId = live?.userId ?? input._userId - if (typeof userId !== 'number') { - return { - version: '1', - title: i18n('Could Not Update Profile'), - message: i18n('No active user — the bot may not be fully started yet.'), - result: null, - } - } - - // Apply edits on top of the current profile so peerType and any other - // preferences ride through untouched. - const base = live?.profile - const newProfile: SX.Profile = { - displayName: input.displayName, - fullName: base?.fullName ?? input._fullName ?? '', - shortDescr: base?.shortDescr, - image: input.image?.trim() || undefined, - contactLink: base?.contactLink, - preferences: { - ...base?.preferences, - files: { - allow: input.allowFiles - ? SX.FeatureAllowed.Yes - : SX.FeatureAllowed.No, - }, - }, - peerType: base?.peerType, - } - - const cmd = `/_profile ${userId} ${JSON.stringify(newProfile)}` - - let env - try { - env = await withBotSession(effects, (send) => send(cmd)) - } catch (err) { - return { - version: '1', - title: i18n('Could Not Update Profile'), - message: (err as Error).message, - result: null, - } - } - - const respType = env.resp?.type - // The bot acknowledges /_profile with type: "userProfileUpdated" (or - // "userProfileNoChange" if nothing changed). Anything else is unexpected. - if ( - respType !== 'userProfileUpdated' && - respType !== 'userProfileNoChange' - ) { - return { - version: '1', - title: i18n('Bot Refused Update'), - message: `resp.type=${respType}\n\n${JSON.stringify(env.resp, null, 2).slice(0, 1800)}`, - result: null, - } - } - - // Success — no modal; hitting save already implies the update. - return null - }, -) diff --git a/startos/actions/configureClient.ts b/startos/actions/configureClient.ts new file mode 100644 index 0000000..02e3ec7 --- /dev/null +++ b/startos/actions/configureClient.ts @@ -0,0 +1,306 @@ +import { sdk } from '../sdk' +import { i18n } from '../i18n' +import { + clientSettingsJson, + readClientSettings, + ClientSettings, +} from '../fileModels/clientSettings.json' +import { syncClientSettings, configureServers } from '../liveSync' +import { pastedImageToAvatarDataUrl } from '../avatar' + +const { InputSpec, Value, Variants } = sdk + +/** + * Configure Client — the single pre-flight + live editor for the + * bridge's SimpleX identity and relays. Replaces the old "Configure Bot + * Profile" action. + * + * Meant to be run BEFORE first start (allowedStatuses: 'any'): it writes a + * settings file that main.ts reads to build the container's start env. When + * the service is already running, submitting also reconciles the live profile + * and address settings over the WebSocket API (see liveSync.ts), so most edits + * take effect immediately. + * + * Env-seeded, first-start-only fields (display name, peer type) and relay / + * retention settings are applied by re-reading the settings file on the next + * start; the form notes which changes need a restart. + */ + +const customServersSpec = InputSpec.of({ + smp: Value.textarea({ + name: i18n('SMP relay URIs'), + description: i18n( + 'One SMP server address per line, e.g. smp://@host. These REPLACE the public preset servers.', + ), + required: false, + default: null, + placeholder: 'smp://@host', + minLength: null, + maxLength: null, + patterns: [], + }), + xftp: Value.textarea({ + name: i18n('XFTP relay URIs'), + description: i18n( + 'One XFTP server address per line, e.g. xftp://@host. These REPLACE the public preset servers.', + ), + required: false, + default: null, + placeholder: 'xftp://@host', + minLength: null, + maxLength: null, + patterns: [], + }), +}) + +const inputSpec = InputSpec.of({ + displayName: Value.text({ + name: i18n('Display Name'), + description: i18n('The name peers see when they connect to your client.'), + required: true, + default: 'SimpleX Bot', + masked: false, + placeholder: 'SimpleX Bot', + minLength: 1, + maxLength: 64, + patterns: [], + inputmode: 'text', + }), + fullName: Value.text({ + name: i18n('Full Name'), + description: i18n('Optional longer name shown alongside the display name.'), + required: false, + default: null, + masked: false, + placeholder: null, + minLength: null, + maxLength: 128, + patterns: [], + inputmode: 'text', + }), + image: Value.textarea({ + name: i18n('Profile Picture'), + description: i18n( + 'Set a profile picture from an image URL (http/https), a data URL, or base64. Any size — it is cropped to a square and shrunk to fit the SimpleX avatar size limit. Leave empty to remove the picture.', + ), + required: false, + default: null, + placeholder: 'https://example.com/avatar.png', + minLength: null, + maxLength: null, + patterns: [], + }), + peerType: Value.select({ + name: i18n('Peer Type'), + description: i18n( + 'Bot marks the profile as a SimpleX bot so peer apps show command menus. Human presents as a regular user. Cosmetic — file and message transfer work either way.', + ), + default: 'bot', + values: { bot: i18n('Bot'), human: i18n('Human') }, + }), + autoAcceptContacts: Value.toggle({ + name: i18n('Auto-Accept Contact Requests'), + description: i18n( + 'Automatically accept incoming contact requests to the client address.', + ), + default: true, + }), + businessMode: Value.toggle({ + name: i18n('Business Mode'), + description: i18n( + 'Present the address as a business address (each contact becomes a group with the business, enabling multiple agents).', + ), + default: false, + }), + welcomeMessage: Value.textarea({ + name: i18n('Welcome Message'), + description: i18n( + 'Optional auto-reply sent to each new contact when they connect.', + ), + required: false, + default: null, + placeholder: null, + minLength: null, + maxLength: 2000, + patterns: [], + }), + servers: Value.union({ + name: i18n('Message Relays (SMP/XFTP)'), + description: i18n( + 'Which servers relay your messages and files. Applied immediately (no restart) and only to NEW connections — existing contacts and your current address keep using the server they were created on. Use Reset Address to move your address onto the new relays.', + ), + default: 'public', + variants: Variants.of({ + public: { + name: i18n('SimpleX defaults (public)'), + spec: InputSpec.of({}), + }, + local: { + name: i18n('My self-hosted SimpleX Server'), + spec: InputSpec.of({}), + }, + custom: { name: i18n('Custom'), spec: customServersSpec }, + }), + }), + cleanupDays: Value.number({ + name: i18n('Cleanup Received Files After (days)'), + description: i18n( + 'Delete received files older than this many days. Leave empty to keep files forever.', + ), + required: false, + default: null, + min: 1, + max: 3650, + integer: true, + units: i18n('days'), + placeholder: i18n('never'), + }), +}) + +/** Split a textarea of whitespace/newline-separated URIs into a clean array. */ +function splitUris(value: string | null | undefined): string[] { + return (value ?? '') + .split(/\s+/) + .map((s) => s.trim()) + .filter(Boolean) +} + +export const configureClient = sdk.Action.withInput( + 'configure-client', + async () => ({ + name: i18n('Configure Client'), + description: i18n( + 'Set the client display name, profile, contact-request handling, message relays, and file retention. Run this before starting the service for the first time.', + ), + warning: null, + allowedStatuses: 'any', + group: i18n('General'), + visibility: 'enabled', + }), + inputSpec, + // getInput — prefill from the saved settings, or code defaults on a fresh + // install where the file doesn't exist yet. + async ({ effects }) => { + const s = await readClientSettings(effects) + return { + displayName: s.displayName, + fullName: s.fullName || null, + // Prefill the stored avatar so it's visible/editable: leaving it unchanged + // keeps it, emptying it removes it, replacing it re-processes (see run()). + image: s.image || null, + peerType: s.peerType, + autoAcceptContacts: s.autoAcceptContacts, + businessMode: s.businessMode, + welcomeMessage: s.welcomeMessage || null, + servers: + s.servers.mode === 'custom' + ? { + selection: 'custom' as const, + value: { + smp: s.servers.smp.join('\n') || null, + xftp: s.servers.xftp.join('\n') || null, + }, + } + : s.servers.mode === 'local' + ? { selection: 'local' as const, value: {} } + : { selection: 'public' as const, value: {} }, + cleanupDays: s.cleanupDays, + } + }, + async ({ effects, input }) => { + // Read what was saved before: needed to tell which kind of change this is, + // and to retain the existing avatar when no new file is uploaded. + const previous = await readClientSettings(effects) + + // Avatar: the field is prefilled with the stored data URL, so an unchanged + // value is kept as-is (already normalized), an emptied value removes the + // picture, and a newly pasted image is cropped to a square and shrunk. + const pasted = input.image?.trim() ?? '' + let image = previous.image + if (pasted === '') { + image = '' + } else if (pasted !== previous.image) { + try { + image = await pastedImageToAvatarDataUrl(pasted) + } catch (err) { + return { + version: '1', + title: i18n('Could Not Process Image'), + message: i18n('The profile picture could not be processed: ').concat( + (err as Error).message, + ), + result: null, + } + } + } + + const servers: ClientSettings['servers'] = + input.servers.selection === 'custom' + ? { + mode: 'custom', + smp: splitUris(input.servers.value.smp), + xftp: splitUris(input.servers.value.xftp), + } + : { + mode: input.servers.selection as 'public' | 'local', + smp: [], + xftp: [], + } + + const settings: ClientSettings = { + displayName: input.displayName, + fullName: input.fullName?.trim() || '', + image, + peerType: input.peerType, + autoAcceptContacts: input.autoAcceptContacts, + businessMode: input.businessMode, + welcomeMessage: input.welcomeMessage?.trim() || '', + servers, + cleanupDays: input.cleanupDays ?? null, + } + + await clientSettingsJson.write(effects, settings) + + // On success return null (no modal). + + // If the client is stopped (the pre-first-start case), everything applies + // on the next start — nothing to do live. + const status = await sdk.getStatus(effects).once() + const running = !!status?.started + if (!running) return null + + const relaysChanged = + previous.servers.mode !== settings.servers.mode || + previous.servers.smp.join(' ') !== settings.servers.smp.join(' ') || + previous.servers.xftp.join(' ') !== settings.servers.xftp.join(' ') + const retentionChanged = previous.cleanupDays !== settings.cleanupDays + + // Received-file retention is a container env / janitor setting read at + // launch, so a change there needs a restart. The restart also re-applies + // relays and profile via the post-ready one-shot, so it covers everything. + if (retentionChanged) { + await effects.restart() + return null + } + + // Everything else applies live over the WebSocket — no restart, no + // downtime for dependents. Relays go through configureServers (which sets + // custom/local or resets to presets); profile/address through the sync. A + // rejected relay config surfaces here so it's diagnosable immediately. + try { + if (relaysChanged) await configureServers(effects, settings) + await syncClientSettings(effects, settings) + } catch (err) { + return { + version: '1', + title: i18n('Saved, But Live Update Failed'), + message: i18n( + 'Settings were saved, but applying them to the running client failed: ', + ).concat((err as Error).message), + result: null, + } + } + + return null + }, +) diff --git a/startos/actions/create-invitation.ts b/startos/actions/create-invitation.ts index 3f6f509..e5ccd0c 100644 --- a/startos/actions/create-invitation.ts +++ b/startos/actions/create-invitation.ts @@ -1,7 +1,7 @@ -import { T } from '@start9labs/start-sdk' import { CR } from '@simplex-chat/types' import { sdk } from '../sdk' import { withBotSession } from '../bot-client' +import { connLinkMembers } from '../links' import { i18n } from '../i18n' /** @@ -62,11 +62,8 @@ export const createInvitation = sdk.Action.withoutInput( } } - const inv = invitation.connLinkInvitation - const short = inv.connShortLink?.trim() - const full = inv.connFullLink?.trim() - - if (!short && !full) { + const members = connLinkMembers(invitation.connLinkInvitation) + if (members.length === 0) { return { version: '1', title: i18n('No Invitation Link Returned'), @@ -75,34 +72,6 @@ export const createInvitation = sdk.Action.withoutInput( } } - const members: T.ActionResultMember[] = [] - if (short) { - members.push({ - type: 'single', - name: i18n('Short Link (recommended)'), - description: i18n( - 'Use this with modern SimpleX clients. Includes a QR code.', - ), - value: short, - copyable: true, - qr: true, - masked: false, - }) - } - if (full) { - members.push({ - type: 'single', - name: i18n('Full Link (older clients)'), - description: i18n( - 'Backup format for older SimpleX clients that do not understand short links.', - ), - value: full, - copyable: true, - qr: true, - masked: false, - }) - } - return { version: '1', title: i18n('One-Time Invitation Link'), diff --git a/startos/actions/index.ts b/startos/actions/index.ts index d121fe7..ee16137 100644 --- a/startos/actions/index.ts +++ b/startos/actions/index.ts @@ -1,11 +1,15 @@ import { sdk } from '../sdk' -import { configure } from './configure' +import { configureClient } from './configureClient' import { createInvitation } from './create-invitation' +import { viewAddress } from './view-address' import { apiKeys } from './api-keys' -import { resetProfile } from './reset-profile' +import { resetClient } from './reset-client' +import { resetAddress } from './reset-address' export const actions = sdk.Actions.of() - .addAction(configure) + .addAction(configureClient) .addAction(createInvitation) + .addAction(viewAddress) .addAction(apiKeys) - .addAction(resetProfile) + .addAction(resetClient) + .addAction(resetAddress) diff --git a/startos/actions/reset-address.ts b/startos/actions/reset-address.ts new file mode 100644 index 0000000..0bcc891 --- /dev/null +++ b/startos/actions/reset-address.ts @@ -0,0 +1,83 @@ +import { CC } from '@simplex-chat/types' +import { sdk } from '../sdk' +import { withBotSession } from '../bot-client' +import { connLinkMembers } from '../links' +import { i18n } from '../i18n' + +/** + * Replace the client's long-lived SimpleX address with a fresh one. + * + * Deleting the address invalidates its connection link (new people can no + * longer use the old link), but existing contacts already have direct + * connections and are unaffected. Runs live over the WebSocket, so the service + * stays up — no stop required. + * + * Flow in one WebSocket session: + * 1. /user → the active user's id + * 2. /_delete_address → drop the current address (tolerated if none exists) + * 3. /_address → create the replacement, and return its link + * + * See: + * https://github.com/simplex-chat/simplex-chat/blob/stable/bots/api/COMMANDS.md#apideletemyaddress + * https://github.com/simplex-chat/simplex-chat/blob/stable/bots/api/COMMANDS.md#apicreatemyaddress + */ +export const resetAddress = sdk.Action.withoutInput( + 'reset-address', + async () => ({ + name: i18n('Reset Address'), + description: i18n( + 'Replace the long-lived SimpleX address for this client with a new one — useful after changing message relays, so the address is hosted on the new servers. Existing contacts are unaffected; only the old address link stops working.', + ), + warning: i18n( + 'This replaces the SimpleX address for this client with a new one. The old address link will no longer connect anyone to the client, so update anywhere you have shared it. Existing contacts are not affected — they can still chat with the client as before.', + ), + allowedStatuses: 'only-running', + group: i18n('Danger Zone'), + visibility: 'enabled', + }), + async ({ effects }) => { + let link + try { + link = await withBotSession(effects, async (send) => { + const userEnv = await send('/user') + if (userEnv.resp?.type !== 'activeUser') { + throw new Error( + `Bot did not return an active user. Got: ${JSON.stringify(userEnv.resp).slice(0, 300)}`, + ) + } + const userId = userEnv.resp.user.userId + + // Delete the current address. If there isn't one, the bot replies with + // a command error — tolerated, since the end state (a fresh address) is + // the same either way. + await send(CC.APIDeleteMyAddress.cmdString({ userId })) + + const createEnv = await send( + CC.APICreateMyAddress.cmdString({ userId }), + ) + if (createEnv.resp?.type !== 'userContactLinkCreated') { + throw new Error( + `Bot refused to create the new address. resp.type=${createEnv.resp?.type}\n\n${JSON.stringify(createEnv.resp, null, 2).slice(0, 1200)}`, + ) + } + return createEnv.resp.connLinkContact + }) + } catch (err) { + return { + version: '1', + title: i18n('Reset Failed'), + message: (err as Error).message, + result: null, + } + } + + return { + version: '1', + title: i18n('Address Reset'), + message: i18n( + 'A new SimpleX address has been created and is shown below. The previous address link no longer works; existing contacts are unaffected.', + ), + result: { type: 'group', value: connLinkMembers(link) }, + } + }, +) diff --git a/startos/actions/reset-profile.ts b/startos/actions/reset-client.ts similarity index 55% rename from startos/actions/reset-profile.ts rename to startos/actions/reset-client.ts index bb3457a..af22276 100644 --- a/startos/actions/reset-profile.ts +++ b/startos/actions/reset-client.ts @@ -15,15 +15,15 @@ const VOLUME_PATH = sdk.volumes.main.path * which means the fresh profile boots back at "SimpleX Bot" with no picture * and file sharing on. */ -export const resetProfile = sdk.Action.withoutInput( - 'reset-profile', +export const resetClient = sdk.Action.withoutInput( + 'reset-client', async () => ({ - name: i18n('Reset Profile'), + name: i18n('Reset Client'), description: i18n( - 'Permanently delete the bot identity, all chats, and all contacts. A fresh profile will be created the next time the service starts.', + 'Permanently delete the SimpleX identity, all chats, and all contacts. A fresh identity is created the next time the service starts.', ), warning: i18n( - 'This will permanently delete the bot identity, all chats, and all contacts. Anyone who has your current connection link will no longer be able to reach the bot. The fresh profile will revert to display name "SimpleX Bot" with no profile picture — you can change those again in the Configure action once the service is back up. This cannot be undone.', + 'This permanently deletes the SimpleX identity, all chats, and all contacts. Anyone who has your current connection link will no longer be able to reach this client — they must reconnect with a new invitation or address. The fresh identity reverts to display name "SimpleX Bot" with no profile picture; you can set those again in the Configure Client action once the service is back up. If this client is used with OpenClaw, note that SimpleX reuses low, sequential contact ids after a reset: a brand-new contact can take a former id (such as 4) and OpenClaw would treat them as the old contact, inheriting their session history, pairing approval, and allowlist membership. Purge the matching OpenClaw state for the channel before letting anyone reconnect. This cannot be undone.', ), allowedStatuses: 'only-stopped', group: i18n('Danger Zone'), @@ -51,9 +51,9 @@ export const resetProfile = sdk.Action.withoutInput( return { version: '1', - title: i18n('Profile Reset'), + title: i18n('Client Reset'), message: i18n( - 'The bot identity and all chat data have been deleted. Start the service to generate a fresh profile.', + 'The SimpleX identity, all contacts, and chat history have been deleted; your API keys are kept. Start the service to create a fresh identity. Every previous contact must reconnect with a new invitation or address — if this client is used with OpenClaw, purge the channel state before they do.', ), result: null, } diff --git a/startos/actions/view-address.ts b/startos/actions/view-address.ts new file mode 100644 index 0000000..469d5d6 --- /dev/null +++ b/startos/actions/view-address.ts @@ -0,0 +1,88 @@ +import { CC } from '@simplex-chat/types' +import { sdk } from '../sdk' +import { withBotSession } from '../bot-client' +import { connLinkMembers } from '../links' +import { i18n } from '../i18n' + +/** + * Show the client's long-lived SimpleX address — a reusable connection link. + * Unlike a one-time invitation (see Create Invitation), the same address can be + * shared with any number of contacts. + * + * Flow in one WebSocket session: + * 1. /user → the active user's id + * 2. /_show_address → the current address, if one exists + * 3. /_address → create one if step 2 found none, then use that + * + * See: + * https://github.com/simplex-chat/simplex-chat/blob/stable/bots/api/COMMANDS.md#apishowmyaddress + * https://github.com/simplex-chat/simplex-chat/blob/stable/bots/api/COMMANDS.md#apicreatemyaddress + */ +export const viewAddress = sdk.Action.withoutInput( + 'view-address', + async () => ({ + name: i18n('View Address'), + description: i18n( + 'Show the long-lived SimpleX address for this client — a reusable connection link. Unlike a one-time invitation, the same address works for any number of contacts.', + ), + warning: null, + allowedStatuses: 'only-running', + group: i18n('General'), + visibility: 'enabled', + }), + async ({ effects }) => { + let link + try { + link = await withBotSession(effects, async (send) => { + const userEnv = await send('/user') + if (userEnv.resp?.type !== 'activeUser') { + throw new Error( + `Bot did not return an active user. Got: ${JSON.stringify(userEnv.resp).slice(0, 300)}`, + ) + } + const userId = userEnv.resp.user.userId + + // Show the existing address, or create one if there isn't any yet. + const showEnv = await send(CC.APIShowMyAddress.cmdString({ userId })) + if (showEnv.resp?.type === 'userContactLink') { + return showEnv.resp.contactLink.connLinkContact + } + const createEnv = await send( + CC.APICreateMyAddress.cmdString({ userId }), + ) + if (createEnv.resp?.type !== 'userContactLinkCreated') { + throw new Error( + `Bot refused to create an address. resp.type=${createEnv.resp?.type}\n\n${JSON.stringify(createEnv.resp, null, 2).slice(0, 1200)}`, + ) + } + return createEnv.resp.connLinkContact + }) + } catch (err) { + return { + version: '1', + title: i18n('Could Not Reach Bot'), + message: (err as Error).message, + result: null, + } + } + + const members = connLinkMembers(link) + if (members.length === 0) { + return { + version: '1', + title: i18n('No Address Returned'), + message: JSON.stringify(link).slice(0, 2048), + result: null, + } + } + + return { + version: '1', + title: i18n('SimpleX Address'), + message: i18n( + 'Share this address so others can request to connect. It is reusable — the same link works for any number of contacts. To accept requests automatically, enable Auto-Accept Contact Requests in Configure Client.', + ), + result: { type: 'group', value: members }, + } + }, +) diff --git a/startos/avatar.ts b/startos/avatar.ts new file mode 100644 index 0000000..3ee07a0 --- /dev/null +++ b/startos/avatar.ts @@ -0,0 +1,112 @@ +import Jimp from 'jimp' +import { readFile } from 'fs/promises' + +/** + * Turn an arbitrary source image into a SimpleX-ready avatar data URL. + * + * SimpleX embeds the avatar in the profile "info" sent to every contact and + * rejects oversized profiles ("large info"), so the picture must be tiny. We + * center-crop to a square (SimpleX renders avatars square, so a non-square + * source would otherwise be stretched) and step the dimensions/quality down + * until the JPEG fits under the byte cap — mirroring the openclaw-simplex + * plugin's avatar handling. + * + * Returns a `data:image/jpg;base64,...` URL. SimpleX clients expect the + * "image/jpg" media type specifically (not "image/jpeg"). Throws if the image + * can't be brought under the cap or can't be decoded. + */ + +const MAX_AVATAR_BYTES = 12 * 1024 + +// [maxSide, jpegQuality] tiers, largest/highest first. +const STEPS: ReadonlyArray = [ + [192, 70], + [128, 60], + [96, 50], +] + +async function bufferToAvatarDataUrl(source: Buffer): Promise { + for (const [size, quality] of STEPS) { + // Re-decode from the original each step: cover() mutates the instance, so + // we always crop/scale from full resolution rather than a prior downscale. + const image = await Jimp.read(source) + image.cover(size, size) // center-crop + scale to fill the square + image.quality(quality) + const jpeg = await image.getBufferAsync(Jimp.MIME_JPEG) + if (jpeg.length <= MAX_AVATAR_BYTES) { + return `data:image/jpg;base64,${jpeg.toString('base64')}` + } + } + + throw new Error( + 'Image is too detailed to fit the SimpleX avatar size limit even after downscaling. Try a simpler or lower-resolution image.', + ) +} + +// Guards for remote fetches. The cap is on the SOURCE download, before +// downscaling; a normal avatar is well under this. +const MAX_DOWNLOAD_BYTES = 8 * 1024 * 1024 // 8 MB +const FETCH_TIMEOUT_MS = 15_000 + +/** Download an image from an http(s) URL into a buffer, bounded in time and size. */ +async function fetchImageBuffer(url: string): Promise { + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS) + let res + try { + res = await fetch(url, { signal: controller.signal, redirect: 'follow' }) + } catch (err) { + throw new Error(`Could not fetch image URL: ${(err as Error).message}`) + } finally { + clearTimeout(timer) + } + if (!res.ok) { + throw new Error(`Image URL returned HTTP ${res.status}.`) + } + const declared = Number(res.headers.get('content-length') ?? '') + if (Number.isFinite(declared) && declared > MAX_DOWNLOAD_BYTES) { + throw new Error('Image at that URL is too large (max 8 MB).') + } + const buffer = Buffer.from(await res.arrayBuffer()) + if (buffer.length === 0) { + throw new Error('Image URL returned no data.') + } + if (buffer.length > MAX_DOWNLOAD_BYTES) { + throw new Error('Image at that URL is too large (max 8 MB).') + } + return buffer +} + +/** + * Normalize a supplied image into the avatar data URL. Accepts an http(s) URL + * (fetched here), a `data:image/...;base64,...` URL, or a raw base64 blob + * (whitespace/newlines tolerated). Jimp decodes and validates the bytes, so a + * non-image input throws a clear error. + */ +export async function pastedImageToAvatarDataUrl( + value: string, +): Promise { + const trimmed = value.trim() + + if (/^https?:\/\//i.test(trimmed)) { + return bufferToAvatarDataUrl(await fetchImageBuffer(trimmed)) + } + + const comma = trimmed.startsWith('data:') ? trimmed.indexOf(',') : -1 + const base64 = (comma >= 0 ? trimmed.slice(comma + 1) : trimmed).replace( + /\s+/g, + '', + ) + const buffer = Buffer.from(base64, 'base64') + if (buffer.length === 0) { + throw new Error( + 'No image data found. Provide an image URL, data URL, or base64.', + ) + } + return bufferToAvatarDataUrl(buffer) +} + +/** Same normalization, but from a file path (for a future native file picker). */ +export async function fileToAvatarDataUrl(path: string): Promise { + return bufferToAvatarDataUrl(await readFile(path)) +} diff --git a/startos/bot-client.ts b/startos/bot-client.ts index d725670..fc7bf64 100644 --- a/startos/bot-client.ts +++ b/startos/bot-client.ts @@ -43,6 +43,39 @@ export async function callBot( return withBotSession(effects, (send) => send(cmd, corrId)) } +/** + * Poll until the bot is actually reachable and its chat controller answers. + * + * On a fresh start the container brings up simplex-chat, then websocat binds + * the external port a moment later, so an immediate connect races and fails + * with ECONNREFUSED. StartOS `requires` gating orders daemon *launch*, not + * socket readiness, so we do our own wait loop (mirroring entrypoint.sh's + * /dev/tcp probe): retry `/user` until it returns an active user, then return. + * Throws if the bot isn't ready within `timeoutMs`. + */ +export async function waitForBotReady( + effects: T.Effects, + { + timeoutMs = 90_000, + intervalMs = 1_000, + }: { timeoutMs?: number; intervalMs?: number } = {}, +): Promise { + const deadline = Date.now() + timeoutMs + let lastError = 'unknown error' + for (;;) { + try { + const env = await callBot(effects, '/user') + if (env.resp?.type === 'activeUser') return + lastError = `unexpected response: ${env.resp?.type}` + } catch (err) { + lastError = (err as Error).message + } + if (Date.now() + intervalMs >= deadline) break + await new Promise((resolve) => setTimeout(resolve, intervalMs)) + } + throw new Error(`Bot not ready after ${timeoutMs}ms (last: ${lastError})`) +} + /** * Open a single connection to the bot and yield a `send` function that * issues commands sequentially. The connection is closed when `fn` resolves diff --git a/startos/dependencies.ts b/startos/dependencies.ts index 7221c4b..4ce8378 100644 --- a/startos/dependencies.ts +++ b/startos/dependencies.ts @@ -1,5 +1,27 @@ import { sdk } from './sdk' +import { clientSettingsJson } from './fileModels/clientSettings.json' +import { SIMPLEX_SERVER_PACKAGE_ID } from './serverConfig' -export const setDependencies = sdk.setupDependencies( - async ({ effects }) => ({}), -) +/** + * The bridge has no hard dependencies. When the user selects "My self-hosted + * SimpleX Server" for relays (servers.mode === 'local') in the Configure + * action, we flip the optional `simplex` package to a `running` dependency so + * StartOS prompts for / tracks it. Reactive: re-runs whenever the settings file + * changes, so toggling the relay mode adds or drops the dependency with no + * explicit re-trigger. + */ +export const setDependencies = sdk.setupDependencies(async ({ effects }) => { + const mode = await clientSettingsJson + .read((c) => c.servers.mode) + .const(effects) + + if (mode !== 'local') return {} + + return { + [SIMPLEX_SERVER_PACKAGE_ID]: { + kind: 'running', + versionRange: '*', + healthChecks: [], + }, + } +}) diff --git a/startos/fileModels/clientSettings.json.ts b/startos/fileModels/clientSettings.json.ts new file mode 100644 index 0000000..a88adff --- /dev/null +++ b/startos/fileModels/clientSettings.json.ts @@ -0,0 +1,101 @@ +import { FileHelper, T, z } from '@start9labs/start-sdk' +import { sdk } from '../sdk' + +/** + * Persisted SimpleX client settings. + * + * Written by the "Configure Client" action and read by: + * - main.ts, to compute the container's start environment (display name, + * peer type, SMP/XFTP relays, received-file retention); and + * - the live WebSocket sync, to reconcile the running profile and address + * settings (full name, image, auto-accept, business mode, welcome message) + * that the image can't seed from env. + * + * The action is intended to run *before* first start, so this file may not + * exist yet — callers use `readClientSettings`, which falls back to + * SETTINGS_DEFAULTS. Every field also has a `.catch(...)` so a partial or older + * file still parses into a complete, valid object rather than throwing. + */ + +export type ServerMode = 'public' | 'local' | 'custom' + +export interface ClientSettings { + displayName: string + fullName: string + /** Base64 data URL (e.g. "data:image/jpg;base64,...") or "" for none. */ + image: string + peerType: 'bot' | 'human' + /** Auto-accept incoming contact requests on the bot's address. */ + autoAcceptContacts: boolean + businessMode: boolean + /** Auto-reply sent to new contacts; "" = none. */ + welcomeMessage: string + servers: { + mode: ServerMode + /** Full SMP relay URIs (custom mode). */ + smp: string[] + /** Full XFTP relay URIs (custom mode). */ + xftp: string[] + } + /** Delete received files older than N days; null = never. */ + cleanupDays: number | null +} + +// Shared defaults. The display-name default matches the Docker image +// (PROFILE_DISPLAY_NAME) so a bare install produces the same identity whether +// or not the user opens the action first. +export const SETTINGS_DEFAULTS: ClientSettings = { + displayName: 'SimpleX Bot', + fullName: '', + image: '', + peerType: 'bot', + autoAcceptContacts: true, + businessMode: false, + welcomeMessage: '', + servers: { mode: 'public', smp: [], xftp: [] }, + cleanupDays: null, +} + +const serversShape = z + .object({ + mode: z.enum(['public', 'local', 'custom']).catch('public'), + smp: z.array(z.string()).catch([]), + xftp: z.array(z.string()).catch([]), + }) + .catch(SETTINGS_DEFAULTS.servers) + +const shape = z.object({ + displayName: z.string().min(1).catch(SETTINGS_DEFAULTS.displayName), + fullName: z.string().catch(''), + image: z.string().catch(''), + peerType: z.enum(['bot', 'human']).catch('bot'), + autoAcceptContacts: z.boolean().catch(true), + businessMode: z.boolean().catch(false), + welcomeMessage: z.string().catch(''), + servers: serversShape, + cleanupDays: z.number().int().nonnegative().nullable().catch(null), +}) + +export const clientSettingsJson = FileHelper.json( + { base: sdk.volumes.main, subpath: '/client-settings.json' }, + shape, +) + +/** + * Read the settings file, falling back to defaults when it doesn't exist yet + * (fresh install, before the Configure action has ever been submitted). The + * per-field `.catch(...)` guarantees a complete object once the file is present. + * + * Deliberately a NON-reactive `.once()` read. `main` reads settings through + * this to compute the container's start env; a reactive `.const()` would + * re-run `main` (restarting the whole service) every time the Configure action + * writes the file — even for a display-name change that we want to apply live + * over the WebSocket. Relay/retention changes trigger a restart explicitly from + * the action instead. (`effects` is kept for signature symmetry / future use.) + */ +export async function readClientSettings( + _effects: T.Effects, +): Promise { + const stored = await clientSettingsJson.read((c) => c).once() + return stored ?? SETTINGS_DEFAULTS +} diff --git a/startos/i18n/dictionaries/default.ts b/startos/i18n/dictionaries/default.ts index 7943e14..3af62c8 100644 --- a/startos/i18n/dictionaries/default.ts +++ b/startos/i18n/dictionaries/default.ts @@ -13,17 +13,8 @@ const dict = { 'Danger Zone': 6, // configure action - 'Configure Bot Profile': 7, - 'Edit the bot profile — display name, picture, and file sharing.': 8, 'Display Name': 9, - 'The display name peers see when they connect to your bot.': 10, 'Profile Picture': 11, - 'Profile picture as a base64 data URL (e.g. "data:image/jpg;base64,..."). Leave empty for no picture. Tip: small images (< 64KB) render best in SimpleX clients.': 12, - 'Could Not Update Profile': 13, - 'No active user — the bot may not be fully started yet.': 14, - 'Bot Refused Update': 15, - 'Allow files & media': 16, - 'When on, contacts can send files and media to the bridge, and the bridge can send them. When off, file and media transfers are disabled.': 17, // create-invitation action 'Create Invitation': 18, @@ -37,13 +28,13 @@ const dict = { 'Could Not Reach Bot': 26, 'No Invitation Link Returned': 27, - // reset-profile action - 'Reset Profile': 28, - 'Permanently delete the bot identity, all chats, and all contacts. A fresh profile will be created the next time the service starts.': 29, - 'This will permanently delete the bot identity, all chats, and all contacts. Anyone who has your current connection link will no longer be able to reach the bot. The fresh profile will revert to display name "SimpleX Bot" with no profile picture — you can change those again in the Configure action once the service is back up. This cannot be undone.': 30, + // reset-client action + 'Reset Client': 28, + 'Permanently delete the SimpleX identity, all chats, and all contacts. A fresh identity is created the next time the service starts.': 29, + 'This permanently deletes the SimpleX identity, all chats, and all contacts. Anyone who has your current connection link will no longer be able to reach this client — they must reconnect with a new invitation or address. The fresh identity reverts to display name "SimpleX Bot" with no profile picture; you can set those again in the Configure Client action once the service is back up. If this client is used with OpenClaw, note that SimpleX reuses low, sequential contact ids after a reset: a brand-new contact can take a former id (such as 4) and OpenClaw would treat them as the old contact, inheriting their session history, pairing approval, and allowlist membership. Purge the matching OpenClaw state for the channel before letting anyone reconnect. This cannot be undone.': 30, 'Reset Failed': 31, - 'Profile Reset': 32, - 'The bot identity and all chat data have been deleted. Start the service to generate a fresh profile.': 33, + 'Client Reset': 32, + 'The SimpleX identity, all contacts, and chat history have been deleted; your API keys are kept. Start the service to create a fresh identity. Every previous contact must reconnect with a new invitation or address — if this client is used with OpenClaw, purge the channel state before they do.': 33, // api-keys action 'API Keys': 34, @@ -55,6 +46,59 @@ const dict = { 'Leave blank when adding a key and one is generated for you. Keep it secret.': 40, 'API Keys Saved': 41, 'Outside clients authenticate with the header: Authorization: Bearer ': 42, + + // configure SimpleX client action + 'SMP relay URIs': 43, + 'One SMP server address per line, e.g. smp://@host. These REPLACE the public preset servers.': 44, + 'XFTP relay URIs': 45, + 'One XFTP server address per line, e.g. xftp://@host. These REPLACE the public preset servers.': 46, + 'The name peers see when they connect to your client.': 47, + 'Full Name': 48, + 'Optional longer name shown alongside the display name.': 49, + 'Set a profile picture from an image URL (http/https), a data URL, or base64. Any size — it is cropped to a square and shrunk to fit the SimpleX avatar size limit. Leave empty to remove the picture.': 50, + 'Peer Type': 51, + 'Bot marks the profile as a SimpleX bot so peer apps show command menus. Human presents as a regular user. Cosmetic — file and message transfer work either way.': 52, + Bot: 53, + Human: 54, + 'Auto-Accept Contact Requests': 55, + 'Automatically accept incoming contact requests to the client address.': 56, + 'Business Mode': 57, + 'Present the address as a business address (each contact becomes a group with the business, enabling multiple agents).': 58, + 'Welcome Message': 59, + 'Optional auto-reply sent to each new contact when they connect.': 60, + 'Message Relays (SMP/XFTP)': 61, + 'Which servers relay your messages and files. Applied immediately (no restart) and only to NEW connections — existing contacts and your current address keep using the server they were created on. Use Reset Address to move your address onto the new relays.': 62, + 'SimpleX defaults (public)': 63, + 'My self-hosted SimpleX Server': 64, + Custom: 65, + 'Cleanup Received Files After (days)': 66, + 'Delete received files older than this many days. Leave empty to keep files forever.': 67, + days: 68, + never: 69, + 'Configure Client': 70, + 'Set the client display name, profile, contact-request handling, message relays, and file retention. Run this before starting the service for the first time.': 71, + 'Saved, But Live Update Failed': 74, + 'Settings were saved, but applying them to the running client failed: ': 75, + + // main.ts sync + 'SimpleX client settings synced': 78, + 'Could not sync SimpleX client settings: ': 79, + + // avatar processing (configure action) + 'Could Not Process Image': 80, + 'The profile picture could not be processed: ': 81, + + // view-address / reset-address actions + 'View Address': 83, + 'Show the long-lived SimpleX address for this client — a reusable connection link. Unlike a one-time invitation, the same address works for any number of contacts.': 84, + 'SimpleX Address': 85, + 'Share this address so others can request to connect. It is reusable — the same link works for any number of contacts. To accept requests automatically, enable Auto-Accept Contact Requests in Configure Client.': 86, + 'No Address Returned': 87, + 'Reset Address': 88, + 'Replace the long-lived SimpleX address for this client with a new one — useful after changing message relays, so the address is hosted on the new servers. Existing contacts are unaffected; only the old address link stops working.': 89, + 'This replaces the SimpleX address for this client with a new one. The old address link will no longer connect anyone to the client, so update anywhere you have shared it. Existing contacts are not affected — they can still chat with the client as before.': 90, + 'Address Reset': 91, + 'A new SimpleX address has been created and is shown below. The previous address link no longer works; existing contacts are unaffected.': 92, } as const /** diff --git a/startos/i18n/dictionaries/translations.ts b/startos/i18n/dictionaries/translations.ts index 7972ac3..75f5cc1 100644 --- a/startos/i18n/dictionaries/translations.ts +++ b/startos/i18n/dictionaries/translations.ts @@ -9,17 +9,8 @@ export default { 4: 'El Websocket no está listo', 5: 'General', 6: 'Zona de peligro', - 7: 'Configurar perfil del bot', - 8: 'Edita el perfil del bot: nombre visible, imagen y uso compartido de archivos.', 9: 'Nombre visible', - 10: 'El nombre visible que ven los contactos cuando se conectan a tu bot.', 11: 'Imagen de perfil', - 12: 'Imagen de perfil como data URL en base64 (p. ej., "data:image/jpg;base64,..."). Déjalo vacío para no tener imagen. Consejo: las imágenes pequeñas (< 64 KB) se ven mejor en los clientes de SimpleX.', - 13: 'No se pudo actualizar el perfil', - 14: 'No hay un usuario activo: puede que el bot aún no se haya iniciado por completo.', - 15: 'El bot rechazó la actualización', - 16: 'Permitir archivos y multimedia', - 17: 'Cuando está activado, los contactos pueden enviar archivos y multimedia al puente, y el puente puede enviarlos. Cuando está desactivado, las transferencias de archivos y multimedia están deshabilitadas.', 18: 'Crear invitación', 19: 'Crea un enlace de invitación de SimpleX de un solo uso. Cada ejecución genera un enlace nuevo que puede usar exactamente un contacto nuevo: compártelo por cualquier canal y pídele que lo pegue en su cliente de SimpleX.', 20: 'Enlace de invitación de un solo uso', @@ -30,12 +21,12 @@ export default { 25: 'Formato de respaldo para clientes antiguos de SimpleX que no entienden los enlaces cortos.', 26: 'No se pudo contactar con el bot', 27: 'No se devolvió ningún enlace de invitación', - 28: 'Restablecer perfil', - 29: 'Elimina permanentemente la identidad del bot, todos los chats y todos los contactos. Se creará un perfil nuevo la próxima vez que se inicie el servicio.', - 30: 'Esto eliminará permanentemente la identidad del bot, todos los chats y todos los contactos. Quien tenga tu enlace de conexión actual ya no podrá comunicarse con el bot. El perfil nuevo volverá al nombre visible "SimpleX Bot" sin imagen de perfil: puedes cambiarlos de nuevo en la acción Configurar una vez que el servicio vuelva a estar activo. Esto no se puede deshacer.', + 28: 'Restablecer cliente', + 29: 'Elimina permanentemente la identidad de SimpleX, todos los chats y todos los contactos. Se crea una identidad nueva la próxima vez que se inicie el servicio.', + 30: 'Esto elimina permanentemente la identidad de SimpleX, todos los chats y todos los contactos. Quien tenga tu enlace de conexión actual ya no podrá comunicarse con este cliente: deberá volver a conectarse con una nueva invitación o dirección. La identidad nueva vuelve al nombre visible "SimpleX Bot" sin imagen de perfil; puedes configurarlos de nuevo en la acción Configurar cliente una vez que el servicio vuelva a estar activo. Si este cliente se usa con OpenClaw, ten en cuenta que SimpleX reutiliza los ids de contacto bajos y secuenciales tras un restablecimiento: un contacto totalmente nuevo puede tomar un id anterior (como 4) y OpenClaw lo trataría como el contacto antiguo, heredando su historial de sesión, la aprobación de emparejamiento y la pertenencia a la lista de permitidos. Purga el estado correspondiente de OpenClaw para el canal antes de permitir que alguien se reconecte. Esto no se puede deshacer.', 31: 'Error al restablecer', - 32: 'Perfil restablecido', - 33: 'Se han eliminado la identidad del bot y todos los datos de chat. Inicia el servicio para generar un perfil nuevo.', + 32: 'Cliente restablecido', + 33: 'Se han eliminado la identidad de SimpleX, todos los contactos y el historial de chat; se conservan tus claves de API. Inicia el servicio para crear una identidad nueva. Cada contacto anterior debe volver a conectarse con una nueva invitación o dirección; si este cliente se usa con OpenClaw, purga el estado del canal antes de que lo hagan.', 34: 'Claves de API', 35: 'Gestiona los tokens bearer que controlan el acceso externo a la API Websocket.', 36: 'Tokens bearer que conceden acceso externo a la API Websocket. Añade uno por cliente; elimínalo para revocarlo. Los servicios del mismo equipo se conectan directamente y nunca necesitan una clave.', @@ -45,6 +36,51 @@ export default { 40: 'Déjalo en blanco al añadir una clave y se generará uno por ti. Mantenlo en secreto.', 41: 'Claves de API guardadas', 42: 'Los clientes externos se autentican con la cabecera: Authorization: Bearer ', + 43: 'URIs de relé SMP', + 44: 'Una dirección de servidor SMP por línea, p. ej. smp://@host. Estas REEMPLAZAN los servidores públicos predeterminados.', + 45: 'URIs de relé XFTP', + 46: 'Una dirección de servidor XFTP por línea, p. ej. xftp://@host. Estas REEMPLAZAN los servidores públicos predeterminados.', + 47: 'El nombre que ven los contactos cuando se conectan a tu cliente.', + 48: 'Nombre completo', + 49: 'Nombre más largo opcional que se muestra junto al nombre visible.', + 50: 'Establece una imagen de perfil desde una URL de imagen (http/https), una data URL o base64. De cualquier tamaño: se recorta a un cuadrado y se reduce para ajustarse al límite de tamaño del avatar de SimpleX. Déjalo vacío para eliminar la imagen.', + 51: 'Tipo de par', + 52: 'Bot marca el perfil como un bot de SimpleX para que las apps de los contactos muestren menús de comandos. Human se presenta como un usuario normal. Es cosmético: la transferencia de archivos y mensajes funciona igual.', + 53: 'Bot', + 54: 'Humano', + 55: 'Aceptar automáticamente solicitudes de contacto', + 56: 'Acepta automáticamente las solicitudes de contacto entrantes a la dirección del cliente.', + 57: 'Modo empresa', + 58: 'Presenta la dirección como una dirección de empresa (cada contacto se convierte en un grupo con la empresa, lo que permite varios agentes).', + 59: 'Mensaje de bienvenida', + 60: 'Respuesta automática opcional que se envía a cada nuevo contacto cuando se conecta.', + 61: 'Relés de mensajes (SMP/XFTP)', + 62: 'Qué servidores retransmiten tus mensajes y archivos. Se aplica de inmediato (sin reinicio) y solo a las conexiones NUEVAS: los contactos existentes y tu dirección actual siguen usando el servidor con el que se crearon. Usa Restablecer dirección para mover tu dirección a los nuevos relés.', + 63: 'Predeterminados de SimpleX (públicos)', + 64: 'Mi servidor SimpleX autoalojado', + 65: 'Personalizado', + 66: 'Eliminar archivos recibidos después de (días)', + 67: 'Elimina los archivos recibidos con más de estos días de antigüedad. Déjalo vacío para conservarlos para siempre.', + 68: 'días', + 69: 'nunca', + 70: 'Configurar cliente', + 71: 'Configura el nombre visible del cliente, el perfil, la gestión de solicitudes de contacto, los relés de mensajes y la retención de archivos. Ejecuta esto antes de iniciar el servicio por primera vez.', + 74: 'Guardado, pero falló la actualización en vivo', + 75: 'La configuración se guardó, pero no se pudo aplicar al cliente en ejecución: ', + 78: 'Configuración del cliente SimpleX sincronizada', + 79: 'No se pudo sincronizar la configuración del cliente SimpleX: ', + 80: 'No se pudo procesar la imagen', + 81: 'No se pudo procesar la imagen de perfil: ', + 83: 'Ver dirección', + 84: 'Muestra la dirección SimpleX de larga duración de este cliente: un enlace de conexión reutilizable. A diferencia de una invitación de un solo uso, la misma dirección sirve para cualquier número de contactos.', + 85: 'Dirección SimpleX', + 86: 'Comparte esta dirección para que otros puedan solicitar conectarse. Es reutilizable: el mismo enlace sirve para cualquier número de contactos. Para aceptar solicitudes automáticamente, activa Aceptar automáticamente solicitudes de contacto en Configurar cliente.', + 87: 'No se devolvió ninguna dirección', + 88: 'Restablecer dirección', + 89: 'Reemplaza la dirección SimpleX de larga duración de este cliente por una nueva, útil tras cambiar los relés de mensajes para que la dirección se aloje en los nuevos servidores. Los contactos existentes no se ven afectados; solo deja de funcionar el enlace de la dirección anterior.', + 90: 'Esto reemplaza la dirección SimpleX de este cliente por una nueva. El enlace de la dirección anterior ya no conectará a nadie con el cliente, así que actualízalo donde lo hayas compartido. Los contactos existentes no se ven afectados: pueden seguir chateando con el cliente como antes.', + 91: 'Dirección restablecida', + 92: 'Se ha creado una nueva dirección SimpleX y se muestra a continuación. El enlace de la dirección anterior ya no funciona; los contactos existentes no se ven afectados.', }, de_DE: { 0: 'SimpleX Websocket Bridge wird gestartet!', @@ -54,17 +90,8 @@ export default { 4: 'Websocket ist nicht bereit', 5: 'Allgemein', 6: 'Gefahrenzone', - 7: 'Bot-Profil konfigurieren', - 8: 'Bearbeite das Bot-Profil – Anzeigename, Bild und Dateifreigabe.', 9: 'Anzeigename', - 10: 'Der Anzeigename, den Kontakte sehen, wenn sie sich mit deinem Bot verbinden.', 11: 'Profilbild', - 12: 'Profilbild als base64-Data-URL (z. B. "data:image/jpg;base64,..."). Leer lassen für kein Bild. Tipp: Kleine Bilder (< 64 KB) werden in SimpleX-Clients am besten dargestellt.', - 13: 'Profil konnte nicht aktualisiert werden', - 14: 'Kein aktiver Benutzer – der Bot ist möglicherweise noch nicht vollständig gestartet.', - 15: 'Bot hat die Aktualisierung abgelehnt', - 16: 'Dateien & Medien erlauben', - 17: 'Wenn aktiviert, können Kontakte Dateien und Medien an die Brücke senden, und die Brücke kann sie senden. Wenn deaktiviert, sind Datei- und Medienübertragungen deaktiviert.', 18: 'Einladung erstellen', 19: 'Erstelle einen einmaligen SimpleX-Einladungslink. Jeder Aufruf erzeugt einen neuen Link, der von genau einem neuen Kontakt verwendet werden kann – teile ihn über einen beliebigen Kanal und lass ihn in den SimpleX-Client einfügen.', 20: 'Einmaliger Einladungslink', @@ -75,12 +102,12 @@ export default { 25: 'Reserveformat für ältere SimpleX-Clients, die Kurzlinks nicht verstehen.', 26: 'Bot konnte nicht erreicht werden', 27: 'Kein Einladungslink zurückgegeben', - 28: 'Profil zurücksetzen', - 29: 'Löscht dauerhaft die Bot-Identität, alle Chats und alle Kontakte. Beim nächsten Start des Dienstes wird ein neues Profil erstellt.', - 30: 'Dies löscht dauerhaft die Bot-Identität, alle Chats und alle Kontakte. Wer deinen aktuellen Verbindungslink hat, kann den Bot nicht mehr erreichen. Das neue Profil kehrt zum Anzeigenamen "SimpleX Bot" ohne Profilbild zurück – du kannst beides in der Aktion „Konfigurieren“ wieder ändern, sobald der Dienst wieder läuft. Dies kann nicht rückgängig gemacht werden.', + 28: 'Client zurücksetzen', + 29: 'Löscht dauerhaft die SimpleX-Identität, alle Chats und alle Kontakte. Beim nächsten Start des Dienstes wird eine neue Identität erstellt.', + 30: 'Dies löscht dauerhaft die SimpleX-Identität, alle Chats und alle Kontakte. Wer deinen aktuellen Verbindungslink hat, kann diesen Client nicht mehr erreichen – die Person muss sich mit einer neuen Einladung oder Adresse neu verbinden. Die neue Identität kehrt zum Anzeigenamen "SimpleX Bot" ohne Profilbild zurück; du kannst beides in der Aktion „Client konfigurieren“ wieder festlegen, sobald der Dienst wieder läuft. Wird dieser Client mit OpenClaw verwendet, beachte, dass SimpleX nach einem Zurücksetzen niedrige, fortlaufende Kontakt-IDs wiederverwendet: ein völlig neuer Kontakt kann eine frühere ID (etwa 4) erhalten, und OpenClaw würde ihn als den alten Kontakt behandeln und dessen Sitzungsverlauf, Kopplungsfreigabe und Allowlist-Mitgliedschaft erben. Bereinige den entsprechenden OpenClaw-Status für den Kanal, bevor sich jemand neu verbindet. Dies kann nicht rückgängig gemacht werden.', 31: 'Zurücksetzen fehlgeschlagen', - 32: 'Profil zurückgesetzt', - 33: 'Die Bot-Identität und alle Chatdaten wurden gelöscht. Starte den Dienst, um ein neues Profil zu erzeugen.', + 32: 'Client zurückgesetzt', + 33: 'Die SimpleX-Identität, alle Kontakte und der Chatverlauf wurden gelöscht; deine API-Schlüssel bleiben erhalten. Starte den Dienst, um eine neue Identität zu erstellen. Jeder frühere Kontakt muss sich mit einer neuen Einladung oder Adresse neu verbinden – wird dieser Client mit OpenClaw verwendet, bereinige zuvor den Kanalstatus.', 34: 'API-Schlüssel', 35: 'Verwalte die Bearer-Token, die den externen Zugriff auf die Websocket-API absichern.', 36: 'Bearer-Token, die externen Zugriff auf die Websocket-API gewähren. Füge einen pro Client hinzu; lösche ihn zum Widerrufen. Dienste auf demselben Gerät verbinden sich direkt und benötigen nie einen Schlüssel.', @@ -90,6 +117,51 @@ export default { 40: 'Beim Hinzufügen eines Schlüssels leer lassen, dann wird einer für dich erzeugt. Halte ihn geheim.', 41: 'API-Schlüssel gespeichert', 42: 'Externe Clients authentifizieren sich mit dem Header: Authorization: Bearer ', + 43: 'SMP-Relay-URIs', + 44: 'Eine SMP-Serveradresse pro Zeile, z. B. smp://@host. Diese ERSETZEN die öffentlichen Standardserver.', + 45: 'XFTP-Relay-URIs', + 46: 'Eine XFTP-Serveradresse pro Zeile, z. B. xftp://@host. Diese ERSETZEN die öffentlichen Standardserver.', + 47: 'Der Name, den Kontakte sehen, wenn sie sich mit deinem Client verbinden.', + 48: 'Vollständiger Name', + 49: 'Optionaler längerer Name, der neben dem Anzeigenamen angezeigt wird.', + 50: 'Lege ein Profilbild über eine Bild-URL (http/https), eine Data-URL oder base64 fest. Beliebige Größe – es wird auf ein Quadrat zugeschnitten und verkleinert, um in das Avatar-Größenlimit von SimpleX zu passen. Leer lassen, um das Bild zu entfernen.', + 51: 'Peer-Typ', + 52: 'Bot markiert das Profil als SimpleX-Bot, sodass die Apps der Kontakte Befehlsmenüs anzeigen. Human erscheint als normaler Benutzer. Kosmetisch – Datei- und Nachrichtenübertragung funktionieren in beiden Fällen.', + 53: 'Bot', + 54: 'Mensch', + 55: 'Kontaktanfragen automatisch annehmen', + 56: 'Nimmt eingehende Kontaktanfragen an die Adresse des Clients automatisch an.', + 57: 'Geschäftsmodus', + 58: 'Präsentiert die Adresse als Geschäftsadresse (jeder Kontakt wird zu einer Gruppe mit dem Unternehmen, was mehrere Agenten ermöglicht).', + 59: 'Willkommensnachricht', + 60: 'Optionale automatische Antwort, die an jeden neuen Kontakt gesendet wird, wenn er sich verbindet.', + 61: 'Nachrichten-Relays (SMP/XFTP)', + 62: 'Welche Server deine Nachrichten und Dateien weiterleiten. Wird sofort angewendet (ohne Neustart) und nur auf NEUE Verbindungen — bestehende Kontakte und deine aktuelle Adresse verwenden weiterhin den Server, mit dem sie erstellt wurden. Verwende „Adresse zurücksetzen“, um deine Adresse auf die neuen Relays zu verschieben.', + 63: 'SimpleX-Standard (öffentlich)', + 64: 'Mein selbst gehosteter SimpleX-Server', + 65: 'Benutzerdefiniert', + 66: 'Empfangene Dateien löschen nach (Tagen)', + 67: 'Empfangene Dateien löschen, die älter als so viele Tage sind. Leer lassen, um Dateien dauerhaft zu behalten.', + 68: 'Tage', + 69: 'nie', + 70: 'Client konfigurieren', + 71: 'Lege Anzeigename, Profil, Umgang mit Kontaktanfragen, Nachrichten-Relays und Dateiaufbewahrung des Clients fest. Führe dies vor dem ersten Start des Dienstes aus.', + 74: 'Gespeichert, aber Live-Aktualisierung fehlgeschlagen', + 75: 'Die Einstellungen wurden gespeichert, aber die Anwendung auf den laufenden Client ist fehlgeschlagen: ', + 78: 'SimpleX-Client-Einstellungen synchronisiert', + 79: 'SimpleX-Client-Einstellungen konnten nicht synchronisiert werden: ', + 80: 'Bild konnte nicht verarbeitet werden', + 81: 'Das Profilbild konnte nicht verarbeitet werden: ', + 83: 'Adresse anzeigen', + 84: 'Zeigt die langlebige SimpleX-Adresse dieses Clients an – einen wiederverwendbaren Verbindungslink. Anders als eine einmalige Einladung funktioniert dieselbe Adresse für beliebig viele Kontakte.', + 85: 'SimpleX-Adresse', + 86: 'Teile diese Adresse, damit andere eine Verbindung anfragen können. Sie ist wiederverwendbar – derselbe Link funktioniert für beliebig viele Kontakte. Um Anfragen automatisch anzunehmen, aktiviere „Kontaktanfragen automatisch annehmen“ unter „Client konfigurieren“.', + 87: 'Keine Adresse zurückgegeben', + 88: 'Adresse zurücksetzen', + 89: 'Ersetzt die langlebige SimpleX-Adresse dieses Clients durch eine neue – nützlich nach dem Wechsel der Nachrichten-Relays, damit die Adresse auf den neuen Servern liegt. Bestehende Kontakte sind nicht betroffen; nur der alte Adresslink funktioniert nicht mehr.', + 90: 'Dies ersetzt die SimpleX-Adresse dieses Clients durch eine neue. Der alte Adresslink verbindet niemanden mehr mit dem Client – aktualisiere ihn also überall, wo du ihn geteilt hast. Bestehende Kontakte sind nicht betroffen: Sie können weiterhin wie zuvor mit dem Client chatten.', + 91: 'Adresse zurückgesetzt', + 92: 'Eine neue SimpleX-Adresse wurde erstellt und wird unten angezeigt. Der vorherige Adresslink funktioniert nicht mehr; bestehende Kontakte sind nicht betroffen.', }, pl_PL: { 0: 'Uruchamianie SimpleX Websocket Bridge!', @@ -99,17 +171,8 @@ export default { 4: 'Websocket nie jest gotowy', 5: 'Ogólne', 6: 'Strefa zagrożenia', - 7: 'Konfiguruj profil bota', - 8: 'Edytuj profil bota – nazwę wyświetlaną, obraz i udostępnianie plików.', 9: 'Nazwa wyświetlana', - 10: 'Nazwa wyświetlana, którą widzą kontakty, gdy łączą się z Twoim botem.', 11: 'Obraz profilu', - 12: 'Obraz profilu jako data URL w base64 (np. "data:image/jpg;base64,..."). Pozostaw puste, aby nie ustawiać obrazu. Wskazówka: małe obrazy (< 64 KB) wyświetlają się najlepiej w klientach SimpleX.', - 13: 'Nie można zaktualizować profilu', - 14: 'Brak aktywnego użytkownika – bot może nie być jeszcze w pełni uruchomiony.', - 15: 'Bot odrzucił aktualizację', - 16: 'Zezwól na pliki i multimedia', - 17: 'Gdy włączone, kontakty mogą wysyłać pliki i multimedia do mostu, a most może je wysyłać. Gdy wyłączone, przesyłanie plików i multimediów jest zablokowane.', 18: 'Utwórz zaproszenie', 19: 'Utwórz jednorazowy link zaproszenia SimpleX. Każde wywołanie generuje nowy link, którego może użyć dokładnie jeden nowy kontakt – udostępnij go dowolnym kanałem i poproś o wklejenie go w kliencie SimpleX.', 20: 'Jednorazowy link zaproszenia', @@ -120,12 +183,12 @@ export default { 25: 'Format zapasowy dla starszych klientów SimpleX, które nie obsługują krótkich linków.', 26: 'Nie można połączyć się z botem', 27: 'Nie zwrócono żadnego linku zaproszenia', - 28: 'Zresetuj profil', - 29: 'Trwale usuwa tożsamość bota, wszystkie czaty i wszystkie kontakty. Przy następnym uruchomieniu usługi zostanie utworzony nowy profil.', - 30: 'To trwale usunie tożsamość bota, wszystkie czaty i wszystkie kontakty. Każdy, kto ma Twój obecny link połączenia, nie będzie już mógł skontaktować się z botem. Nowy profil powróci do nazwy wyświetlanej "SimpleX Bot" bez obrazu profilu – możesz je ponownie zmienić w akcji Konfiguruj, gdy usługa znów będzie działać. Tej operacji nie można cofnąć.', + 28: 'Zresetuj klienta', + 29: 'Trwale usuwa tożsamość SimpleX, wszystkie czaty i wszystkie kontakty. Przy następnym uruchomieniu usługi zostanie utworzona nowa tożsamość.', + 30: 'Trwale usuwa to tożsamość SimpleX, wszystkie czaty i wszystkie kontakty. Każdy, kto ma Twój obecny link połączenia, nie będzie już mógł skontaktować się z tym klientem – musi połączyć się ponownie za pomocą nowego zaproszenia lub adresu. Nowa tożsamość powróci do nazwy wyświetlanej "SimpleX Bot" bez obrazu profilu; możesz je ponownie ustawić w akcji Konfiguruj klienta, gdy usługa znów będzie działać. Jeśli ten klient jest używany z OpenClaw, pamiętaj, że SimpleX po zresetowaniu ponownie używa niskich, kolejnych identyfikatorów kontaktów: zupełnie nowy kontakt może otrzymać wcześniejszy identyfikator (np. 4), a OpenClaw potraktuje go jak stary kontakt, dziedzicząc jego historię sesji, zatwierdzenie parowania i członkostwo na liście dozwolonych. Wyczyść odpowiedni stan OpenClaw dla kanału, zanim ktokolwiek połączy się ponownie. Tej operacji nie można cofnąć.', 31: 'Resetowanie nie powiodło się', - 32: 'Profil zresetowany', - 33: 'Tożsamość bota i wszystkie dane czatu zostały usunięte. Uruchom usługę, aby wygenerować nowy profil.', + 32: 'Zresetowano klienta', + 33: 'Tożsamość SimpleX, wszystkie kontakty i historia czatu zostały usunięte; Twoje klucze API są zachowane. Uruchom usługę, aby utworzyć nową tożsamość. Każdy poprzedni kontakt musi połączyć się ponownie za pomocą nowego zaproszenia lub adresu — jeśli ten klient jest używany z OpenClaw, najpierw wyczyść stan kanału.', 34: 'Klucze API', 35: 'Zarządzaj tokenami bearer, które kontrolują dostęp z zewnątrz do API Websocket.', 36: 'Tokeny bearer przyznające dostęp z zewnątrz do API Websocket. Dodaj jeden na klienta; usuń, aby cofnąć. Usługi na tym samym urządzeniu łączą się bezpośrednio i nigdy nie potrzebują klucza.', @@ -135,6 +198,51 @@ export default { 40: 'Pozostaw puste podczas dodawania klucza, a zostanie wygenerowany automatycznie. Zachowaj go w tajemnicy.', 41: 'Zapisano klucze API', 42: 'Klienci zewnętrzni uwierzytelniają się nagłówkiem: Authorization: Bearer ', + 43: 'Adresy URI przekaźników SMP', + 44: 'Jeden adres serwera SMP w linii, np. smp://@host. ZASTĘPUJĄ one publiczne serwery domyślne.', + 45: 'Adresy URI przekaźników XFTP', + 46: 'Jeden adres serwera XFTP w linii, np. xftp://@host. ZASTĘPUJĄ one publiczne serwery domyślne.', + 47: 'Nazwa, którą widzą kontakty, gdy łączą się z Twoim klientem.', + 48: 'Imię i nazwisko', + 49: 'Opcjonalna dłuższa nazwa wyświetlana obok nazwy wyświetlanej.', + 50: 'Ustaw obraz profilu z adresu URL obrazu (http/https), data URL lub base64. Dowolny rozmiar – zostanie przycięty do kwadratu i pomniejszony, aby zmieścić się w limicie rozmiaru awatara SimpleX. Pozostaw puste, aby usunąć obraz.', + 51: 'Typ węzła', + 52: 'Bot oznacza profil jako bota SimpleX, dzięki czemu aplikacje kontaktów pokazują menu poleceń. Human prezentuje się jako zwykły użytkownik. Kosmetyczne – przesyłanie plików i wiadomości działa tak samo.', + 53: 'Bot', + 54: 'Człowiek', + 55: 'Automatycznie akceptuj prośby o kontakt', + 56: 'Automatycznie akceptuj przychodzące prośby o kontakt na adres klienta.', + 57: 'Tryb biznesowy', + 58: 'Prezentuje adres jako adres biznesowy (każdy kontakt staje się grupą z firmą, co umożliwia obsługę przez wielu agentów).', + 59: 'Wiadomość powitalna', + 60: 'Opcjonalna automatyczna odpowiedź wysyłana do każdego nowego kontaktu po połączeniu.', + 61: 'Przekaźniki wiadomości (SMP/XFTP)', + 62: 'Które serwery przekazują Twoje wiadomości i pliki. Stosowane natychmiast (bez ponownego uruchamiania) i tylko do NOWYCH połączeń — istniejące kontakty i Twój bieżący adres nadal używają serwera, z którym zostały utworzone. Użyj „Zresetuj adres”, aby przenieść adres na nowe przekaźniki.', + 63: 'Domyślne SimpleX (publiczne)', + 64: 'Mój samodzielnie hostowany serwer SimpleX', + 65: 'Niestandardowe', + 66: 'Usuwaj odebrane pliki po (dniach)', + 67: 'Usuwaj odebrane pliki starsze niż podana liczba dni. Pozostaw puste, aby zachować pliki na zawsze.', + 68: 'dni', + 69: 'nigdy', + 70: 'Konfiguruj klienta', + 71: 'Ustaw nazwę wyświetlaną klienta, profil, obsługę próśb o kontakt, przekaźniki wiadomości i przechowywanie plików. Uruchom to przed pierwszym uruchomieniem usługi.', + 74: 'Zapisano, ale aktualizacja na żywo nie powiodła się', + 75: 'Ustawienia zostały zapisane, ale zastosowanie ich do działającego klienta nie powiodło się: ', + 78: 'Zsynchronizowano ustawienia klienta SimpleX', + 79: 'Nie można zsynchronizować ustawień klienta SimpleX: ', + 80: 'Nie można przetworzyć obrazu', + 81: 'Nie można przetworzyć obrazu profilu: ', + 83: 'Pokaż adres', + 84: 'Pokazuje długoterminowy adres SimpleX tego klienta — link połączenia wielokrotnego użytku. W przeciwieństwie do jednorazowego zaproszenia ten sam adres działa dla dowolnej liczby kontaktów.', + 85: 'Adres SimpleX', + 86: 'Udostępnij ten adres, aby inni mogli poprosić o połączenie. Jest wielokrotnego użytku — ten sam link działa dla dowolnej liczby kontaktów. Aby automatycznie akceptować prośby, włącz „Automatycznie akceptuj prośby o kontakt” w Konfiguruj klienta.', + 87: 'Nie zwrócono żadnego adresu', + 88: 'Zresetuj adres', + 89: 'Zastępuje długoterminowy adres SimpleX tego klienta nowym — przydatne po zmianie przekaźników wiadomości, aby adres był hostowany na nowych serwerach. Istniejące kontakty nie są naruszone; przestaje działać tylko stary link adresu.', + 90: 'To zastępuje adres SimpleX tego klienta nowym. Stary link adresu nie połączy już nikogo z klientem, więc zaktualizuj go wszędzie tam, gdzie go udostępniono. Istniejące kontakty nie są naruszone: mogą nadal rozmawiać z klientem jak wcześniej.', + 91: 'Zresetowano adres', + 92: 'Utworzono nowy adres SimpleX, pokazany poniżej. Poprzedni link adresu już nie działa; istniejące kontakty nie są naruszone.', }, fr_FR: { 0: 'Démarrage de SimpleX Websocket Bridge !', @@ -144,17 +252,8 @@ export default { 4: "Le Websocket n'est pas prêt", 5: 'Général', 6: 'Zone de danger', - 7: 'Configurer le profil du bot', - 8: 'Modifiez le profil du bot — nom affiché, image et partage de fichiers.', 9: 'Nom affiché', - 10: "Le nom affiché que voient les contacts lorsqu'ils se connectent à votre bot.", 11: 'Image de profil', - 12: 'Image de profil sous forme de data URL en base64 (par ex. "data:image/jpg;base64,..."). Laissez vide pour aucune image. Astuce : les petites images (< 64 Ko) sont mieux rendues dans les clients SimpleX.', - 13: 'Impossible de mettre à jour le profil', - 14: "Aucun utilisateur actif — le bot n'est peut-être pas encore complètement démarré.", - 15: 'Le bot a refusé la mise à jour', - 16: 'Autoriser les fichiers et médias', - 17: "Lorsque cette option est activée, les contacts peuvent envoyer des fichiers et des médias au pont, et le pont peut en envoyer. Lorsqu'elle est désactivée, les transferts de fichiers et de médias sont désactivés.", 18: 'Créer une invitation', 19: "Créez un lien d'invitation SimpleX à usage unique. Chaque exécution génère un nouveau lien qu'un seul nouveau contact peut utiliser — partagez-le par n'importe quel canal et demandez-lui de le coller dans son client SimpleX.", 20: "Lien d'invitation à usage unique", @@ -165,12 +264,12 @@ export default { 25: 'Format de secours pour les anciens clients SimpleX qui ne comprennent pas les liens courts.', 26: 'Impossible de joindre le bot', 27: "Aucun lien d'invitation renvoyé", - 28: 'Réinitialiser le profil', - 29: "Supprime définitivement l'identité du bot, tous les chats et tous les contacts. Un nouveau profil sera créé au prochain démarrage du service.", - 30: 'Cela supprimera définitivement l\'identité du bot, tous les chats et tous les contacts. Quiconque possède votre lien de connexion actuel ne pourra plus joindre le bot. Le nouveau profil reviendra au nom affiché "SimpleX Bot" sans image de profil — vous pourrez les modifier à nouveau dans l\'action Configurer une fois le service redémarré. Cette action est irréversible.', + 28: 'Réinitialiser le client', + 29: "Supprime définitivement l'identité SimpleX, tous les chats et tous les contacts. Une nouvelle identité est créée au prochain démarrage du service.", + 30: "Cela supprime définitivement l'identité SimpleX, tous les chats et tous les contacts. Quiconque possède votre lien de connexion actuel ne pourra plus joindre ce client — il devra se reconnecter avec une nouvelle invitation ou adresse. La nouvelle identité revient au nom affiché « SimpleX Bot » sans image de profil ; vous pourrez les redéfinir dans l'action Configurer le client une fois le service redémarré. Si ce client est utilisé avec OpenClaw, notez que SimpleX réutilise les ids de contact bas et séquentiels après une réinitialisation : un tout nouveau contact peut prendre un ancien id (par exemple 4) et OpenClaw le traiterait comme l'ancien contact, héritant de son historique de session, de son approbation d'appairage et de son appartenance à la liste d'autorisation. Purgez l'état OpenClaw correspondant au canal avant de laisser quiconque se reconnecter. Cette action est irréversible.", 31: 'Échec de la réinitialisation', - 32: 'Profil réinitialisé', - 33: "L'identité du bot et toutes les données de chat ont été supprimées. Démarrez le service pour générer un nouveau profil.", + 32: 'Client réinitialisé', + 33: "L'identité SimpleX, tous les contacts et l'historique des chats ont été supprimés ; vos clés API sont conservées. Démarrez le service pour créer une nouvelle identité. Chaque ancien contact doit se reconnecter avec une nouvelle invitation ou adresse — si ce client est utilisé avec OpenClaw, purgez l'état du canal au préalable.", 34: 'Clés API', 35: "Gérez les jetons bearer qui protègent l'accès externe à l'API Websocket.", 36: "Jetons bearer qui accordent l'accès externe à l'API Websocket. Ajoutez-en un par client ; supprimez-le pour le révoquer. Les services du même appareil se connectent directement et n'ont jamais besoin de clé.", @@ -180,5 +279,50 @@ export default { 40: "Laissez vide lors de l'ajout d'une clé et un jeton sera généré pour vous. Gardez-le secret.", 41: 'Clés API enregistrées', 42: "Les clients externes s'authentifient avec l'en-tête : Authorization: Bearer ", + 43: 'URI de relais SMP', + 44: 'Une adresse de serveur SMP par ligne, par ex. smp://@host. Elles REMPLACENT les serveurs publics par défaut.', + 45: 'URI de relais XFTP', + 46: 'Une adresse de serveur XFTP par ligne, par ex. xftp://@host. Elles REMPLACENT les serveurs publics par défaut.', + 47: "Le nom que voient les contacts lorsqu'ils se connectent à votre client.", + 48: 'Nom complet', + 49: 'Nom plus long facultatif affiché à côté du nom affiché.', + 50: "Définissez une image de profil à partir d'une URL d'image (http/https), d'une data URL ou de base64. N'importe quelle taille — elle est recadrée en carré et réduite pour respecter la limite de taille de l'avatar SimpleX. Laissez vide pour supprimer l'image.", + 51: 'Type de pair', + 52: 'Bot marque le profil comme un bot SimpleX afin que les applications des contacts affichent des menus de commandes. Human se présente comme un utilisateur normal. Cosmétique — le transfert de fichiers et de messages fonctionne dans les deux cas.', + 53: 'Bot', + 54: 'Humain', + 55: 'Accepter automatiquement les demandes de contact', + 56: "Accepte automatiquement les demandes de contact entrantes vers l'adresse du client.", + 57: 'Mode entreprise', + 58: "Présente l'adresse comme une adresse professionnelle (chaque contact devient un groupe avec l'entreprise, permettant plusieurs agents).", + 59: 'Message de bienvenue', + 60: 'Réponse automatique facultative envoyée à chaque nouveau contact lors de sa connexion.', + 61: 'Relais de messages (SMP/XFTP)', + 62: "Quels serveurs relaient vos messages et fichiers. Appliqué immédiatement (sans redémarrage) et uniquement aux NOUVELLES connexions — vos contacts existants et votre adresse actuelle continuent d'utiliser le serveur avec lequel ils ont été créés. Utilisez « Réinitialiser l'adresse » pour déplacer votre adresse vers les nouveaux relais.", + 63: 'Valeurs par défaut SimpleX (publiques)', + 64: 'Mon serveur SimpleX auto-hébergé', + 65: 'Personnalisé', + 66: 'Supprimer les fichiers reçus après (jours)', + 67: 'Supprime les fichiers reçus plus anciens que ce nombre de jours. Laissez vide pour conserver les fichiers indéfiniment.', + 68: 'jours', + 69: 'jamais', + 70: 'Configurer le client', + 71: 'Définissez le nom affiché du client, le profil, la gestion des demandes de contact, les relais de messages et la conservation des fichiers. Exécutez ceci avant de démarrer le service pour la première fois.', + 74: 'Enregistré, mais la mise à jour en direct a échoué', + 75: "Les paramètres ont été enregistrés, mais leur application au client en cours d'exécution a échoué : ", + 78: 'Paramètres du client SimpleX synchronisés', + 79: 'Impossible de synchroniser les paramètres du client SimpleX : ', + 80: "Impossible de traiter l'image", + 81: "L'image de profil n'a pas pu être traitée : ", + 83: "Afficher l'adresse", + 84: "Affiche l'adresse SimpleX durable de ce client — un lien de connexion réutilisable. Contrairement à une invitation à usage unique, la même adresse fonctionne pour un nombre illimité de contacts.", + 85: 'Adresse SimpleX', + 86: "Partagez cette adresse pour que d'autres puissent demander à se connecter. Elle est réutilisable — le même lien fonctionne pour un nombre illimité de contacts. Pour accepter les demandes automatiquement, activez « Accepter automatiquement les demandes de contact » dans Configurer le client.", + 87: 'Aucune adresse renvoyée', + 88: "Réinitialiser l'adresse", + 89: "Remplace l'adresse SimpleX durable de ce client par une nouvelle — utile après un changement de relais de messages, pour que l'adresse soit hébergée sur les nouveaux serveurs. Les contacts existants ne sont pas affectés ; seul l'ancien lien d'adresse cesse de fonctionner.", + 90: "Cela remplace l'adresse SimpleX de ce client par une nouvelle. L'ancien lien d'adresse ne connectera plus personne au client, alors mettez-le à jour partout où vous l'avez partagé. Les contacts existants ne sont pas affectés : ils peuvent continuer à discuter avec le client comme avant.", + 91: 'Adresse réinitialisée', + 92: "Une nouvelle adresse SimpleX a été créée et est affichée ci-dessous. L'ancien lien d'adresse ne fonctionne plus ; les contacts existants ne sont pas affectés.", }, } satisfies Record diff --git a/startos/links.ts b/startos/links.ts new file mode 100644 index 0000000..2cb62df --- /dev/null +++ b/startos/links.ts @@ -0,0 +1,43 @@ +import { T } from '@start9labs/start-sdk' +import { T as SX } from '@simplex-chat/types' +import { i18n } from './i18n' + +/** + * Build result members for a SimpleX connection link (one-time invitation or + * long-lived address). Both a short link (modern clients) and a full link + * (older clients) are surfaced when present, each copyable and with a QR code. + */ +export function connLinkMembers( + link: SX.CreatedConnLink, +): T.ActionResultMember[] { + const members: T.ActionResultMember[] = [] + const short = link.connShortLink?.trim() + const full = link.connFullLink?.trim() + if (short) { + members.push({ + type: 'single', + name: i18n('Short Link (recommended)'), + description: i18n( + 'Use this with modern SimpleX clients. Includes a QR code.', + ), + value: short, + copyable: true, + qr: true, + masked: false, + }) + } + if (full) { + members.push({ + type: 'single', + name: i18n('Full Link (older clients)'), + description: i18n( + 'Backup format for older SimpleX clients that do not understand short links.', + ), + value: full, + copyable: true, + qr: true, + masked: false, + }) + } + return members +} diff --git a/startos/liveSync.ts b/startos/liveSync.ts new file mode 100644 index 0000000..1872f58 --- /dev/null +++ b/startos/liveSync.ts @@ -0,0 +1,235 @@ +import { T } from '@start9labs/start-sdk' +import { CC, T as SX } from '@simplex-chat/types' +import { withBotSession, Envelope } from './bot-client' +import { ClientSettings } from './fileModels/clientSettings.json' +import { resolveServerUris } from './serverConfig' + +/** + * Reconcile a running bot's live profile and address settings with the + * persisted client settings, over a single WebSocket session. + * + * The Docker image can only seed display name and peer type from env, and only + * on first start. Everything else — full name, avatar, business mode, and the + * address's auto-accept / welcome-message settings — must be applied to the + * running client over its control API. This runs both at start (a post-ready + * one-shot in main.ts) and immediately on a Configure submit when the service + * is already up. + * + * Mirrors the openclaw-simplex plugin's ensureProfile/ensureAddress: read the + * live values first and only write when they've drifted, so a restart with + * unchanged settings makes no interactive network calls. + * + * Command/response reference: + * https://github.com/simplex-chat/simplex-chat/blob/stable/bots/api/COMMANDS.md + */ + +export interface SyncResult { + profileUpdated: boolean + addressCreated: boolean + addressSettingsUpdated: boolean +} + +function activeUser(env: Envelope): { userId: number; profile: SX.Profile } { + if (env.resp?.type !== 'activeUser') { + throw new Error( + `Bot did not return an active user (resp.type=${env.resp?.type}). It may not be fully started yet.`, + ) + } + return { userId: env.resp.user.userId, profile: env.resp.user.profile } +} + +function assertNotCmdError(env: Envelope, what: string): void { + if (env.resp?.type === 'chatCmdError') { + throw new Error( + `Bot refused ${what}: ${JSON.stringify(env.resp).slice(0, 800)}`, + ) + } +} + +/** Plain text of a welcome message (stored as MsgContent), or '' if none. */ +function welcomeText(mc: SX.MsgContent | undefined): string { + if (mc && 'text' in mc && typeof mc.text === 'string') return mc.text + return '' +} + +// --- Relay (SMP/XFTP) configuration over the v6 operator-servers API --- +// +// SimpleX v6 groups servers by "operator". `/_servers ` (GET) returns a +// `userServers` array: one entry per preset operator (e.g. SimpleX Chat, whose +// preset servers can be individually enabled/disabled) plus, if the user added +// their own, one operator-less entry holding custom servers. `/_servers +// ` (SET, APISetUserServers) writes it back. +// +// This is why dropping the `--server` flag never reverts to public: the flag +// persisted a state with the preset servers disabled and a custom server +// enabled. To switch relays we GET, minimally mutate, and SET the SAME +// structure back — per protocol: use custom => disable that operator's preset +// servers and (re)place the custom entry; use presets => enable them and drop +// the custom entry. Round-tripping the structure (only flipping enabled/deleted +// and swapping custom server rows) keeps every operator field the server +// expects intact. + +interface ServerRow { + serverId?: number + server: string + preset?: boolean + enabled: boolean + deleted: boolean +} +interface ServerGroup { + operator?: unknown + smpServers: ServerRow[] + xftpServers: ServerRow[] + chatRelays?: unknown[] +} + +function applyProtocol( + groups: ServerGroup[], + key: 'smpServers' | 'xftpServers', + uris: string[], +): void { + const useCustom = uris.length > 0 + for (const g of groups) { + const rows = g[key] ?? [] + if (g.operator) { + // Preset operator servers: enabled only when using presets for this proto. + for (const s of rows) if (!s.deleted) s.enabled = !useCustom + } else { + // Existing custom rows: remove (keep serverId so the server deletes them). + for (const s of rows) { + s.enabled = false + s.deleted = true + } + } + g[key] = rows + } + if (useCustom) { + let custom = groups.find((g) => !g.operator) + if (!custom) { + custom = { smpServers: [], xftpServers: [], chatRelays: [] } + groups.push(custom) + } + for (const server of uris) { + custom[key].push({ server, preset: false, enabled: true, deleted: false }) + } + } +} + +/** + * Apply the selected SMP/XFTP relays to the running client over the WS API, + * making the chat database — not the removed `--server` env — authoritative. + * Sets custom/local servers and resets to the public presets, per protocol. + * + * Runs only on (re)start (the post-ready one-shot in main.ts) and on the + * Configure action when relays change (which applies live — no restart). + */ +export async function configureServers( + effects: T.Effects, + settings: ClientSettings, +): Promise { + const { smp, xftp } = await resolveServerUris(effects, settings) + await withBotSession(effects, async (send) => { + const { userId } = activeUser(await send('/user')) + + const getEnv = await send(`/_servers ${userId}`) + assertNotCmdError(getEnv, 'reading server configuration') + // `userServers` isn't in the bots-subset ChatResponse union, so read the + // envelope through a loose shape. + const resp = getEnv.resp as unknown as + | { type?: string; userServers?: ServerGroup[] } + | undefined + if (resp?.type !== 'userServers' || !Array.isArray(resp.userServers)) { + throw new Error(`Unexpected response reading servers: ${resp?.type}`) + } + const userServers = resp.userServers + + applyProtocol(userServers, 'smpServers', smp) + applyProtocol(userServers, 'xftpServers', xftp) + + assertNotCmdError( + await send(`/_servers ${userId} ${JSON.stringify(userServers)}`), + 'server configuration', + ) + }) +} + +export async function syncClientSettings( + effects: T.Effects, + settings: ClientSettings, +): Promise { + return withBotSession(effects, async (send) => { + const result: SyncResult = { + profileUpdated: false, + addressCreated: false, + addressSettingsUpdated: false, + } + + // ---- Profile (display name, full name, avatar, peer type) ---- + const { userId, profile } = activeUser(await send('/user')) + + const desiredImage = settings.image.trim() || undefined + const desiredPeerType = settings.peerType as SX.ChatPeerType + const profileDrifted = + (profile.displayName ?? '') !== settings.displayName || + (profile.fullName ?? '') !== settings.fullName || + (profile.image ?? undefined) !== desiredImage || + (profile.peerType ?? 'bot') !== desiredPeerType + + if (profileDrifted) { + // Apply edits on top of the live profile so preferences (e.g. file + // sharing) and any fields we don't manage ride through untouched. + const newProfile: SX.Profile = { + ...profile, + displayName: settings.displayName, + fullName: settings.fullName, + image: desiredImage, + peerType: desiredPeerType, + } + const env = await send( + CC.APIUpdateProfile.cmdString({ userId, profile: newProfile }), + ) + assertNotCmdError(env, 'profile update') + if (env.resp?.type !== 'userProfileNoChange') result.profileUpdated = true + } + + // ---- Address (create if missing) + settings (auto-accept, business, welcome) ---- + let showEnv = await send(CC.APIShowMyAddress.cmdString({ userId })) + if (showEnv.resp?.type !== 'userContactLink') { + // No address yet — create one, then re-read it. + const created = await send(CC.APICreateMyAddress.cmdString({ userId })) + assertNotCmdError(created, 'address creation') + result.addressCreated = true + showEnv = await send(CC.APIShowMyAddress.cmdString({ userId })) + } + + if (showEnv.resp?.type === 'userContactLink') { + const current = showEnv.resp.contactLink.addressSettings + const desired: SX.AddressSettings = { + businessAddress: settings.businessMode, + // Auto-accept present => accept incoming requests (non-incognito). + // Absent => manual acceptance. + autoAccept: settings.autoAcceptContacts + ? { acceptIncognito: false } + : undefined, + autoReply: settings.welcomeMessage.trim() + ? { type: 'text', text: settings.welcomeMessage.trim() } + : undefined, + } + + const drifted = + (current.businessAddress ?? false) !== desired.businessAddress || + !!current.autoAccept !== !!desired.autoAccept || + welcomeText(current.autoReply) !== welcomeText(desired.autoReply) + + if (drifted) { + const env = await send( + CC.APISetAddressSettings.cmdString({ userId, settings: desired }), + ) + assertNotCmdError(env, 'address settings update') + result.addressSettingsUpdated = true + } + } + + return result + }) +} diff --git a/startos/main.ts b/startos/main.ts index 7e93f9b..39bb2db 100644 --- a/startos/main.ts +++ b/startos/main.ts @@ -1,10 +1,22 @@ import { sdk } from './sdk' import { port, mainMounts } from './utils' import { i18n } from './i18n' +import { readClientSettings } from './fileModels/clientSettings.json' +import { computeStartEnv } from './serverConfig' +import { syncClientSettings, configureServers } from './liveSync' +import { waitForBotReady } from './bot-client' export const main = sdk.setupMain(async ({ effects }) => { console.info(i18n('Starting SimpleX Websocket Bridge!')) + // Compute the container's start environment from the saved client settings + // (or code defaults on a fresh install). Seeds the profile on first start and + // applies relay / retention choices every start. Throws (failing start) if + // the settings ask for something unresolvable — e.g. Local relays, which + // aren't implemented yet — rather than silently using public presets. + const settings = await readClientSettings(effects) + const env = await computeStartEnv(effects, settings) + const subcontainer = await sdk.SubContainer.of( effects, { imageId: 'simplex' }, @@ -18,6 +30,7 @@ export const main = sdk.setupMain(async ({ effects }) => { subcontainer, exec: { command: sdk.useEntrypoint(), + env, }, ready: { display: null, // surfaced to users (and dependents) via the 'websocket' health check below @@ -43,5 +56,37 @@ export const main = sdk.setupMain(async ({ effects }) => { }, requires: ['simplex'], }) + // Once the WebSocket is reachable, reconcile the running client's profile + // and address settings with the saved settings — the fields the image + // can't seed from env (full name, avatar, business mode, auto-accept, + // welcome message). Best-effort: a transient failure here must not wedge + // startup, so we log and move on; the next Configure submit or restart + // reconciles again. + .addOneshot('sync-settings', { + subcontainer, + exec: { + fn: async () => { + try { + // The daemon `requires` gate orders launch, not socket readiness, + // so wait for the WebSocket to actually answer before syncing — + // otherwise the first connect races websocat's bind (ECONNREFUSED). + await waitForBotReady(effects) + // Apply the selected relays first (authoritative over the DB — + // sets custom/local, resets public), then reconcile the profile. + await configureServers(effects, settings) + await syncClientSettings(effects, settings) + console.info(i18n('SimpleX client settings synced')) + } catch (err) { + console.warn( + i18n('Could not sync SimpleX client settings: ').concat( + (err as Error).message, + ), + ) + } + return null + }, + }, + requires: ['simplex'], + }) ) }) diff --git a/startos/manifest/index.ts b/startos/manifest/index.ts index 35aa77e..9246744 100644 --- a/startos/manifest/index.ts +++ b/startos/manifest/index.ts @@ -26,5 +26,20 @@ export const manifest = setupManifest({ arch: ['x86_64', 'aarch64'], }, }, - dependencies: {}, + // Declared optional; setupDependencies (dependencies.ts) flips `simplex` to a + // `running` dependency when the user picks self-hosted (Local) relays in the + // Configure action. Public and Custom relays need no dependency. + dependencies: { + simplex: { + optional: true, + description: { + en_US: + 'Optional: relay your messages and files through your own self-hosted SimpleX Server instead of the public servers. Select "My self-hosted SimpleX Server" in the Configure Client action.', + }, + metadata: { + title: 'SimpleX Server', + icon: 'https://raw.githubusercontent.com/Start9Labs/simplex-startos/master/icon.svg', + }, + }, + }, }) diff --git a/startos/serverConfig.ts b/startos/serverConfig.ts new file mode 100644 index 0000000..ca054ab --- /dev/null +++ b/startos/serverConfig.ts @@ -0,0 +1,135 @@ +import { T } from '@start9labs/start-sdk' +import { sdk } from './sdk' +import { ClientSettings } from './fileModels/clientSettings.json' + +/** + * Translate persisted client settings into the container's start environment. + * + * The Docker image (lundog/simplex-websocket-bridge) reads these on start: + * PROFILE_DISPLAY_NAME / PROFILE_PEER_TYPE — seed the profile on FIRST start + * only; afterwards the profile lives in the DB and is edited over the + * WebSocket API (see liveSync.ts). Passing them every start is harmless. + * INBOUND_RETENTION_HOURS — positive integer; a janitor deletes received + * files older than this. Unset = keep forever. + * + * Message relays are deliberately NOT passed as env. simplex-chat persists the + * `--server`/`--xftp-server` values into the per-user chat database, and the + * built-in presets are only used when the DB has NO configured servers — so + * once a custom/local server is set via the flag it sticks even after the flag + * is removed, and dropping the flag never reverts to the public presets. To + * make the selection authoritative we instead apply it over the WebSocket API + * on every (re)start — see configureServers in liveSync.ts — which both sets + * custom/local servers and resets to presets for public. + */ + +/** StartOS package id of the self-hosted SimpleX Server (Local relays). */ +export const SIMPLEX_SERVER_PACKAGE_ID = 'simplex' + +// Service-interface IDs the SimpleX Server (simplex) package exports. Each URI +// it produces is a full relay address including the server's CA fingerprint and +// basic-auth password (username = ":", scheme "smp"/ +// "xftp"), which is exactly what simplex-chat's --server / --xftp-server want. +// See Start9Labs/simplex-startos startos/interfaces.ts. +const SMP_INTERFACE_ID = 'smp' +const XFTP_INTERFACE_ID = 'xftp' + +/** + * Best available full URI for one exported relay interface. + * + * Preference order (issue #3): clearnet domain, then clearnet IP, then Tor + * onion (exported as a plugin hostname), then .local (mDNS, LAN-only) as a last + * resort. A contact must be able to reach the SMP relay to deliver messages, so + * a LAN-only address only works for same-network peers — hence it ranks last. + */ +async function resolveLocalRelayUri( + effects: T.Effects, + interfaceId: string, +): Promise { + const iface = await sdk.serviceInterface + .get(effects, { packageId: SIMPLEX_SERVER_PACKAGE_ID, id: interfaceId }) + .const() + + const addressInfo = iface?.addressInfo + if (!addressInfo) { + throw new Error( + `SimpleX Server did not expose its "${interfaceId}" address. Make sure the SimpleX Server package is installed and running.`, + ) + } + + // Literal filters here so each satisfies the .filter() generic; ordered + // from most to least universally reachable. + const tiers = [ + addressInfo.filter({ kind: 'domain', visibility: 'public' }), + addressInfo.filter({ kind: 'ip', visibility: 'public' }), + addressInfo.filter({ kind: 'plugin' }), + addressInfo.filter({ kind: 'mdns' }), + ] + for (const tier of tiers) { + const urls = tier.format('urlstring') + if (urls.length) return urls[0] + } + + throw new Error( + `SimpleX Server exposed its "${interfaceId}" interface but no reachable address (clearnet, Tor, or .local) was found.`, + ) +} + +/** Full SMP/XFTP relay URIs for the selected mode, to apply over the WS API. */ +export interface ResolvedServerUris { + smp: string[] + xftp: string[] +} + +/** + * Resolve the SMP/XFTP relay URIs for the selected server mode. An empty list + * for a protocol means "use SimpleX's public presets" (the caller issues a + * reset for that protocol). + * + * public — no custom URIs (reset both protocols to presets). + * custom — the user's URIs (each already a full `smp://@host` + * / `xftp://@host`); an empty side resets to presets. + * local — auto-pull the user's own SimpleX Server SMP/XFTP addresses from + * its StartOS service interfaces (full URIs, fingerprint included). + * Fails fast if a relay can't be resolved rather than silently + * falling back to presets — choosing self-hosted relays is a + * deliberate opt-out. + */ +export async function resolveServerUris( + effects: T.Effects, + settings: ClientSettings, +): Promise { + const clean = (list: string[]) => list.map((s) => s.trim()).filter(Boolean) + const { mode, smp, xftp } = settings.servers + + if (mode === 'custom') return { smp: clean(smp), xftp: clean(xftp) } + + if (mode === 'local') { + const [smpUri, xftpUri] = await Promise.all([ + resolveLocalRelayUri(effects, SMP_INTERFACE_ID), + resolveLocalRelayUri(effects, XFTP_INTERFACE_ID), + ]) + return { smp: [smpUri], xftp: [xftpUri] } + } + + // public + return { smp: [], xftp: [] } +} + +/** Full start environment for the simplex daemon, derived from settings. */ +export async function computeStartEnv( + _effects: T.Effects, + settings: ClientSettings, +): Promise> { + const env: Record = { + PROFILE_DISPLAY_NAME: settings.displayName, + PROFILE_PEER_TYPE: settings.peerType, + } + + // The image's janitor takes hours; the form collects days for a friendlier + // UX. null / 0 => omit (keep files forever). + if (settings.cleanupDays && settings.cleanupDays > 0) { + env.INBOUND_RETENTION_HOURS = String(settings.cleanupDays * 24) + } + + return env +} diff --git a/startos/versions/current.ts b/startos/versions/current.ts index a28cce5..87f570e 100644 --- a/startos/versions/current.ts +++ b/startos/versions/current.ts @@ -4,39 +4,59 @@ export const current = VersionInfo.of({ version: '0.3.0:0', releaseNotes: { en_US: [ - 'Bundles simplex-chat v6.5.5 and improves connection reliability and the file-exchange contract.', + 'Adds client configuration and SimpleX address management, bundles simplex-chat v6.5.5, and reworks the file-exchange contract.', '', '- Bundles simplex-chat v6.5.5 (previously v6.5.4).', - '- More reliable connections: large SimpleX events (for example, when a new contact connects) are no longer split into invalid WebSocket frames.', - '- File exchange reworked for consumer packages: received files are shared via the .simplex/files subpath (mounted read-only, resolved by name) and outgoing files via the .simplex/outbound subpath (the consumer stages the file and translates the path). See the README.', + '- New "Configure Client" action: set the display name, full name, profile picture (from an image URL, data URL, or base64 — auto-cropped to a square and shrunk), peer type, auto-accept contact requests, business mode, welcome message, message relays, and received-file cleanup. Most changes apply live, with no restart.', + "- Message relays: use SimpleX's public servers, your own self-hosted SimpleX Server (via an optional dependency), or custom SMP/XFTP addresses — switching between them applies reliably, including back to public.", + '- New "View Address" and "Reset Address" actions for the long-lived, reusable SimpleX address, alongside the existing one-time "Create Invitation".', + '- "Reset Profile" is now "Reset Client", with clearer warnings about reconnecting contacts.', + '- More reliable connections: large SimpleX events are no longer split into invalid WebSocket frames.', + '- File exchange reworked for consumer packages: received and outgoing files via the .simplex/files and .simplex/outbound subpaths. See the README.', ].join('\n'), es_ES: [ - 'Incluye simplex-chat v6.5.5 y mejora la fiabilidad de las conexiones y el contrato de intercambio de archivos.', + 'Añade la configuración del cliente y la gestión de direcciones de SimpleX, incluye simplex-chat v6.5.5 y rediseña el contrato de intercambio de archivos.', '', '- Incluye simplex-chat v6.5.5 (antes v6.5.4).', - '- Conexiones más fiables: los eventos grandes de SimpleX (por ejemplo, cuando un nuevo contacto se conecta) ya no se dividen en tramas WebSocket no válidas.', - '- Intercambio de archivos rediseñado para paquetes consumidores: los archivos recibidos se comparten mediante la subruta .simplex/files (montada en solo lectura, resuelta por nombre) y los archivos salientes mediante la subruta .simplex/outbound (el consumidor prepara el archivo y traduce la ruta). Consulte el README.', + '- Nueva acción «Configurar cliente»: define el nombre visible, el nombre completo, la imagen de perfil (desde una URL de imagen, data URL o base64, recortada a un cuadrado y reducida automáticamente), el tipo de par, la aceptación automática de solicitudes de contacto, el modo empresa, el mensaje de bienvenida, los relés de mensajes y la limpieza de archivos recibidos. La mayoría de los cambios se aplican en vivo, sin reinicio.', + '- Relés de mensajes: usa los servidores públicos de SimpleX, tu propio servidor SimpleX autoalojado (mediante una dependencia opcional) o direcciones SMP/XFTP personalizadas; cambiar entre ellos se aplica de forma fiable, incluido volver a los públicos.', + '- Nuevas acciones «Ver dirección» y «Restablecer dirección» para la dirección SimpleX de larga duración y reutilizable, junto a la ya existente «Crear invitación» de un solo uso.', + '- «Restablecer perfil» ahora es «Restablecer cliente», con advertencias más claras sobre la reconexión de contactos.', + '- Conexiones más fiables: los eventos grandes de SimpleX ya no se dividen en tramas WebSocket no válidas.', + '- Intercambio de archivos rediseñado para paquetes consumidores: archivos recibidos y salientes mediante las subrutas .simplex/files y .simplex/outbound. Consulte el README.', ].join('\n'), de_DE: [ - 'Enthält simplex-chat v6.5.5 und verbessert die Verbindungszuverlässigkeit sowie den Dateiaustausch-Vertrag.', + 'Fügt Client-Konfiguration und SimpleX-Adressverwaltung hinzu, enthält simplex-chat v6.5.5 und überarbeitet den Dateiaustausch-Vertrag.', '', '- Enthält simplex-chat v6.5.5 (zuvor v6.5.4).', - '- Zuverlässigere Verbindungen: Große SimpleX-Ereignisse (zum Beispiel, wenn ein neuer Kontakt eine Verbindung herstellt) werden nicht mehr in ungültige WebSocket-Frames aufgeteilt.', - '- Dateiaustausch für konsumierende Pakete überarbeitet: Empfangene Dateien werden über den Unterpfad .simplex/files (schreibgeschützt eingebunden, anhand des Namens aufgelöst) und ausgehende Dateien über den Unterpfad .simplex/outbound geteilt (der Konsument legt die Datei ab und übersetzt den Pfad). Siehe README.', + '- Neue Aktion „Client konfigurieren“: Anzeigename, vollständiger Name, Profilbild (aus Bild-URL, Data-URL oder base64 — automatisch quadratisch zugeschnitten und verkleinert), Peer-Typ, automatisches Annehmen von Kontaktanfragen, Geschäftsmodus, Willkommensnachricht, Nachrichten-Relays und Bereinigung empfangener Dateien festlegen. Die meisten Änderungen werden ohne Neustart angewendet.', + '- Nachrichten-Relays: nutze die öffentlichen SimpleX-Server, deinen eigenen selbst gehosteten SimpleX-Server (über eine optionale Abhängigkeit) oder eigene SMP/XFTP-Adressen; der Wechsel dazwischen wird zuverlässig angewendet, auch zurück zu den öffentlichen.', + '- Neue Aktionen „Adresse anzeigen“ und „Adresse zurücksetzen“ für die langlebige, wiederverwendbare SimpleX-Adresse, neben der bestehenden einmaligen „Einladung erstellen“.', + '- „Profil zurücksetzen“ heißt jetzt „Client zurücksetzen“, mit klareren Hinweisen zum erneuten Verbinden von Kontakten.', + '- Zuverlässigere Verbindungen: große SimpleX-Ereignisse werden nicht mehr in ungültige WebSocket-Frames aufgeteilt.', + '- Dateiaustausch für konsumierende Pakete überarbeitet: empfangene und ausgehende Dateien über die Unterpfade .simplex/files und .simplex/outbound. Siehe README.', ].join('\n'), pl_PL: [ - 'Zawiera simplex-chat v6.5.5 oraz poprawia niezawodność połączeń i kontrakt wymiany plików.', + 'Dodaje konfigurację klienta i zarządzanie adresami SimpleX, zawiera simplex-chat v6.5.5 oraz przebudowuje kontrakt wymiany plików.', '', '- Zawiera simplex-chat v6.5.5 (poprzednio v6.5.4).', - '- Bardziej niezawodne połączenia: duże zdarzenia SimpleX (na przykład gdy łączy się nowy kontakt) nie są już dzielone na nieprawidłowe ramki WebSocket.', - '- Przebudowano wymianę plików dla pakietów konsumujących: pliki odebrane są udostępniane przez podścieżkę .simplex/files (montowaną tylko do odczytu, rozwiązywaną po nazwie), a pliki wychodzące przez podścieżkę .simplex/outbound (konsument przygotowuje plik i tłumaczy ścieżkę). Zobacz README.', + '- Nowa akcja „Konfiguruj klienta”: ustaw nazwę wyświetlaną, imię i nazwisko, obraz profilu (z adresu URL obrazu, data URL lub base64 — automatycznie przycięty do kwadratu i pomniejszony), typ węzła, automatyczne akceptowanie próśb o kontakt, tryb biznesowy, wiadomość powitalną, przekaźniki wiadomości i czyszczenie odebranych plików. Większość zmian jest stosowana na żywo, bez ponownego uruchamiania.', + '- Przekaźniki wiadomości: użyj publicznych serwerów SimpleX, własnego samodzielnie hostowanego serwera SimpleX (przez opcjonalną zależność) lub niestandardowych adresów SMP/XFTP; przełączanie między nimi jest stosowane niezawodnie, także z powrotem na publiczne.', + '- Nowe akcje „Pokaż adres” i „Zresetuj adres” dla długoterminowego, wielokrotnego adresu SimpleX, obok istniejącej jednorazowej „Utwórz zaproszenie”.', + '- „Zresetuj profil” to teraz „Zresetuj klienta”, z jaśniejszymi ostrzeżeniami o ponownym łączeniu kontaktów.', + '- Bardziej niezawodne połączenia: duże zdarzenia SimpleX nie są już dzielone na nieprawidłowe ramki WebSocket.', + '- Przebudowano wymianę plików dla pakietów konsumujących: pliki odebrane i wychodzące przez podścieżki .simplex/files i .simplex/outbound. Zobacz README.', ].join('\n'), fr_FR: [ - "Inclut simplex-chat v6.5.5 et améliore la fiabilité des connexions ainsi que le contrat d'échange de fichiers.", + "Ajoute la configuration du client et la gestion des adresses SimpleX, inclut simplex-chat v6.5.5 et repense le contrat d'échange de fichiers.", '', '- Inclut simplex-chat v6.5.5 (auparavant v6.5.4).', - "- Connexions plus fiables : les événements SimpleX volumineux (par exemple lorsqu'un nouveau contact se connecte) ne sont plus fractionnés en trames WebSocket invalides.", - '- Échange de fichiers repensé pour les paquets consommateurs : les fichiers reçus sont partagés via le sous-chemin .simplex/files (monté en lecture seule, résolu par nom) et les fichiers sortants via le sous-chemin .simplex/outbound (le consommateur prépare le fichier et traduit le chemin). Voir le README.', + "- Nouvelle action « Configurer le client » : définit le nom affiché, le nom complet, l'image de profil (depuis une URL d'image, une data URL ou base64 — recadrée en carré et réduite automatiquement), le type de pair, l'acceptation automatique des demandes de contact, le mode entreprise, le message de bienvenue, les relais de messages et le nettoyage des fichiers reçus. La plupart des changements s'appliquent en direct, sans redémarrage.", + "- Relais de messages : utilisez les serveurs publics de SimpleX, votre propre serveur SimpleX auto-hébergé (via une dépendance optionnelle) ou des adresses SMP/XFTP personnalisées ; le basculement entre eux s'applique de façon fiable, y compris le retour aux serveurs publics.", + "- Nouvelles actions « Afficher l'adresse » et « Réinitialiser l'adresse » pour l'adresse SimpleX durable et réutilisable, en plus de l'action « Créer une invitation » à usage unique existante.", + '- « Réinitialiser le profil » devient « Réinitialiser le client », avec des avertissements plus clairs sur la reconnexion des contacts.', + '- Connexions plus fiables : les événements SimpleX volumineux ne sont plus fractionnés en trames WebSocket invalides.', + '- Échange de fichiers repensé pour les paquets consommateurs : fichiers reçus et sortants via les sous-chemins .simplex/files et .simplex/outbound. Voir le README.', ].join('\n'), }, migrations: { From a04f275840e1411d07b122e9c99f2f9285f9a669 Mon Sep 17 00:00:00 2001 From: lundog <8041501+lundog@users.noreply.github.com> Date: Sat, 4 Jul 2026 01:57:21 -0600 Subject: [PATCH 5/6] Add hands-off (app-managed) profile mode; prefix action names with "SimpleX" Hands-off mode lets a non-OpenClaw application own the SimpleX client while still using the bridge as transport. - New manageProfile setting (default true). In Configure Client the profile fields are wrapped in a "SimpleX Profile" union: "Managed by StartOS" (current behavior) or "Managed by my application". - When app-managed, the bridge makes NO WebSocket writes: main skips the post-ready sync one-shot entirely, so the profile, address, and any runtime server changes are the app's to make over the WS. computeStartEnv instead sets SMP_SERVERS/XFTP_SERVERS (from the selected relays) and INBOUND_RETENTION_HOURS via container env. Relay/retention/mode changes take effect on restart (no live /_servers round-trip in this mode). - Caveat documented: env relays are set-once (--server persists in the DB), so reverting to public is the app's responsibility in this mode. Action name polish (display names only; ids/forms unchanged): - "Create Invitation" -> "Create SimpleX Invitation" - "View Address" -> "View SimpleX Address" - "Reset Address" -> "Reset SimpleX Address" (also its confirmation title) Updates i18n across all five languages, README, and instructions.md. --- README.md | 23 ++-- instructions.md | 16 +-- startos/actions/configureClient.ts | 132 +++++++++++++++------ startos/actions/create-invitation.ts | 2 +- startos/actions/reset-address.ts | 2 +- startos/actions/view-address.ts | 2 +- startos/fileModels/clientSettings.json.ts | 9 ++ startos/i18n/dictionaries/default.ts | 14 ++- startos/i18n/dictionaries/translations.ts | 48 +++++--- startos/main.ts | 138 +++++++++++----------- startos/serverConfig.ts | 12 +- startos/versions/current.ts | 5 + 12 files changed, 256 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index 19bf4a9..1c9a7f5 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Both directions mount a subpath at whatever path the consumer likes — the brid 1. On install, the bridge seeds **one API key** so outside access is gated from first start; copy it from the **API Keys** action. 2. On first start, the bundled client auto-creates a fresh SimpleX profile (marked as a bot by default). -3. Copy the Websocket URL from **Interfaces → Websocket**, point a client at it with the API key as a bearer token, and drive it with the SimpleX bot/chat protocol. To hand a contact a connection link, run the **Create Invitation** action. +3. Copy the Websocket URL from **Interfaces → Websocket**, point a client at it with the API key as a bearer token, and drive it with the SimpleX bot/chat protocol. To hand a contact a connection link, run the **Create SimpleX Invitation** action. --- @@ -82,8 +82,9 @@ Both directions mount a subpath at whatever path the consumer likes — the brid Client settings are saved to `client-settings.json` by the **Configure Client** action and applied two ways depending on the field. -- **Profile & address** (display name, full name, picture, peer type, auto-accept contact requests, business mode, welcome message) — pushed to the running client live over the Websocket, no restart. Run the action before first start to seed the identity. -- **Message relays** (public / self-hosted SimpleX Server / custom SMP+XFTP) — applied live over the client's API on save, no restart. SimpleX persists the relay choice in its database and only uses the public presets when none are set, so switching back to public actively resets to the presets rather than relying on removing a flag. Relays are also re-applied on every (re)start so the database stays authoritative. +- **Profile management mode** — by default StartOS manages the SimpleX profile and address (below). Setting the Configure Client "SimpleX Profile" option to _Managed by my application_ switches to a hands-off transport: StartOS makes **no** WebSocket writes, and message relays + received-file cleanup are applied via container env at start. Your own application (over the Websocket) owns the profile, address, and any runtime server changes. Useful for driving the bridge from a non-OpenClaw app that manages its own identity. +- **Profile & address** (display name, full name, picture, peer type, auto-accept contact requests, business mode, welcome message) — in managed mode, pushed to the running client live over the Websocket, no restart. Run the action before first start to seed the identity. +- **Message relays** (public / self-hosted SimpleX Server / custom SMP+XFTP) — in managed mode, applied live over the client's API on save, no restart. SimpleX persists the relay choice in its database and only uses the public presets when none are set, so switching back to public actively resets to the presets rather than relying on removing a flag. Relays are re-applied on every (re)start so the database stays authoritative. In hands-off mode relays are set via container env instead (set-once: `--server` persists in the DB, so reverting to public is then the app's responsibility). - **Received-file retention** — a container/janitor setting read at launch, so changing it saves and then restarts the service. - **API keys** — stored in `store.json` and managed via the **API Keys** action. Editing keys re-binds the reverse proxy's accepted-token set with no restart. @@ -107,14 +108,14 @@ Client settings are saved to `client-settings.json` by the **Configure Client** ## Actions (StartOS UI) -| Action | Group | Purpose | -| --------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Configure Client** | General | Set the client identity and behavior — display name, full name, picture, peer type, auto-accept contact requests, business mode, welcome message, message relays (public / self-hosted / custom), and received-file retention. Run before first start. Profile, address, and relay edits apply live; a received-file-retention change restarts the service. | -| **Create Invitation** | General | Drive the running client to mint a fresh one-time SimpleX invitation link (with QR). One redemption per link. | -| **View Address** | General | Show the client's long-lived, reusable SimpleX address (with QR), creating one if it doesn't exist yet. Unlike an invitation, the same address works for any number of contacts. | -| **API Keys** | General | Manage the bearer tokens that gate outside access — add a labeled key per client, delete to revoke. | -| **Reset Client** | Danger Zone | Wipe the SimpleX identity, contacts, and chat history. Service must be stopped. API keys are preserved. Contacts must reconnect afterward; if used with OpenClaw, purge the channel's per-contact state first (SimpleX reuses low contact ids). | -| **Reset Address** | Danger Zone | Replace the long-lived address with a new one (runs live, no stop needed). The old address link stops working; existing contacts are unaffected. | +| Action | Group | Purpose | +| ----------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Configure Client** | General | Set the client identity and behavior — display name, full name, picture, peer type, auto-accept contact requests, business mode, welcome message, message relays (public / self-hosted / custom), and received-file retention. Run before first start. Profile, address, and relay edits apply live; a received-file-retention change restarts the service. | +| **Create SimpleX Invitation** | General | Drive the running client to mint a fresh one-time SimpleX invitation link (with QR). One redemption per link. | +| **View SimpleX Address** | General | Show the client's long-lived, reusable SimpleX address (with QR), creating one if it doesn't exist yet. Unlike an invitation, the same address works for any number of contacts. | +| **API Keys** | General | Manage the bearer tokens that gate outside access — add a labeled key per client, delete to revoke. | +| **Reset Client** | Danger Zone | Wipe the SimpleX identity, contacts, and chat history. Service must be stopped. API keys are preserved. Contacts must reconnect afterward; if used with OpenClaw, purge the channel's per-contact state first (SimpleX reuses low contact ids). | +| **Reset SimpleX Address** | Danger Zone | Replace the long-lived address with a new one (runs live, no stop needed). The old address link stops working; existing contacts are unaffected. | --- diff --git a/instructions.md b/instructions.md index c3a6597..7bf27c2 100644 --- a/instructions.md +++ b/instructions.md @@ -20,7 +20,7 @@ This service has no human chat interface — it is driven entirely by your own s 2. Open the **API Keys** action and copy the key created on install (or add your own). Outside clients send it as `Authorization: Bearer `. 3. Open **Interfaces → Websocket** and copy the URL StartOS publishes for your network (LAN, Tor, etc.). 4. Connect any Websocket client to that URL with the bearer token, then drive it with the SimpleX protocol (see Documentation). -5. To give someone a way to reach the bridge, run the **Create Invitation** action and share the link or QR — they paste it into their SimpleX client. +5. To give someone a way to reach the bridge, run the **Create SimpleX Invitation** action and share the link or QR — they paste it into their SimpleX client. On-box StartOS services that depend on this package connect directly and do not need an API key. @@ -34,6 +34,8 @@ Manage tokens in the **API Keys** action — each has a label (to identify the c Run the **Configure Client** action to set the client's identity and behavior. It writes a settings file the service reads on start, so you can run it **before the first start** to seed the identity. +**Who manages the profile?** The "SimpleX Profile" option chooses between _Managed by StartOS_ (default) and _Managed by my application_. Pick the latter to drive the bridge from your own software that manages its own SimpleX identity: StartOS then makes no changes to the running client over the Websocket, and only applies message relays and received-file cleanup via container settings at startup. The profile fields below apply only in the StartOS-managed mode. + - **Display Name** / **Full Name** — how the client presents to contacts. - **Profile Picture** — an image URL (http/https), a data URL, or base64; it is cropped to a square and shrunk automatically to fit SimpleX's avatar limit. Leave empty to remove it. - **Peer Type** — Bot or Human (cosmetic; both transfer files and messages either way). @@ -50,23 +52,23 @@ Choosing **SimpleX defaults (public)**, **self-hosted**, or **custom** relays is - **New contacts and new invitation/address links** use the selected relays. - **Existing contacts keep working** over the connections they already have. In SimpleX a message queue lives on the server the _recipient_ chose when the connection was made, and it doesn't move; selecting new relays disables the old ones for _new_ queues but doesn't delete or stop existing ones. Messages are never re-routed between your servers by this setting — a sender only adds its own forwarding relay to hide its IP, still delivering to the queue's original server. -- Your **existing published address** likewise stays on its original server — run **Reset Address** to mint a new one on the current relays. +- Your **existing published address** likewise stays on its original server — run **Reset SimpleX Address** to mint a new one on the current relays. When moving to a self-hosted server, keep two things in mind: -- If you later **shut the old server down**, any contact or address still hosted there stops working; reconnect them (and Reset Address) on the new relays first. +- If you later **shut the old server down**, any contact or address still hosted there stops working; reconnect them (and Reset SimpleX Address) on the new relays first. - A self-hosted relay must be **reachable by your contacts and by this client**. A LAN-only/private address only works for peers on that network; to reach outside contacts, publish the server on clearnet or Tor. ## Adding a contact There are two ways to hand out a connection link: -- **Create Invitation** mints a fresh one-time link (with QR). Each link can be redeemed by exactly one peer — run it again to invite another person. -- **View Address** shows the client's long-lived, reusable address (with QR), creating one if needed. The same address works for any number of contacts, so it's convenient to publish. To accept incoming requests automatically, enable **Auto-Accept Contact Requests** in Configure Client. +- **Create SimpleX Invitation** mints a fresh one-time link (with QR). Each link can be redeemed by exactly one peer — run it again to invite another person. +- **View SimpleX Address** shows the client's long-lived, reusable address (with QR), creating one if needed. The same address works for any number of contacts, so it's convenient to publish. To accept incoming requests automatically, enable **Auto-Accept Contact Requests** in Configure Client. Send the link or QR to the person you want to connect; they paste it into their SimpleX client. -If a published address is ever over-shared or abused, run **Reset Address** (Danger Zone) to replace it with a new one — the service keeps running, existing contacts are unaffected, and only the old link stops working. (See [Switching message relays](#switching-message-relays) for why you may also want to reset the address after changing servers.) +If a published address is ever over-shared or abused, run **Reset SimpleX Address** (Danger Zone) to replace it with a new one — the service keeps running, existing contacts are unaffected, and only the old link stops working. (See [Switching message relays](#switching-message-relays) for why you may also want to reset the address after changing servers.) ## Connecting programmatically @@ -79,7 +81,7 @@ The bridge does not accept raw command strings; wrap every command in a small JS - `corrId` is a correlation id you choose; the bridge echoes it back in the matching reply so you can pair requests with responses. - `cmd` is the SimpleX command, exactly as in the `simplex-chat` CLI (leading slash included). -Each reply is a JSON object with the same `corrId` and a `resp` field. The bridge also pushes unsolicited event messages (without your `corrId`) for incoming chats, contact updates, and the like. A few useful commands: `/user` (the active user), `/_connect 1` (a one-time invitation link for user 1 — what **Create Invitation** does), and `/contacts` (connected peers). +Each reply is a JSON object with the same `corrId` and a `resp` field. The bridge also pushes unsolicited event messages (without your `corrId`) for incoming chats, contact updates, and the like. A few useful commands: `/user` (the active user), `/_connect 1` (a one-time invitation link for user 1 — what **Create SimpleX Invitation** does), and `/contacts` (connected peers). ## Resetting diff --git a/startos/actions/configureClient.ts b/startos/actions/configureClient.ts index 02e3ec7..e86b2b4 100644 --- a/startos/actions/configureClient.ts +++ b/startos/actions/configureClient.ts @@ -53,7 +53,10 @@ const customServersSpec = InputSpec.of({ }), }) -const inputSpec = InputSpec.of({ +// The profile/address fields StartOS manages over the WebSocket. Grouped under +// a union so they hide entirely when the operator's own application owns the +// profile (hands-off mode). +const managedProfileSpec = InputSpec.of({ displayName: Value.text({ name: i18n('Display Name'), description: i18n('The name peers see when they connect to your client.'), @@ -124,10 +127,27 @@ const inputSpec = InputSpec.of({ maxLength: 2000, patterns: [], }), +}) + +const inputSpec = InputSpec.of({ + profile: Value.union({ + name: i18n('SimpleX Profile'), + description: i18n( + 'Choose whether StartOS manages the client profile and address, or leaves them to your own application. When your application manages them, StartOS makes no changes to the running client over the Websocket — it only applies message relays and file cleanup at startup.', + ), + default: 'managed', + variants: Variants.of({ + managed: { name: i18n('Managed by StartOS'), spec: managedProfileSpec }, + unmanaged: { + name: i18n('Managed by my application'), + spec: InputSpec.of({}), + }, + }), + }), servers: Value.union({ name: i18n('Message Relays (SMP/XFTP)'), description: i18n( - 'Which servers relay your messages and files. Applied immediately (no restart) and only to NEW connections — existing contacts and your current address keep using the server they were created on. Use Reset Address to move your address onto the new relays.', + 'Which servers relay your messages and files. Applied immediately (no restart) and only to NEW connections — existing contacts and your current address keep using the server they were created on. Use Reset SimpleX Address to move your address onto the new relays.', ), default: 'public', variants: Variants.of({ @@ -182,16 +202,27 @@ export const configureClient = sdk.Action.withInput( // install where the file doesn't exist yet. async ({ effects }) => { const s = await readClientSettings(effects) - return { + // Prefill the stored avatar so it's visible/editable: leaving it unchanged + // keeps it, emptying it removes it, replacing it re-processes (see run()). + const profileValue = { displayName: s.displayName, fullName: s.fullName || null, - // Prefill the stored avatar so it's visible/editable: leaving it unchanged - // keeps it, emptying it removes it, replacing it re-processes (see run()). image: s.image || null, peerType: s.peerType, autoAcceptContacts: s.autoAcceptContacts, businessMode: s.businessMode, welcomeMessage: s.welcomeMessage || null, + } + return { + // When unmanaged, carry the stored profile in `other` so switching back + // to Managed in the form restores it instead of showing defaults. + profile: s.manageProfile + ? { selection: 'managed' as const, value: profileValue } + : { + selection: 'unmanaged' as const, + value: {}, + other: { managed: profileValue }, + }, servers: s.servers.mode === 'custom' ? { @@ -209,29 +240,48 @@ export const configureClient = sdk.Action.withInput( }, async ({ effects, input }) => { // Read what was saved before: needed to tell which kind of change this is, - // and to retain the existing avatar when no new file is uploaded. + // and to retain the stored profile/avatar when the operator's app owns it. const previous = await readClientSettings(effects) + const manageProfile = input.profile.selection === 'managed' - // Avatar: the field is prefilled with the stored data URL, so an unchanged - // value is kept as-is (already normalized), an emptied value removes the - // picture, and a newly pasted image is cropped to a square and shrunk. - const pasted = input.image?.trim() ?? '' + // Profile fields: from the form when StartOS-managed; otherwise keep what's + // stored (the app owns the live profile, and we never touch it over the WS). + let displayName = previous.displayName + let fullName = previous.fullName let image = previous.image - if (pasted === '') { - image = '' - } else if (pasted !== previous.image) { - try { - image = await pastedImageToAvatarDataUrl(pasted) - } catch (err) { - return { - version: '1', - title: i18n('Could Not Process Image'), - message: i18n('The profile picture could not be processed: ').concat( - (err as Error).message, - ), - result: null, + let peerType = previous.peerType + let autoAcceptContacts = previous.autoAcceptContacts + let businessMode = previous.businessMode + let welcomeMessage = previous.welcomeMessage + + if (input.profile.selection === 'managed') { + const p = input.profile.value + // Avatar: prefilled with the stored data URL, so an unchanged value is + // kept as-is, an emptied value removes it, and a new URL/data-URL/base64 + // is cropped to a square and shrunk. + const pasted = p.image?.trim() ?? '' + if (pasted === '') { + image = '' + } else if (pasted !== previous.image) { + try { + image = await pastedImageToAvatarDataUrl(pasted) + } catch (err) { + return { + version: '1', + title: i18n('Could Not Process Image'), + message: i18n( + 'The profile picture could not be processed: ', + ).concat((err as Error).message), + result: null, + } } } + displayName = p.displayName + fullName = p.fullName?.trim() || '' + peerType = p.peerType + autoAcceptContacts = p.autoAcceptContacts + businessMode = p.businessMode + welcomeMessage = p.welcomeMessage?.trim() || '' } const servers: ClientSettings['servers'] = @@ -248,13 +298,14 @@ export const configureClient = sdk.Action.withInput( } const settings: ClientSettings = { - displayName: input.displayName, - fullName: input.fullName?.trim() || '', + manageProfile, + displayName, + fullName, image, - peerType: input.peerType, - autoAcceptContacts: input.autoAcceptContacts, - businessMode: input.businessMode, - welcomeMessage: input.welcomeMessage?.trim() || '', + peerType, + autoAcceptContacts, + businessMode, + welcomeMessage, servers, cleanupDays: input.cleanupDays ?? null, } @@ -274,18 +325,27 @@ export const configureClient = sdk.Action.withInput( previous.servers.smp.join(' ') !== settings.servers.smp.join(' ') || previous.servers.xftp.join(' ') !== settings.servers.xftp.join(' ') const retentionChanged = previous.cleanupDays !== settings.cleanupDays + const manageChanged = previous.manageProfile !== manageProfile + + // Hands-off mode: StartOS makes no WebSocket writes. Relays and retention + // are container env (applied at launch), so any of those — or toggling the + // mode itself — takes effect on restart. + if (!manageProfile) { + if (relaysChanged || retentionChanged || manageChanged) { + await effects.restart() + } + return null + } - // Received-file retention is a container env / janitor setting read at - // launch, so a change there needs a restart. The restart also re-applies - // relays and profile via the post-ready one-shot, so it covers everything. - if (retentionChanged) { + // Managed mode. Retention (env/janitor) and switching into managed mode + // both need a restart; the post-ready one-shot then re-applies everything. + if (retentionChanged || manageChanged) { await effects.restart() return null } - // Everything else applies live over the WebSocket — no restart, no - // downtime for dependents. Relays go through configureServers (which sets - // custom/local or resets to presets); profile/address through the sync. A + // Otherwise apply live over the WebSocket — no restart, no downtime for + // dependents. Relays via configureServers, profile/address via the sync. A // rejected relay config surfaces here so it's diagnosable immediately. try { if (relaysChanged) await configureServers(effects, settings) diff --git a/startos/actions/create-invitation.ts b/startos/actions/create-invitation.ts index e5ccd0c..aef3192 100644 --- a/startos/actions/create-invitation.ts +++ b/startos/actions/create-invitation.ts @@ -22,7 +22,7 @@ import { i18n } from '../i18n' export const createInvitation = sdk.Action.withoutInput( 'create-invitation', async () => ({ - name: i18n('Create Invitation'), + name: i18n('Create SimpleX Invitation'), description: i18n( 'Create a one-time SimpleX invitation link. Each invocation produces a fresh link that can be used by exactly one new contact — share it through any channel and have them paste it into their SimpleX client.', ), diff --git a/startos/actions/reset-address.ts b/startos/actions/reset-address.ts index 0bcc891..61e2d8a 100644 --- a/startos/actions/reset-address.ts +++ b/startos/actions/reset-address.ts @@ -24,7 +24,7 @@ import { i18n } from '../i18n' export const resetAddress = sdk.Action.withoutInput( 'reset-address', async () => ({ - name: i18n('Reset Address'), + name: i18n('Reset SimpleX Address'), description: i18n( 'Replace the long-lived SimpleX address for this client with a new one — useful after changing message relays, so the address is hosted on the new servers. Existing contacts are unaffected; only the old address link stops working.', ), diff --git a/startos/actions/view-address.ts b/startos/actions/view-address.ts index 469d5d6..9244b66 100644 --- a/startos/actions/view-address.ts +++ b/startos/actions/view-address.ts @@ -21,7 +21,7 @@ import { i18n } from '../i18n' export const viewAddress = sdk.Action.withoutInput( 'view-address', async () => ({ - name: i18n('View Address'), + name: i18n('View SimpleX Address'), description: i18n( 'Show the long-lived SimpleX address for this client — a reusable connection link. Unlike a one-time invitation, the same address works for any number of contacts.', ), diff --git a/startos/fileModels/clientSettings.json.ts b/startos/fileModels/clientSettings.json.ts index a88adff..4fb8940 100644 --- a/startos/fileModels/clientSettings.json.ts +++ b/startos/fileModels/clientSettings.json.ts @@ -20,6 +20,13 @@ import { sdk } from '../sdk' export type ServerMode = 'public' | 'local' | 'custom' export interface ClientSettings { + /** + * When true (default), StartOS manages the profile/address over the WebSocket + * and applies relays authoritatively over the API. When false the operator's + * own application owns the client: StartOS makes no WebSocket writes and only + * applies relays + file cleanup via container env at start (hands-off mode). + */ + manageProfile: boolean displayName: string fullName: string /** Base64 data URL (e.g. "data:image/jpg;base64,...") or "" for none. */ @@ -45,6 +52,7 @@ export interface ClientSettings { // (PROFILE_DISPLAY_NAME) so a bare install produces the same identity whether // or not the user opens the action first. export const SETTINGS_DEFAULTS: ClientSettings = { + manageProfile: true, displayName: 'SimpleX Bot', fullName: '', image: '', @@ -65,6 +73,7 @@ const serversShape = z .catch(SETTINGS_DEFAULTS.servers) const shape = z.object({ + manageProfile: z.boolean().catch(true), displayName: z.string().min(1).catch(SETTINGS_DEFAULTS.displayName), fullName: z.string().catch(''), image: z.string().catch(''), diff --git a/startos/i18n/dictionaries/default.ts b/startos/i18n/dictionaries/default.ts index 3af62c8..dae0d77 100644 --- a/startos/i18n/dictionaries/default.ts +++ b/startos/i18n/dictionaries/default.ts @@ -17,7 +17,7 @@ const dict = { 'Profile Picture': 11, // create-invitation action - 'Create Invitation': 18, + 'Create SimpleX Invitation': 18, 'Create a one-time SimpleX invitation link. Each invocation produces a fresh link that can be used by exactly one new contact — share it through any channel and have them paste it into their SimpleX client.': 19, 'One-Time Invitation Link': 20, 'Send this link to one new contact. Each link can be redeemed by exactly one peer — run this action again to invite another person.': 21, @@ -67,7 +67,7 @@ const dict = { 'Welcome Message': 59, 'Optional auto-reply sent to each new contact when they connect.': 60, 'Message Relays (SMP/XFTP)': 61, - 'Which servers relay your messages and files. Applied immediately (no restart) and only to NEW connections — existing contacts and your current address keep using the server they were created on. Use Reset Address to move your address onto the new relays.': 62, + 'Which servers relay your messages and files. Applied immediately (no restart) and only to NEW connections — existing contacts and your current address keep using the server they were created on. Use Reset SimpleX Address to move your address onto the new relays.': 62, 'SimpleX defaults (public)': 63, 'My self-hosted SimpleX Server': 64, Custom: 65, @@ -89,16 +89,22 @@ const dict = { 'The profile picture could not be processed: ': 81, // view-address / reset-address actions - 'View Address': 83, + 'View SimpleX Address': 83, 'Show the long-lived SimpleX address for this client — a reusable connection link. Unlike a one-time invitation, the same address works for any number of contacts.': 84, 'SimpleX Address': 85, 'Share this address so others can request to connect. It is reusable — the same link works for any number of contacts. To accept requests automatically, enable Auto-Accept Contact Requests in Configure Client.': 86, 'No Address Returned': 87, - 'Reset Address': 88, + 'Reset SimpleX Address': 88, 'Replace the long-lived SimpleX address for this client with a new one — useful after changing message relays, so the address is hosted on the new servers. Existing contacts are unaffected; only the old address link stops working.': 89, 'This replaces the SimpleX address for this client with a new one. The old address link will no longer connect anyone to the client, so update anywhere you have shared it. Existing contacts are not affected — they can still chat with the client as before.': 90, 'Address Reset': 91, 'A new SimpleX address has been created and is shown below. The previous address link no longer works; existing contacts are unaffected.': 92, + + // configure client — profile management mode + 'SimpleX Profile': 93, + 'Choose whether StartOS manages the client profile and address, or leaves them to your own application. When your application manages them, StartOS makes no changes to the running client over the Websocket — it only applies message relays and file cleanup at startup.': 94, + 'Managed by StartOS': 95, + 'Managed by my application': 96, } as const /** diff --git a/startos/i18n/dictionaries/translations.ts b/startos/i18n/dictionaries/translations.ts index 75f5cc1..535ebb4 100644 --- a/startos/i18n/dictionaries/translations.ts +++ b/startos/i18n/dictionaries/translations.ts @@ -11,7 +11,7 @@ export default { 6: 'Zona de peligro', 9: 'Nombre visible', 11: 'Imagen de perfil', - 18: 'Crear invitación', + 18: 'Crear invitación de SimpleX', 19: 'Crea un enlace de invitación de SimpleX de un solo uso. Cada ejecución genera un enlace nuevo que puede usar exactamente un contacto nuevo: compártelo por cualquier canal y pídele que lo pegue en su cliente de SimpleX.', 20: 'Enlace de invitación de un solo uso', 21: 'Envía este enlace a un contacto nuevo. Cada enlace puede ser canjeado por exactamente un par: ejecuta esta acción de nuevo para invitar a otra persona.', @@ -55,7 +55,7 @@ export default { 59: 'Mensaje de bienvenida', 60: 'Respuesta automática opcional que se envía a cada nuevo contacto cuando se conecta.', 61: 'Relés de mensajes (SMP/XFTP)', - 62: 'Qué servidores retransmiten tus mensajes y archivos. Se aplica de inmediato (sin reinicio) y solo a las conexiones NUEVAS: los contactos existentes y tu dirección actual siguen usando el servidor con el que se crearon. Usa Restablecer dirección para mover tu dirección a los nuevos relés.', + 62: 'Qué servidores retransmiten tus mensajes y archivos. Se aplica de inmediato (sin reinicio) y solo a las conexiones NUEVAS: los contactos existentes y tu dirección actual siguen usando el servidor con el que se crearon. Usa Restablecer dirección SimpleX para mover tu dirección a los nuevos relés.', 63: 'Predeterminados de SimpleX (públicos)', 64: 'Mi servidor SimpleX autoalojado', 65: 'Personalizado', @@ -71,16 +71,20 @@ export default { 79: 'No se pudo sincronizar la configuración del cliente SimpleX: ', 80: 'No se pudo procesar la imagen', 81: 'No se pudo procesar la imagen de perfil: ', - 83: 'Ver dirección', + 83: 'Ver dirección SimpleX', 84: 'Muestra la dirección SimpleX de larga duración de este cliente: un enlace de conexión reutilizable. A diferencia de una invitación de un solo uso, la misma dirección sirve para cualquier número de contactos.', 85: 'Dirección SimpleX', 86: 'Comparte esta dirección para que otros puedan solicitar conectarse. Es reutilizable: el mismo enlace sirve para cualquier número de contactos. Para aceptar solicitudes automáticamente, activa Aceptar automáticamente solicitudes de contacto en Configurar cliente.', 87: 'No se devolvió ninguna dirección', - 88: 'Restablecer dirección', + 88: 'Restablecer dirección SimpleX', 89: 'Reemplaza la dirección SimpleX de larga duración de este cliente por una nueva, útil tras cambiar los relés de mensajes para que la dirección se aloje en los nuevos servidores. Los contactos existentes no se ven afectados; solo deja de funcionar el enlace de la dirección anterior.', 90: 'Esto reemplaza la dirección SimpleX de este cliente por una nueva. El enlace de la dirección anterior ya no conectará a nadie con el cliente, así que actualízalo donde lo hayas compartido. Los contactos existentes no se ven afectados: pueden seguir chateando con el cliente como antes.', 91: 'Dirección restablecida', 92: 'Se ha creado una nueva dirección SimpleX y se muestra a continuación. El enlace de la dirección anterior ya no funciona; los contactos existentes no se ven afectados.', + 93: 'Perfil de SimpleX', + 94: 'Elige si StartOS gestiona el perfil y la dirección del cliente, o los deja a tu propia aplicación. Cuando los gestiona tu aplicación, StartOS no realiza ningún cambio en el cliente en ejecución a través del Websocket; solo aplica los relés de mensajes y la limpieza de archivos al iniciar.', + 95: 'Gestionado por StartOS', + 96: 'Gestionado por mi aplicación', }, de_DE: { 0: 'SimpleX Websocket Bridge wird gestartet!', @@ -92,7 +96,7 @@ export default { 6: 'Gefahrenzone', 9: 'Anzeigename', 11: 'Profilbild', - 18: 'Einladung erstellen', + 18: 'SimpleX-Einladung erstellen', 19: 'Erstelle einen einmaligen SimpleX-Einladungslink. Jeder Aufruf erzeugt einen neuen Link, der von genau einem neuen Kontakt verwendet werden kann – teile ihn über einen beliebigen Kanal und lass ihn in den SimpleX-Client einfügen.', 20: 'Einmaliger Einladungslink', 21: 'Sende diesen Link an einen neuen Kontakt. Jeder Link kann von genau einem Gegenüber eingelöst werden – führe diese Aktion erneut aus, um eine weitere Person einzuladen.', @@ -136,7 +140,7 @@ export default { 59: 'Willkommensnachricht', 60: 'Optionale automatische Antwort, die an jeden neuen Kontakt gesendet wird, wenn er sich verbindet.', 61: 'Nachrichten-Relays (SMP/XFTP)', - 62: 'Welche Server deine Nachrichten und Dateien weiterleiten. Wird sofort angewendet (ohne Neustart) und nur auf NEUE Verbindungen — bestehende Kontakte und deine aktuelle Adresse verwenden weiterhin den Server, mit dem sie erstellt wurden. Verwende „Adresse zurücksetzen“, um deine Adresse auf die neuen Relays zu verschieben.', + 62: 'Welche Server deine Nachrichten und Dateien weiterleiten. Wird sofort angewendet (ohne Neustart) und nur auf NEUE Verbindungen — bestehende Kontakte und deine aktuelle Adresse verwenden weiterhin den Server, mit dem sie erstellt wurden. Verwende „SimpleX-Adresse zurücksetzen“, um deine Adresse auf die neuen Relays zu verschieben.', 63: 'SimpleX-Standard (öffentlich)', 64: 'Mein selbst gehosteter SimpleX-Server', 65: 'Benutzerdefiniert', @@ -152,16 +156,20 @@ export default { 79: 'SimpleX-Client-Einstellungen konnten nicht synchronisiert werden: ', 80: 'Bild konnte nicht verarbeitet werden', 81: 'Das Profilbild konnte nicht verarbeitet werden: ', - 83: 'Adresse anzeigen', + 83: 'SimpleX-Adresse anzeigen', 84: 'Zeigt die langlebige SimpleX-Adresse dieses Clients an – einen wiederverwendbaren Verbindungslink. Anders als eine einmalige Einladung funktioniert dieselbe Adresse für beliebig viele Kontakte.', 85: 'SimpleX-Adresse', 86: 'Teile diese Adresse, damit andere eine Verbindung anfragen können. Sie ist wiederverwendbar – derselbe Link funktioniert für beliebig viele Kontakte. Um Anfragen automatisch anzunehmen, aktiviere „Kontaktanfragen automatisch annehmen“ unter „Client konfigurieren“.', 87: 'Keine Adresse zurückgegeben', - 88: 'Adresse zurücksetzen', + 88: 'SimpleX-Adresse zurücksetzen', 89: 'Ersetzt die langlebige SimpleX-Adresse dieses Clients durch eine neue – nützlich nach dem Wechsel der Nachrichten-Relays, damit die Adresse auf den neuen Servern liegt. Bestehende Kontakte sind nicht betroffen; nur der alte Adresslink funktioniert nicht mehr.', 90: 'Dies ersetzt die SimpleX-Adresse dieses Clients durch eine neue. Der alte Adresslink verbindet niemanden mehr mit dem Client – aktualisiere ihn also überall, wo du ihn geteilt hast. Bestehende Kontakte sind nicht betroffen: Sie können weiterhin wie zuvor mit dem Client chatten.', 91: 'Adresse zurückgesetzt', 92: 'Eine neue SimpleX-Adresse wurde erstellt und wird unten angezeigt. Der vorherige Adresslink funktioniert nicht mehr; bestehende Kontakte sind nicht betroffen.', + 93: 'SimpleX-Profil', + 94: 'Wähle, ob StartOS das Client-Profil und die Adresse verwaltet oder sie deiner eigenen Anwendung überlässt. Wenn deine Anwendung sie verwaltet, nimmt StartOS keine Änderungen am laufenden Client über das Websocket vor — es wendet nur Nachrichten-Relays und Dateibereinigung beim Start an.', + 95: 'Von StartOS verwaltet', + 96: 'Von meiner Anwendung verwaltet', }, pl_PL: { 0: 'Uruchamianie SimpleX Websocket Bridge!', @@ -173,7 +181,7 @@ export default { 6: 'Strefa zagrożenia', 9: 'Nazwa wyświetlana', 11: 'Obraz profilu', - 18: 'Utwórz zaproszenie', + 18: 'Utwórz zaproszenie SimpleX', 19: 'Utwórz jednorazowy link zaproszenia SimpleX. Każde wywołanie generuje nowy link, którego może użyć dokładnie jeden nowy kontakt – udostępnij go dowolnym kanałem i poproś o wklejenie go w kliencie SimpleX.', 20: 'Jednorazowy link zaproszenia', 21: 'Wyślij ten link jednemu nowemu kontaktowi. Każdy link może wykorzystać dokładnie jedna osoba – uruchom tę akcję ponownie, aby zaprosić kolejną osobę.', @@ -217,7 +225,7 @@ export default { 59: 'Wiadomość powitalna', 60: 'Opcjonalna automatyczna odpowiedź wysyłana do każdego nowego kontaktu po połączeniu.', 61: 'Przekaźniki wiadomości (SMP/XFTP)', - 62: 'Które serwery przekazują Twoje wiadomości i pliki. Stosowane natychmiast (bez ponownego uruchamiania) i tylko do NOWYCH połączeń — istniejące kontakty i Twój bieżący adres nadal używają serwera, z którym zostały utworzone. Użyj „Zresetuj adres”, aby przenieść adres na nowe przekaźniki.', + 62: 'Które serwery przekazują Twoje wiadomości i pliki. Stosowane natychmiast (bez ponownego uruchamiania) i tylko do NOWYCH połączeń — istniejące kontakty i Twój bieżący adres nadal używają serwera, z którym zostały utworzone. Użyj „Zresetuj adres SimpleX”, aby przenieść adres na nowe przekaźniki.', 63: 'Domyślne SimpleX (publiczne)', 64: 'Mój samodzielnie hostowany serwer SimpleX', 65: 'Niestandardowe', @@ -233,16 +241,20 @@ export default { 79: 'Nie można zsynchronizować ustawień klienta SimpleX: ', 80: 'Nie można przetworzyć obrazu', 81: 'Nie można przetworzyć obrazu profilu: ', - 83: 'Pokaż adres', + 83: 'Pokaż adres SimpleX', 84: 'Pokazuje długoterminowy adres SimpleX tego klienta — link połączenia wielokrotnego użytku. W przeciwieństwie do jednorazowego zaproszenia ten sam adres działa dla dowolnej liczby kontaktów.', 85: 'Adres SimpleX', 86: 'Udostępnij ten adres, aby inni mogli poprosić o połączenie. Jest wielokrotnego użytku — ten sam link działa dla dowolnej liczby kontaktów. Aby automatycznie akceptować prośby, włącz „Automatycznie akceptuj prośby o kontakt” w Konfiguruj klienta.', 87: 'Nie zwrócono żadnego adresu', - 88: 'Zresetuj adres', + 88: 'Zresetuj adres SimpleX', 89: 'Zastępuje długoterminowy adres SimpleX tego klienta nowym — przydatne po zmianie przekaźników wiadomości, aby adres był hostowany na nowych serwerach. Istniejące kontakty nie są naruszone; przestaje działać tylko stary link adresu.', 90: 'To zastępuje adres SimpleX tego klienta nowym. Stary link adresu nie połączy już nikogo z klientem, więc zaktualizuj go wszędzie tam, gdzie go udostępniono. Istniejące kontakty nie są naruszone: mogą nadal rozmawiać z klientem jak wcześniej.', 91: 'Zresetowano adres', 92: 'Utworzono nowy adres SimpleX, pokazany poniżej. Poprzedni link adresu już nie działa; istniejące kontakty nie są naruszone.', + 93: 'Profil SimpleX', + 94: 'Wybierz, czy StartOS zarządza profilem i adresem klienta, czy pozostawia je Twojej własnej aplikacji. Gdy zarządza nimi Twoja aplikacja, StartOS nie wprowadza żadnych zmian w działającym kliencie przez Websocket — stosuje jedynie przekaźniki wiadomości i czyszczenie plików przy uruchamianiu.', + 95: 'Zarządzane przez StartOS', + 96: 'Zarządzane przez moją aplikację', }, fr_FR: { 0: 'Démarrage de SimpleX Websocket Bridge !', @@ -254,7 +266,7 @@ export default { 6: 'Zone de danger', 9: 'Nom affiché', 11: 'Image de profil', - 18: 'Créer une invitation', + 18: 'Créer une invitation SimpleX', 19: "Créez un lien d'invitation SimpleX à usage unique. Chaque exécution génère un nouveau lien qu'un seul nouveau contact peut utiliser — partagez-le par n'importe quel canal et demandez-lui de le coller dans son client SimpleX.", 20: "Lien d'invitation à usage unique", 21: "Envoyez ce lien à un nouveau contact. Chaque lien ne peut être utilisé que par une seule personne — relancez cette action pour inviter quelqu'un d'autre.", @@ -298,7 +310,7 @@ export default { 59: 'Message de bienvenue', 60: 'Réponse automatique facultative envoyée à chaque nouveau contact lors de sa connexion.', 61: 'Relais de messages (SMP/XFTP)', - 62: "Quels serveurs relaient vos messages et fichiers. Appliqué immédiatement (sans redémarrage) et uniquement aux NOUVELLES connexions — vos contacts existants et votre adresse actuelle continuent d'utiliser le serveur avec lequel ils ont été créés. Utilisez « Réinitialiser l'adresse » pour déplacer votre adresse vers les nouveaux relais.", + 62: "Quels serveurs relaient vos messages et fichiers. Appliqué immédiatement (sans redémarrage) et uniquement aux NOUVELLES connexions — vos contacts existants et votre adresse actuelle continuent d'utiliser le serveur avec lequel ils ont été créés. Utilisez « Réinitialiser l'adresse SimpleX » pour déplacer votre adresse vers les nouveaux relais.", 63: 'Valeurs par défaut SimpleX (publiques)', 64: 'Mon serveur SimpleX auto-hébergé', 65: 'Personnalisé', @@ -314,15 +326,19 @@ export default { 79: 'Impossible de synchroniser les paramètres du client SimpleX : ', 80: "Impossible de traiter l'image", 81: "L'image de profil n'a pas pu être traitée : ", - 83: "Afficher l'adresse", + 83: "Afficher l'adresse SimpleX", 84: "Affiche l'adresse SimpleX durable de ce client — un lien de connexion réutilisable. Contrairement à une invitation à usage unique, la même adresse fonctionne pour un nombre illimité de contacts.", 85: 'Adresse SimpleX', 86: "Partagez cette adresse pour que d'autres puissent demander à se connecter. Elle est réutilisable — le même lien fonctionne pour un nombre illimité de contacts. Pour accepter les demandes automatiquement, activez « Accepter automatiquement les demandes de contact » dans Configurer le client.", 87: 'Aucune adresse renvoyée', - 88: "Réinitialiser l'adresse", + 88: "Réinitialiser l'adresse SimpleX", 89: "Remplace l'adresse SimpleX durable de ce client par une nouvelle — utile après un changement de relais de messages, pour que l'adresse soit hébergée sur les nouveaux serveurs. Les contacts existants ne sont pas affectés ; seul l'ancien lien d'adresse cesse de fonctionner.", 90: "Cela remplace l'adresse SimpleX de ce client par une nouvelle. L'ancien lien d'adresse ne connectera plus personne au client, alors mettez-le à jour partout où vous l'avez partagé. Les contacts existants ne sont pas affectés : ils peuvent continuer à discuter avec le client comme avant.", 91: 'Adresse réinitialisée', 92: "Une nouvelle adresse SimpleX a été créée et est affichée ci-dessous. L'ancien lien d'adresse ne fonctionne plus ; les contacts existants ne sont pas affectés.", + 93: 'Profil SimpleX', + 94: "Choisissez si StartOS gère le profil et l'adresse du client, ou les laisse à votre propre application. Lorsque votre application les gère, StartOS n'apporte aucune modification au client en cours d'exécution via le Websocket — il applique uniquement les relais de messages et le nettoyage des fichiers au démarrage.", + 95: 'Géré par StartOS', + 96: 'Géré par mon application', }, } satisfies Record diff --git a/startos/main.ts b/startos/main.ts index 39bb2db..11a76fe 100644 --- a/startos/main.ts +++ b/startos/main.ts @@ -10,10 +10,11 @@ export const main = sdk.setupMain(async ({ effects }) => { console.info(i18n('Starting SimpleX Websocket Bridge!')) // Compute the container's start environment from the saved client settings - // (or code defaults on a fresh install). Seeds the profile on first start and - // applies relay / retention choices every start. Throws (failing start) if - // the settings ask for something unresolvable — e.g. Local relays, which - // aren't implemented yet — rather than silently using public presets. + // (or code defaults on a fresh install). Seeds the profile on first start and, + // in hands-off mode, applies relays via env. Throws (failing start) if the + // settings ask for something unresolvable — e.g. Local relays when the + // SimpleX Server dependency isn't reachable — rather than silently using + // public presets. const settings = await readClientSettings(effects) const env = await computeStartEnv(effects, settings) @@ -24,69 +25,68 @@ export const main = sdk.setupMain(async ({ effects }) => { 'simplex-sub', ) - return ( - sdk.Daemons.of(effects) - .addDaemon('simplex', { - subcontainer, - exec: { - command: sdk.useEntrypoint(), - env, - }, - ready: { - display: null, // surfaced to users (and dependents) via the 'websocket' health check below - fn: () => - sdk.healthCheck.checkPortListening(effects, port, { - successMessage: i18n('Websocket is ready'), - errorMessage: i18n('Websocket is not ready'), - }), - }, - requires: [], - }) - // Standalone health check with a stable ID ('websocket') that dependent - // packages can reference in a `kind: 'running'` dependency requirement. - // Part of the file exchange contract (see README). - .addHealthCheck('websocket', { - ready: { - display: i18n('Websocket'), - fn: () => - sdk.healthCheck.checkPortListening(effects, port, { - successMessage: i18n('Websocket is ready'), - errorMessage: i18n('Websocket is not ready'), - }), - }, - requires: ['simplex'], - }) - // Once the WebSocket is reachable, reconcile the running client's profile - // and address settings with the saved settings — the fields the image - // can't seed from env (full name, avatar, business mode, auto-accept, - // welcome message). Best-effort: a transient failure here must not wedge - // startup, so we log and move on; the next Configure submit or restart - // reconciles again. - .addOneshot('sync-settings', { - subcontainer, - exec: { - fn: async () => { - try { - // The daemon `requires` gate orders launch, not socket readiness, - // so wait for the WebSocket to actually answer before syncing — - // otherwise the first connect races websocat's bind (ECONNREFUSED). - await waitForBotReady(effects) - // Apply the selected relays first (authoritative over the DB — - // sets custom/local, resets public), then reconcile the profile. - await configureServers(effects, settings) - await syncClientSettings(effects, settings) - console.info(i18n('SimpleX client settings synced')) - } catch (err) { - console.warn( - i18n('Could not sync SimpleX client settings: ').concat( - (err as Error).message, - ), - ) - } - return null - }, - }, - requires: ['simplex'], - }) - ) + const daemons = sdk.Daemons.of(effects) + .addDaemon('simplex', { + subcontainer, + exec: { + command: sdk.useEntrypoint(), + env, + }, + ready: { + display: null, // surfaced to users (and dependents) via the 'websocket' health check below + fn: () => + sdk.healthCheck.checkPortListening(effects, port, { + successMessage: i18n('Websocket is ready'), + errorMessage: i18n('Websocket is not ready'), + }), + }, + requires: [], + }) + // Standalone health check with a stable ID ('websocket') that dependent + // packages can reference in a `kind: 'running'` dependency requirement. + // Part of the file exchange contract (see README). + .addHealthCheck('websocket', { + ready: { + display: i18n('Websocket'), + fn: () => + sdk.healthCheck.checkPortListening(effects, port, { + successMessage: i18n('Websocket is ready'), + errorMessage: i18n('Websocket is not ready'), + }), + }, + requires: ['simplex'], + }) + + // In hands-off mode the operator's own application owns the client, so we make + // no WebSocket writes — relays went in via env above, and there's nothing to + // sync. Only in managed mode do we reconcile relays + profile/address over the + // WS once the socket is reachable. + if (!settings.manageProfile) return daemons + + return daemons.addOneshot('sync-settings', { + subcontainer, + exec: { + fn: async () => { + try { + // The daemon `requires` gate orders launch, not socket readiness, + // so wait for the WebSocket to actually answer before syncing — + // otherwise the first connect races websocat's bind (ECONNREFUSED). + await waitForBotReady(effects) + // Apply the selected relays first (authoritative over the DB — + // sets custom/local, resets public), then reconcile the profile. + await configureServers(effects, settings) + await syncClientSettings(effects, settings) + console.info(i18n('SimpleX client settings synced')) + } catch (err) { + console.warn( + i18n('Could not sync SimpleX client settings: ').concat( + (err as Error).message, + ), + ) + } + return null + }, + }, + requires: ['simplex'], + }) }) diff --git a/startos/serverConfig.ts b/startos/serverConfig.ts index ca054ab..71174d6 100644 --- a/startos/serverConfig.ts +++ b/startos/serverConfig.ts @@ -117,7 +117,7 @@ export async function resolveServerUris( /** Full start environment for the simplex daemon, derived from settings. */ export async function computeStartEnv( - _effects: T.Effects, + effects: T.Effects, settings: ClientSettings, ): Promise> { const env: Record = { @@ -131,5 +131,15 @@ export async function computeStartEnv( env.INBOUND_RETENTION_HOURS = String(settings.cleanupDays * 24) } + // Hands-off mode: StartOS makes no WebSocket writes, so relays are applied + // via the image's env flags instead of the operator-servers API. (Managed + // mode leaves these unset and configures relays over the WS on start — + // env `--server` persists in the DB and can't be reset from the flag.) + if (!settings.manageProfile) { + const { smp, xftp } = await resolveServerUris(effects, settings) + if (smp.length) env.SMP_SERVERS = smp.join(' ') + if (xftp.length) env.XFTP_SERVERS = xftp.join(' ') + } + return env } diff --git a/startos/versions/current.ts b/startos/versions/current.ts index 87f570e..bb69bd4 100644 --- a/startos/versions/current.ts +++ b/startos/versions/current.ts @@ -8,6 +8,7 @@ export const current = VersionInfo.of({ '', '- Bundles simplex-chat v6.5.5 (previously v6.5.4).', '- New "Configure Client" action: set the display name, full name, profile picture (from an image URL, data URL, or base64 — auto-cropped to a square and shrunk), peer type, auto-accept contact requests, business mode, welcome message, message relays, and received-file cleanup. Most changes apply live, with no restart.', + '- Hands-off mode: choose "Managed by my application" to drive the bridge from your own software — StartOS makes no changes over the Websocket and only applies relays and file cleanup via container settings.', "- Message relays: use SimpleX's public servers, your own self-hosted SimpleX Server (via an optional dependency), or custom SMP/XFTP addresses — switching between them applies reliably, including back to public.", '- New "View Address" and "Reset Address" actions for the long-lived, reusable SimpleX address, alongside the existing one-time "Create Invitation".', '- "Reset Profile" is now "Reset Client", with clearer warnings about reconnecting contacts.', @@ -19,6 +20,7 @@ export const current = VersionInfo.of({ '', '- Incluye simplex-chat v6.5.5 (antes v6.5.4).', '- Nueva acción «Configurar cliente»: define el nombre visible, el nombre completo, la imagen de perfil (desde una URL de imagen, data URL o base64, recortada a un cuadrado y reducida automáticamente), el tipo de par, la aceptación automática de solicitudes de contacto, el modo empresa, el mensaje de bienvenida, los relés de mensajes y la limpieza de archivos recibidos. La mayoría de los cambios se aplican en vivo, sin reinicio.', + '- Modo autónomo: elige «Gestionado por mi aplicación» para controlar el puente desde tu propio software: StartOS no realiza cambios a través del Websocket y solo aplica los relés y la limpieza de archivos mediante la configuración del contenedor.', '- Relés de mensajes: usa los servidores públicos de SimpleX, tu propio servidor SimpleX autoalojado (mediante una dependencia opcional) o direcciones SMP/XFTP personalizadas; cambiar entre ellos se aplica de forma fiable, incluido volver a los públicos.', '- Nuevas acciones «Ver dirección» y «Restablecer dirección» para la dirección SimpleX de larga duración y reutilizable, junto a la ya existente «Crear invitación» de un solo uso.', '- «Restablecer perfil» ahora es «Restablecer cliente», con advertencias más claras sobre la reconexión de contactos.', @@ -30,6 +32,7 @@ export const current = VersionInfo.of({ '', '- Enthält simplex-chat v6.5.5 (zuvor v6.5.4).', '- Neue Aktion „Client konfigurieren“: Anzeigename, vollständiger Name, Profilbild (aus Bild-URL, Data-URL oder base64 — automatisch quadratisch zugeschnitten und verkleinert), Peer-Typ, automatisches Annehmen von Kontaktanfragen, Geschäftsmodus, Willkommensnachricht, Nachrichten-Relays und Bereinigung empfangener Dateien festlegen. Die meisten Änderungen werden ohne Neustart angewendet.', + '- Hands-off-Modus: Wähle „Von meiner Anwendung verwaltet“, um die Brücke aus deiner eigenen Software zu steuern — StartOS nimmt keine Änderungen über das Websocket vor und wendet nur Relays und Dateibereinigung über die Container-Einstellungen an.', '- Nachrichten-Relays: nutze die öffentlichen SimpleX-Server, deinen eigenen selbst gehosteten SimpleX-Server (über eine optionale Abhängigkeit) oder eigene SMP/XFTP-Adressen; der Wechsel dazwischen wird zuverlässig angewendet, auch zurück zu den öffentlichen.', '- Neue Aktionen „Adresse anzeigen“ und „Adresse zurücksetzen“ für die langlebige, wiederverwendbare SimpleX-Adresse, neben der bestehenden einmaligen „Einladung erstellen“.', '- „Profil zurücksetzen“ heißt jetzt „Client zurücksetzen“, mit klareren Hinweisen zum erneuten Verbinden von Kontakten.', @@ -41,6 +44,7 @@ export const current = VersionInfo.of({ '', '- Zawiera simplex-chat v6.5.5 (poprzednio v6.5.4).', '- Nowa akcja „Konfiguruj klienta”: ustaw nazwę wyświetlaną, imię i nazwisko, obraz profilu (z adresu URL obrazu, data URL lub base64 — automatycznie przycięty do kwadratu i pomniejszony), typ węzła, automatyczne akceptowanie próśb o kontakt, tryb biznesowy, wiadomość powitalną, przekaźniki wiadomości i czyszczenie odebranych plików. Większość zmian jest stosowana na żywo, bez ponownego uruchamiania.', + '- Tryb hands-off: wybierz „Zarządzane przez moją aplikację”, aby sterować mostem z własnego oprogramowania — StartOS nie wprowadza zmian przez Websocket i stosuje tylko przekaźniki oraz czyszczenie plików przez ustawienia kontenera.', '- Przekaźniki wiadomości: użyj publicznych serwerów SimpleX, własnego samodzielnie hostowanego serwera SimpleX (przez opcjonalną zależność) lub niestandardowych adresów SMP/XFTP; przełączanie między nimi jest stosowane niezawodnie, także z powrotem na publiczne.', '- Nowe akcje „Pokaż adres” i „Zresetuj adres” dla długoterminowego, wielokrotnego adresu SimpleX, obok istniejącej jednorazowej „Utwórz zaproszenie”.', '- „Zresetuj profil” to teraz „Zresetuj klienta”, z jaśniejszymi ostrzeżeniami o ponownym łączeniu kontaktów.', @@ -52,6 +56,7 @@ export const current = VersionInfo.of({ '', '- Inclut simplex-chat v6.5.5 (auparavant v6.5.4).', "- Nouvelle action « Configurer le client » : définit le nom affiché, le nom complet, l'image de profil (depuis une URL d'image, une data URL ou base64 — recadrée en carré et réduite automatiquement), le type de pair, l'acceptation automatique des demandes de contact, le mode entreprise, le message de bienvenue, les relais de messages et le nettoyage des fichiers reçus. La plupart des changements s'appliquent en direct, sans redémarrage.", + "- Mode autonome : choisissez « Géré par mon application » pour piloter le pont depuis votre propre logiciel — StartOS n'apporte aucune modification via le Websocket et applique uniquement les relais et le nettoyage des fichiers via les paramètres du conteneur.", "- Relais de messages : utilisez les serveurs publics de SimpleX, votre propre serveur SimpleX auto-hébergé (via une dépendance optionnelle) ou des adresses SMP/XFTP personnalisées ; le basculement entre eux s'applique de façon fiable, y compris le retour aux serveurs publics.", "- Nouvelles actions « Afficher l'adresse » et « Réinitialiser l'adresse » pour l'adresse SimpleX durable et réutilisable, en plus de l'action « Créer une invitation » à usage unique existante.", '- « Réinitialiser le profil » devient « Réinitialiser le client », avec des avertissements plus clairs sur la reconnexion des contacts.', From bc524a27cf38832e9cd8c70fceb39ae478e67937 Mon Sep 17 00:00:00 2001 From: lundog <8041501+lundog@users.noreply.github.com> Date: Sat, 4 Jul 2026 11:02:09 -0600 Subject: [PATCH 6/6] Pin file paths to avoid breaking in the future --- README.md | 2 +- startos/serverConfig.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1c9a7f5..a0d994b 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ The image entrypoint starts `simplex-chat` in headless server/bot mode on a loca | ------ | ----------- | ------------------------------------------------------------------------------ | | `main` | `/data` | HOME — the `.simplex` profile database and file dirs, plus `store.json` (keys) | -The SimpleX client's own profile (identity, contacts, chat history) is the source of truth for chat state; there is no separate package-level chat config. A small `store.json` at the volume root holds the bridge's API keys. All SimpleX file dirs live under `/data/.simplex` — `files` (received, `--files-folder`), `tmp` (`--temp-folder`), and `outbound` (consumer-written) — as siblings on one filesystem, so simplex-chat's atomic `tmp` → `files` rename can't fail with `EXDEV`. +The SimpleX client's own database is the source of truth for chat state — identity/keys, contacts, and chat history. Two small JSON files at the volume root hold package-level config: `client-settings.json` (the client configuration StartOS applies — display name, profile, message relays, received-file retention, and whether StartOS manages the profile at all) and `store.json` (the bridge's API keys). All SimpleX file dirs live under `/data/.simplex` — `files` (received, `--files-folder`), `tmp` (`--temp-folder`), and `outbound` (consumer-written) — as siblings on one filesystem, so simplex-chat's atomic `tmp` → `files` rename can't fail with `EXDEV`. The package pins `SIMPLEX_INBOUND_DIR`/`SIMPLEX_TMP_DIR` to these paths at start (rather than relying on the image defaults), so the file-exchange contract stays stable even if the image changes `$HOME` or its own defaults. ### File exchange contract (for consumer packages) diff --git a/startos/serverConfig.ts b/startos/serverConfig.ts index 71174d6..10f4653 100644 --- a/startos/serverConfig.ts +++ b/startos/serverConfig.ts @@ -25,6 +25,16 @@ import { ClientSettings } from './fileModels/clientSettings.json' /** StartOS package id of the self-hosted SimpleX Server (Local relays). */ export const SIMPLEX_SERVER_PACKAGE_ID = 'simplex' +// File-exchange directories, pinned by this package rather than left to the +// image defaults. These paths ARE the file-exchange contract that consumer +// packages mount (`.simplex/files` / `.simplex/outbound`; see README), so the +// image changing $HOME or its SIMPLEX_INBOUND_DIR/SIMPLEX_TMP_DIR defaults must +// not move them out from under consumers. Both live under /data (the `main` +// volume) and are co-located so simplex-chat's atomic tmp->files rename can't +// fail with EXDEV. +const SIMPLEX_INBOUND_DIR = '/data/.simplex/files' +const SIMPLEX_TMP_DIR = '/data/.simplex/tmp' + // Service-interface IDs the SimpleX Server (simplex) package exports. Each URI // it produces is a full relay address including the server's CA fingerprint and // basic-auth password (username = ":", scheme "smp"/ @@ -123,6 +133,9 @@ export async function computeStartEnv( const env: Record = { PROFILE_DISPLAY_NAME: settings.displayName, PROFILE_PEER_TYPE: settings.peerType, + // Pin the file-exchange dirs so the contract is independent of the image. + SIMPLEX_INBOUND_DIR, + SIMPLEX_TMP_DIR, } // The image's janitor takes hours; the form collects days for a friendlier