ciphermesh 2.6.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/README.md +41 -6
- package/README.pt-BR.md +42 -6
- package/bin/cli-binary.js +42 -0
- package/package.json +6 -5
- package/src/server/index.js +12 -0
- package/src/server/presence.js +79 -0
- package/src/shared/banner.js +31 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes per release. Older versions are reconstructed from the git
|
|
4
|
+
history — the commit bodies and pull requests remain the fuller record.
|
|
5
|
+
|
|
6
|
+
## 2.7.1
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- The banner no longer reads a font from disk. `figlet.textSync` loads
|
|
11
|
+
`fonts/ANSI Shadow.flf` at startup — fine under Node, fatal inside a compiled
|
|
12
|
+
binary, where the file does not exist and the process died before printing
|
|
13
|
+
anything. The word never changes, so the art is now the constant it always
|
|
14
|
+
was, pinned against figlet's own output by a test. figlet moved to a
|
|
15
|
+
devDependency and the 1.11.4 hold is gone (#414).
|
|
16
|
+
|
|
17
|
+
## 2.7.0
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Public hub at [ciphermesh.de](https://ciphermesh.de).** An always-on relay
|
|
22
|
+
anyone can connect to, so using CipherMesh no longer means already knowing
|
|
23
|
+
someone who runs one. `general` is the default room. Governed by
|
|
24
|
+
[TERMS.md](TERMS.md).
|
|
25
|
+
- **The website and the relay share one domain.** Caddy routes WebSocket
|
|
26
|
+
upgrades to the relay and everything else to the landing page. No client
|
|
27
|
+
change: the client connects to `/` with no path, so the split is on the
|
|
28
|
+
`Upgrade` handshake rather than a path.
|
|
29
|
+
- **Presence endpoint** (`PRESENCE_PORT`, off by default). Publishes how busy a
|
|
30
|
+
relay is as a coarse **range** — `1-5`, `6-20` — never an exact count, a room
|
|
31
|
+
name or a nickname. An exact live number would let anyone polling it watch
|
|
32
|
+
people arrive and leave, which is the metadata the relay exists to withhold.
|
|
33
|
+
Runs on its own listener so it cannot affect chat.
|
|
34
|
+
- **Standalone client binary.** Every release now ships
|
|
35
|
+
`ciphermesh-<platform>` with the client, relay and P2P, alongside the
|
|
36
|
+
relay-only `ciphermesh-server-<platform>`. No Node needed. Closes the
|
|
37
|
+
long-standing bundling blocker (#328).
|
|
38
|
+
- **External hub monitoring.** A scheduled probe checks the WebSocket upgrade,
|
|
39
|
+
the site and the certificate, and opens an issue when any of them fails.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- A Caddyfile change never reached the running Caddy. `git reset --hard`
|
|
44
|
+
replaces the file, giving it a new inode, and a file bind-mount follows the
|
|
45
|
+
inode it was created with — the container kept reading the old, unlinked copy
|
|
46
|
+
while `docker compose up -d` saw nothing to recreate. `deploy/deploy.sh` is
|
|
47
|
+
now versioned and recreates Caddy when the file changes.
|
|
48
|
+
- The automatic deploy never fired on a release: GitHub does not trigger
|
|
49
|
+
workflows from events created by `GITHUB_TOKEN`.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- Both READMEs describe the hub and link to the site; `homepage` now points at
|
|
54
|
+
ciphermesh.de.
|
|
55
|
+
- `bonjour-service` 1.4.3 → 1.4.4.
|
|
56
|
+
- **`figlet` held at 1.11.3.** 1.11.4 reads its `.flf` font from disk at
|
|
57
|
+
runtime, and that file is not inside a compiled binary, so every standalone
|
|
58
|
+
build died with `ENOENT` on `/$bunfs/fonts/ANSI Shadow.flf` before printing
|
|
59
|
+
the banner. The test suite runs under Node, where the font exists, so CI
|
|
60
|
+
stayed green while the binaries were broken. Tracked in #414.
|
|
61
|
+
- Dependabot now targets `dev`. It was opening PRs straight into `master`,
|
|
62
|
+
which left `dev` behind and dragged unrelated commits into the next release.
|
|
63
|
+
|
|
64
|
+
## 2.6.0 and earlier
|
|
65
|
+
|
|
66
|
+
See the [release history](https://github.com/FelipeKreulich/secret-chat-lan/releases).
|
|
67
|
+
Highlights: hybrid post-quantum handshake (X25519 + ML-KEM-768), sealed sender,
|
|
68
|
+
multi-room buffers, private rooms with zero-knowledge passwords, the plugin API,
|
|
69
|
+
`/doctor`, screen lock, and the standalone relay binary.
|
package/README.md
CHANGED
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
[](LICENSE)
|
|
16
16
|
[](package.json)
|
|
17
17
|
[](docs/ARCHITECTURE.md)
|
|
18
|
+
[](https://ciphermesh.de)
|
|
18
19
|
|
|
19
|
-
**[
|
|
20
|
+
**[ciphermesh.de](https://ciphermesh.de)** — the website, and the public hub anyone can join.
|
|
21
|
+
|
|
22
|
+
**[🇧🇷 Leia em Português](README.pt-BR.md)** · [Setup Guide](docs/SETUP.md) · [Architecture](docs/ARCHITECTURE.md) · [Security Policy](SECURITY.md) · [Hub Terms](TERMS.md) · [Contributing](CONTRIBUTING.md)
|
|
20
23
|
|
|
21
24
|
<img src="docs/demo.svg" alt="CipherMesh terminal demo" width="720">
|
|
22
25
|
|
|
@@ -119,12 +122,15 @@ the server against a real CA (no trust-on-first-use window), and a host that
|
|
|
119
122
|
once served a valid certificate can never be silently downgraded to a
|
|
120
123
|
self-signed one.
|
|
121
124
|
|
|
122
|
-
**No Node
|
|
123
|
-
|
|
125
|
+
**No Node at all?** Every release ships standalone binaries for macOS and Linux
|
|
126
|
+
(arm64/x64) — download from the
|
|
124
127
|
[releases page](https://github.com/FelipeKreulich/secret-chat-lan/releases),
|
|
125
|
-
`chmod +x`, run. Nothing to install
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
`chmod +x`, run. Nothing to install, not even Node.
|
|
129
|
+
|
|
130
|
+
| Binary | What it is |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `ciphermesh-<platform>` | Everything: client, relay and P2P. `ciphermesh server` and `ciphermesh p2p` work exactly as they do on npm. |
|
|
133
|
+
| `ciphermesh-server-<platform>` | Relay only, for self-hosters who want nothing else on the machine. |
|
|
128
134
|
|
|
129
135
|
**Everyone** (including the host):
|
|
130
136
|
|
|
@@ -145,6 +151,35 @@ now speaks nearly the same command set as the relay client (presence, `/lock`,
|
|
|
145
151
|
`/contacts`, `/mentions`, `/topic`, encrypted history and more); only
|
|
146
152
|
`/invite`, `/create` and `/nick` are relay-specific by nature.
|
|
147
153
|
|
|
154
|
+
## 🌐 The community lives at ciphermesh.de
|
|
155
|
+
|
|
156
|
+
For a long time, using CipherMesh meant already knowing someone who ran a relay.
|
|
157
|
+
That is over: **[ciphermesh.de](https://ciphermesh.de)** is a public hub, free
|
|
158
|
+
and open to anyone, always on. Nobody has to coordinate who starts the server.
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npx ciphermesh@latest
|
|
162
|
+
# Server: ciphermesh.de
|
|
163
|
+
# Room: general
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- **`general` is the front door.** It is the default room — connect and say hello
|
|
167
|
+
if you just want to see who else is around.
|
|
168
|
+
- **Any room, opened by anyone.** Pick a name, tell the people you want in it,
|
|
169
|
+
start talking. No account, no invitation, no waiting list.
|
|
170
|
+
- **Private the moment you want it.** Add a password and the room is yours. The
|
|
171
|
+
password never reaches the server — only an Argon2id-derived verifier it cannot
|
|
172
|
+
reverse.
|
|
173
|
+
- **Nothing outlives the conversation.** A room exists while someone is inside
|
|
174
|
+
it. When the last person leaves, the room and everything said in it are gone.
|
|
175
|
+
|
|
176
|
+
Running the hub does not make its operator a reader: it is zero-knowledge like
|
|
177
|
+
every other CipherMesh relay — the only difference is that this one never goes to
|
|
178
|
+
sleep. Use is governed by the **[terms](TERMS.md)**.
|
|
179
|
+
|
|
180
|
+
Bring people in: `/invite` prints a joinable string and a QR code, and
|
|
181
|
+
`/rooms` shows what is live.
|
|
182
|
+
|
|
148
183
|
## 💬 Commands
|
|
149
184
|
|
|
150
185
|
<details>
|
package/README.pt-BR.md
CHANGED
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
[](LICENSE)
|
|
16
16
|
[](package.json)
|
|
17
17
|
[](docs/ARCHITECTURE.md)
|
|
18
|
+
[](https://ciphermesh.de)
|
|
18
19
|
|
|
19
|
-
**[
|
|
20
|
+
**[ciphermesh.de](https://ciphermesh.de)** — o site, e o hub público em que qualquer pessoa pode entrar.
|
|
21
|
+
|
|
22
|
+
**[🇺🇸 Read in English](README.md)** · [Guia de Setup](docs/SETUP.md) · [Arquitetura](docs/ARCHITECTURE.md) · [Política de Segurança](SECURITY.md) · [Termos do Hub](TERMS.md) · [Como contribuir](CONTRIBUTING.md)
|
|
20
23
|
|
|
21
24
|
<img src="docs/demo.svg" alt="Demo do CipherMesh no terminal" width="720">
|
|
22
25
|
|
|
@@ -120,12 +123,15 @@ clientes verificam o servidor contra uma CA de verdade (sem janela de
|
|
|
120
123
|
trust-on-first-use), e um host que já apresentou certificado válido nunca pode
|
|
121
124
|
ser rebaixado silenciosamente para um self-signed.
|
|
122
125
|
|
|
123
|
-
**Sem Node
|
|
124
|
-
|
|
126
|
+
**Sem Node nenhum?** Todo release traz binários standalone para macOS e Linux
|
|
127
|
+
(arm64/x64) — baixe da
|
|
125
128
|
[página de releases](https://github.com/FelipeKreulich/secret-chat-lan/releases),
|
|
126
|
-
`chmod +x`, rode. Nada para instalar
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
`chmod +x`, rode. Nada para instalar, nem Node.
|
|
130
|
+
|
|
131
|
+
| Binário | O que é |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `ciphermesh-<plataforma>` | Tudo: cliente, relay e P2P. `ciphermesh server` e `ciphermesh p2p` funcionam igual ao npm. |
|
|
134
|
+
| `ciphermesh-server-<plataforma>` | Só o relay, para quem hospeda e não quer mais nada na máquina. |
|
|
129
135
|
|
|
130
136
|
**Todo mundo** (incluindo quem hospeda):
|
|
131
137
|
|
|
@@ -146,6 +152,36 @@ P2P agora fala quase o mesmo conjunto de comandos do cliente relay (presença,
|
|
|
146
152
|
`/lock`, `/contacts`, `/mentions`, `/topic`, histórico cifrado e mais); só
|
|
147
153
|
`/invite`, `/create` e `/nick` são específicos do relay por natureza.
|
|
148
154
|
|
|
155
|
+
## 🌐 A comunidade fica no ciphermesh.de
|
|
156
|
+
|
|
157
|
+
Durante muito tempo, usar o CipherMesh exigia já conhecer alguém que rodasse um
|
|
158
|
+
relay. Isso acabou: **[ciphermesh.de](https://ciphermesh.de)** é um hub público,
|
|
159
|
+
gratuito, aberto a qualquer pessoa e sempre no ar. Ninguém precisa combinar quem
|
|
160
|
+
vai subir o servidor.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
npx ciphermesh@latest
|
|
164
|
+
# Server: ciphermesh.de
|
|
165
|
+
# Room: general
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- **A `general` é a porta de entrada.** É a sala padrão — conecte e diga oi se
|
|
169
|
+
você só quer ver quem está por perto.
|
|
170
|
+
- **Qualquer sala, aberta por qualquer um.** Escolha um nome, avise quem você quer
|
|
171
|
+
lá dentro e comece a conversar. Sem conta, sem convite, sem lista de espera.
|
|
172
|
+
- **Privada na hora que você quiser.** Ponha uma senha e a sala é sua. A senha
|
|
173
|
+
nunca chega ao servidor — ele guarda só um verificador derivado com Argon2id,
|
|
174
|
+
que não dá para reverter.
|
|
175
|
+
- **Nada sobrevive à conversa.** A sala existe enquanto alguém está dentro dela.
|
|
176
|
+
Quando a última pessoa sai, a sala e tudo o que foi dito nela desaparecem.
|
|
177
|
+
|
|
178
|
+
Manter o hub não transforma quem o opera em leitor: ele é zero-knowledge como
|
|
179
|
+
qualquer outro relay do CipherMesh — a única diferença é que este nunca dorme. O
|
|
180
|
+
uso é regido pelos **[termos](TERMS.md)**.
|
|
181
|
+
|
|
182
|
+
Para chamar gente: `/invite` imprime uma string de entrada e um QR code, e
|
|
183
|
+
`/rooms` mostra o que está no ar.
|
|
184
|
+
|
|
149
185
|
## 💬 Comandos
|
|
150
186
|
|
|
151
187
|
<details>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Entry point for the standalone CipherMesh binary (`bun build --compile`).
|
|
2
|
+
//
|
|
3
|
+
// Separate from bin/ciphermesh.js because that one dispatches through
|
|
4
|
+
// `await import(new URL(target, import.meta.url))` — a computed specifier no
|
|
5
|
+
// bundler can follow, which is why the binary used to contain nothing but the
|
|
6
|
+
// launcher. Here the three specifiers are literal, so the bundler pulls in all
|
|
7
|
+
// three modes and the branch picks one at runtime.
|
|
8
|
+
//
|
|
9
|
+
// The version is injected at build time: reading package.json off disk does not
|
|
10
|
+
// work once everything lives inside the binary.
|
|
11
|
+
const VERSION = process.env.CIPHERMESH_VERSION || 'unknown';
|
|
12
|
+
|
|
13
|
+
const HELP = `CipherMesh — end-to-end encrypted terminal chat
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
ciphermesh [client] connect to a relay server (default)
|
|
17
|
+
ciphermesh server run the zero-knowledge relay server
|
|
18
|
+
ciphermesh p2p serverless mode (mDNS peer discovery on the LAN)
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
-h, --help show this help
|
|
22
|
+
-v, --version show the version
|
|
23
|
+
|
|
24
|
+
Client options:
|
|
25
|
+
--setup re-run the first-run setup wizard
|
|
26
|
+
--no-onboard skip the first-run wizard (scripts/CI)
|
|
27
|
+
--fresh forget the last session (server/room) and start clean
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const arg = process.argv[2];
|
|
31
|
+
|
|
32
|
+
if (arg === '-h' || arg === '--help' || arg === 'help') {
|
|
33
|
+
process.stdout.write(HELP);
|
|
34
|
+
} else if (arg === '-v' || arg === '--version') {
|
|
35
|
+
process.stdout.write(`${VERSION}\n`);
|
|
36
|
+
} else if (arg === 'server') {
|
|
37
|
+
await import('../src/server/index.js');
|
|
38
|
+
} else if (arg === 'p2p') {
|
|
39
|
+
await import('../src/p2p/index.js');
|
|
40
|
+
} else {
|
|
41
|
+
await import('../src/client/index.js');
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ciphermesh",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.1",
|
|
4
4
|
"description": "Secure terminal chat for the local network (LAN) with real end-to-end encryption (E2EE) using libsodium",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/client/index.js",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"examples",
|
|
15
15
|
"README.md",
|
|
16
16
|
"README.pt-BR.md",
|
|
17
|
-
"LICENSE"
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"CHANGELOG.md"
|
|
18
19
|
],
|
|
19
20
|
"engines": {
|
|
20
21
|
"node": ">=20.0.0"
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"type": "git",
|
|
27
28
|
"url": "git+https://github.com/FelipeKreulich/secret-chat-lan.git"
|
|
28
29
|
},
|
|
29
|
-
"homepage": "https://
|
|
30
|
+
"homepage": "https://ciphermesh.de",
|
|
30
31
|
"bugs": {
|
|
31
32
|
"url": "https://github.com/FelipeKreulich/secret-chat-lan/issues"
|
|
32
33
|
},
|
|
@@ -53,10 +54,9 @@
|
|
|
53
54
|
"dependencies": {
|
|
54
55
|
"@noble/post-quantum": "0.6.1",
|
|
55
56
|
"blessed": "0.1.81",
|
|
56
|
-
"bonjour-service": "1.4.
|
|
57
|
+
"bonjour-service": "1.4.4",
|
|
57
58
|
"boxen": "8.0.1",
|
|
58
59
|
"chalk": "^5.4.1",
|
|
59
|
-
"figlet": "1.11.3",
|
|
60
60
|
"gradient-string": "3.0.0",
|
|
61
61
|
"jimp": "1.6.1",
|
|
62
62
|
"node-notifier": "10.0.1",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@eslint/js": "10.0.1",
|
|
69
69
|
"eslint": "10.8.0",
|
|
70
|
+
"figlet": "^1.11.4",
|
|
70
71
|
"globals": "^17.7.0",
|
|
71
72
|
"prettier": "^3.9.5"
|
|
72
73
|
},
|
package/src/server/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { SessionManager } from './SessionManager.js';
|
|
|
9
9
|
import { MessageRouter } from './MessageRouter.js';
|
|
10
10
|
import { OfflineQueue } from './OfflineQueue.js';
|
|
11
11
|
import { SecureWSServer } from './WebSocketServer.js';
|
|
12
|
+
import { startPresenceServer } from './presence.js';
|
|
12
13
|
import { loadOrGenerateCerts } from './CertManager.js';
|
|
13
14
|
|
|
14
15
|
const log = createLogger('server');
|
|
@@ -55,6 +56,12 @@ function getLocalIPs() {
|
|
|
55
56
|
// ── Bootstrap ──────────────────────────────────────────────────
|
|
56
57
|
const port = parseInt(process.env.PORT, 10) || SERVER_PORT;
|
|
57
58
|
|
|
59
|
+
/** A port number, or 0 meaning "do not listen". */
|
|
60
|
+
function positivePort(value) {
|
|
61
|
+
const n = parseInt(value, 10);
|
|
62
|
+
return Number.isInteger(n) && n > 0 && n < 65536 ? n : 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
const useTls = process.env.TLS !== 'false'; // default: true
|
|
59
66
|
const tlsOptions = useTls ? loadOrGenerateCerts() : null;
|
|
60
67
|
|
|
@@ -63,6 +70,11 @@ const offlineQueue = new OfflineQueue();
|
|
|
63
70
|
const messageRouter = new MessageRouter(sessionManager, offlineQueue);
|
|
64
71
|
const server = new SecureWSServer(sessionManager, messageRouter, offlineQueue, port, tlsOptions);
|
|
65
72
|
|
|
73
|
+
// Optional, off unless PRESENCE_PORT is set. Publishes how busy the hub is as a
|
|
74
|
+
// bucket — never a count, never a room name. Runs on its own listener so a
|
|
75
|
+
// problem here cannot reach the chat.
|
|
76
|
+
startPresenceServer(sessionManager, positivePort(process.env.PRESENCE_PORT));
|
|
77
|
+
|
|
66
78
|
// Cleanup offline queue and recently left peers every 5 minutes
|
|
67
79
|
setInterval(
|
|
68
80
|
() => {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { createLogger } from '../shared/logger.js';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('presence');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Buckets, not counts.
|
|
8
|
+
*
|
|
9
|
+
* A live "3 people online" would let anyone with a polling script watch people
|
|
10
|
+
* arrive and leave — exactly the metadata the relay is built not to hand out.
|
|
11
|
+
* Bucketing hides that: within a bucket, joins and leaves are invisible, and the
|
|
12
|
+
* only thing an observer learns is roughly how busy the hub is.
|
|
13
|
+
*
|
|
14
|
+
* Crossing a boundary is still observable (5 → 6 says somebody joined). That is
|
|
15
|
+
* inherent to publishing anything at all, and it is a far smaller disclosure
|
|
16
|
+
* than an exact number. Wider buckets would leak less and say less; this is the
|
|
17
|
+
* trade-off, stated out loud rather than hidden.
|
|
18
|
+
*/
|
|
19
|
+
const BUCKETS = [
|
|
20
|
+
{ max: 0, label: '0' },
|
|
21
|
+
{ max: 5, label: '1-5' },
|
|
22
|
+
{ max: 20, label: '6-20' },
|
|
23
|
+
{ max: 50, label: '21-50' },
|
|
24
|
+
{ max: Infinity, label: '50+' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function bucketFor(count) {
|
|
28
|
+
const n = Number.isFinite(count) && count > 0 ? Math.floor(count) : 0;
|
|
29
|
+
return BUCKETS.find((b) => n <= b.max).label;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A tiny HTTP listener that publishes how busy the hub is, and nothing else.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately a SEPARATE server from the WebSocket one: if anything here
|
|
36
|
+
* misbehaves, chat is untouched. It is meant to be reachable only from the
|
|
37
|
+
* internal network (the landing page's container asks it), never published to
|
|
38
|
+
* the host — see deploy/docker-compose.public.yml.
|
|
39
|
+
*
|
|
40
|
+
* What it will never expose: room names, nicknames, addresses, or an exact
|
|
41
|
+
* count. Those are the things the relay's whole design refuses to hand over,
|
|
42
|
+
* and a convenience endpoint is not a good reason to start.
|
|
43
|
+
*
|
|
44
|
+
* @param {{ size: number }} sessionManager
|
|
45
|
+
* @param {number} port - 0 or unset disables the listener entirely
|
|
46
|
+
*/
|
|
47
|
+
export function startPresenceServer(sessionManager, port) {
|
|
48
|
+
if (!port) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const server = createServer((req, res) => {
|
|
53
|
+
if (req.method !== 'GET' || !req.url.startsWith('/presence')) {
|
|
54
|
+
res.writeHead(404).end();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const body = JSON.stringify({ online: bucketFor(sessionManager.size) });
|
|
59
|
+
res.writeHead(200, {
|
|
60
|
+
'content-type': 'application/json',
|
|
61
|
+
// The caller caches; this only stops an intermediary from serving a
|
|
62
|
+
// stale bucket for longer than the bucket is meaningful.
|
|
63
|
+
'cache-control': 'public, max-age=30',
|
|
64
|
+
'content-length': Buffer.byteLength(body),
|
|
65
|
+
});
|
|
66
|
+
res.end(body);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
server.on('error', (err) => {
|
|
70
|
+
// Presence is a nice-to-have. It must never take the relay down with it.
|
|
71
|
+
log.error(`Presence server error: ${err.message}`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Bind to all interfaces so the site's container can reach it over the Docker
|
|
75
|
+
// network. The port is never published to the host.
|
|
76
|
+
server.listen(port, () => log.info(`Presence endpoint on port ${port}`));
|
|
77
|
+
|
|
78
|
+
return server;
|
|
79
|
+
}
|
package/src/shared/banner.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import figlet from 'figlet';
|
|
2
1
|
import gradient from 'gradient-string';
|
|
3
2
|
import chalk from 'chalk';
|
|
4
3
|
import boxen from 'boxen';
|
|
@@ -7,8 +6,36 @@ const neon = gradient(['#00ff9f', '#00b8ff', '#7b2dff', '#ff2dff']);
|
|
|
7
6
|
const cyber = gradient(['#f72585', '#7209b7', '#3a0ca3', '#4361ee', '#4cc9f0']);
|
|
8
7
|
const mint = gradient(['#00ff9f', '#00b8ff']);
|
|
9
8
|
|
|
9
|
+
/**
|
|
10
|
+
* "CipherMesh" in figlet's ANSI Shadow, rendered once and kept.
|
|
11
|
+
*
|
|
12
|
+
* It used to be generated at startup with `figlet.textSync`, which reads the
|
|
13
|
+
* font file from disk — fine under Node, fatal inside a compiled binary, where
|
|
14
|
+
* `fonts/ANSI Shadow.flf` does not exist and the process dies before printing
|
|
15
|
+
* anything (issue #414). The word never changes, so there is nothing to
|
|
16
|
+
* generate: the art is the constant it always was.
|
|
17
|
+
*
|
|
18
|
+
* `test/banner.test.js` pins it against figlet's own output, so a change here
|
|
19
|
+
* has to be deliberate.
|
|
20
|
+
*/
|
|
21
|
+
const ART = [
|
|
22
|
+
' ██████╗██╗██████╗ ██╗ ██╗███████╗██████╗ ███╗ ███╗███████╗███████╗██╗ ██╗',
|
|
23
|
+
'██╔════╝██║██╔══██╗██║ ██║██╔════╝██╔══██╗████╗ ████║██╔════╝██╔════╝██║ ██║',
|
|
24
|
+
'██║ ██║██████╔╝███████║█████╗ ██████╔╝██╔████╔██║█████╗ ███████╗███████║',
|
|
25
|
+
'██║ ██║██╔═══╝ ██╔══██║██╔══╝ ██╔══██╗██║╚██╔╝██║██╔══╝ ╚════██║██╔══██║',
|
|
26
|
+
'╚██████╗██║██║ ██║ ██║███████╗██║ ██║██║ ╚═╝ ██║███████╗███████║██║ ██║',
|
|
27
|
+
' ╚═════╝╚═╝╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝',
|
|
28
|
+
// figlet pads the block to a seventh, blank line. It is not decoration: the
|
|
29
|
+
// animations count lines to move the cursor back up (`\x1b[${N + 1}A`), so
|
|
30
|
+
// dropping it would shift every redraw by one row.
|
|
31
|
+
' '.repeat(78),
|
|
32
|
+
].join('\n');
|
|
33
|
+
|
|
34
|
+
/** Byte-for-byte what `figlet.textSync('CipherMesh', { font: 'ANSI Shadow' })` returns. */
|
|
35
|
+
export const LOGO_ART = ART;
|
|
36
|
+
|
|
10
37
|
function logo() {
|
|
11
|
-
return neon(
|
|
38
|
+
return neon(LOGO_ART);
|
|
12
39
|
}
|
|
13
40
|
|
|
14
41
|
export function serverBanner(port, network, tls = false) {
|
|
@@ -80,7 +107,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
80
107
|
// Animated splash: the neon gradient "flows" through the ANSI-Shadow logo for
|
|
81
108
|
// ~1s, then settles. Runs before the TUI (plain terminal output).
|
|
82
109
|
export async function animatedBanner(subtitle = ' ░▒▓ End-to-End Encrypted Chat ▓▒░') {
|
|
83
|
-
const art =
|
|
110
|
+
const art = ART;
|
|
84
111
|
const artLines = art.split('\n');
|
|
85
112
|
const N = artLines.length;
|
|
86
113
|
|
|
@@ -132,7 +159,7 @@ function mixHex(a, b, t) {
|
|
|
132
159
|
// Goodbye animation on /quit: the neon logo fades to black over ~0.8s while a
|
|
133
160
|
// farewell line shows, then the process exits. Static fallback on non-TTY.
|
|
134
161
|
export async function farewellBanner(message = ' 🔒 Session ended — keys wiped from memory') {
|
|
135
|
-
const art =
|
|
162
|
+
const art = ART;
|
|
136
163
|
const N = art.split('\n').length;
|
|
137
164
|
|
|
138
165
|
if (!process.stdout.isTTY) {
|