@solarpunkltd/swarm-chat-js 9.0.0 → 10.0.0
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/README.md +238 -252
- package/dist/index.cjs +1 -1
- package/dist/index.js +27 -57
- package/dist/message/binding.d.ts +7 -7
- package/dist/message/message.d.ts +7 -6
- package/dist/message/personalSign.d.ts +1 -1
- package/dist/message/walletLink.d.ts +5 -5
- package/dist/{message-DQda4_Ge.js → message-BUFAxjtN.js} +16 -20
- package/dist/message-BjeTRy2F.cjs +4 -0
- package/dist/message.cjs +1 -1
- package/dist/message.js +1 -2
- package/dist/pacer.d.ts +1 -1
- package/package.json +3 -3
- package/dist/message-DBi9VeRs.cjs +0 -4
package/README.md
CHANGED
|
@@ -1,37 +1,59 @@
|
|
|
1
1
|
# Swarm Chat JS
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
`@solarpunkltd/swarm-chat-js` is a client library that adds a chat to any web page or Node program, with every message
|
|
4
|
+
stored on [Swarm](https://www.ethswarm.org/), the decentralised storage network. A page that uses it needs no web server
|
|
5
|
+
of its own. It talks only to a Bee node, which is a Swarm node, or to a gateway, which is a Bee node that serves the
|
|
6
|
+
public over HTTP.
|
|
7
|
+
|
|
8
|
+
The library is one half of a chat. The other half is its server,
|
|
9
|
+
[swarm-chat-aggregator-js](https://github.com/Solar-Punk-Ltd/swarm-chat-aggregator-js), which receives the messages,
|
|
10
|
+
checks them and publishes them for every reader. Any project can run the server and use the library to add chat on
|
|
11
|
+
Swarm. One project built on them is [stream-event-client](https://github.com/Solar-Punk-Ltd/stream-event-client), a
|
|
12
|
+
viewer for live streams with a chat beside each video.
|
|
13
|
+
|
|
14
|
+
## Terms
|
|
15
|
+
|
|
16
|
+
- **Chunk.** The unit Swarm stores, at most 4,096 bytes, found by its address.
|
|
17
|
+
- **Single owner chunk.** A chunk whose address comes from an owner's key and an identifier, so only that key can write
|
|
18
|
+
it.
|
|
19
|
+
- **GSOC.** A single owner chunk whose key is shared on purpose, so anyone can write to it. A Bee node near its address
|
|
20
|
+
can subscribe to it and hears every write. The chat uses one GSOC address as a shared inbox.
|
|
21
|
+
- **Feed.** A sequence of single owner chunks written by one owner under one topic, at index 0, 1, 2 and on. Each chat is
|
|
22
|
+
one feed, and the server is its only writer.
|
|
23
|
+
- **Postage stamp.** Every write to Swarm is paid for with a stamp from a postage batch. A gateway can stamp writes
|
|
24
|
+
itself, or a sender can stamp with a batch of its own.
|
|
11
25
|
|
|
12
26
|
## How it works
|
|
13
27
|
|
|
14
|
-
1. **Sending.** The
|
|
15
|
-
|
|
16
|
-
|
|
28
|
+
1. **Sending.** The library builds the message, signs it with the user's key, and writes it once to the inbox. The
|
|
29
|
+
message stays pending until the chat's feed shows it. If it does not show, the identical bytes are written again
|
|
30
|
+
every ten seconds, up to five times, and the message is then marked failed so the page can offer a retry.
|
|
17
31
|
2. **Publishing.** The server checks each message's shape and signature and writes it to the chat's feed as the next
|
|
18
|
-
entry
|
|
19
|
-
|
|
20
|
-
seconds while the chat is active, it writes a slot note naming the newest entry. See
|
|
32
|
+
entry. After each publish it saves a history file of the chat, and every entry links to the newest history file.
|
|
33
|
+
The server also writes slot notes, small chunks that say which entries exist. See
|
|
21
34
|
[Slot notes](#slot-notes-for-servers-and-tools).
|
|
22
|
-
3. **Opening.** A reader reads the notes of the
|
|
23
|
-
newest note
|
|
24
|
-
|
|
25
|
-
4. **Following.** Once each time slot is over, plus
|
|
26
|
-
feed entries up to the newest one a note named,
|
|
27
|
-
written. Bee answers such a request by skipping its peers for that address for a
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
3. **Opening.** A reader reads the slot notes of the last few seconds, newest first, going back up to one heartbeat
|
|
36
|
+
(30 seconds by default). The newest note names the newest entry. The reader shows that entry's history file and
|
|
37
|
+
reads the entries after it. When it finds no note, it asks Bee for the feed's newest index instead.
|
|
38
|
+
4. **Following.** Once each two-second time slot is over, plus one second, the reader asks for that slot's note once.
|
|
39
|
+
It reads feed entries only up to the newest one a note named, so it never asks Bee for an address that is not
|
|
40
|
+
written yet. This matters because Bee answers such a request by skipping its peers for that address for about a
|
|
41
|
+
minute, which would delay the message once it is written. An entry a note named that does not load is read again
|
|
42
|
+
later, so a message arrives late rather than never. A slot that fails its checks is skipped. Gateway failures back
|
|
43
|
+
off with jitter, up to eight seconds. A reader that finds no note polls the next feed index instead, and switches
|
|
44
|
+
to notes as soon as it finds one. An address that came back missing is asked for again no sooner than 20 seconds
|
|
45
|
+
later.
|
|
46
|
+
5. **Older messages.** Loading older messages is a call the page makes, usually on a click. It reads the history file
|
|
47
|
+
before the oldest one shown.
|
|
48
|
+
|
|
49
|
+
## What you need
|
|
50
|
+
|
|
51
|
+
- A running [swarm-chat-aggregator-js](https://github.com/Solar-Punk-Ltd/swarm-chat-aggregator-js). Its operator gives
|
|
52
|
+
you the inbox identifier and key, the feed owner address and the slot note timing.
|
|
53
|
+
- A Bee node or gateway to read through, and one to write through. Write through a different Bee node from the one the
|
|
54
|
+
server listens on. Bee hands an inbox write to the server only when the chunk arrives from another node, so a write
|
|
55
|
+
made on the listening node itself reaches the server only if it later syncs back.
|
|
56
|
+
- A way to pay for the inbox writes: a gateway that stamps them, or a postage batch of the sender's own.
|
|
35
57
|
|
|
36
58
|
## Installation
|
|
37
59
|
|
|
@@ -74,26 +96,26 @@ await chat.stop();
|
|
|
74
96
|
|
|
75
97
|
## Settings
|
|
76
98
|
|
|
77
|
-
| Setting | Meaning
|
|
78
|
-
| ------------------------ |
|
|
79
|
-
| `user.privateKey` | Signs every message. A viewer that only reads may pass any key.
|
|
80
|
-
| `user.nickname` | The name on every message, 1 to 20 characters
|
|
81
|
-
| `user.binding` | Optional. A signed-in viewer's Swarm ID chat binding, `{ msg, sig }`, for the chat address of `user.privateKey`. Every message then carries it.
|
|
82
|
-
| `user.walletLink` | Optional, only with `user.binding`. The viewer's Swarm ID wallet link, `{ msg, wsig, asig }`, for the binding's account. Every message then carries it.
|
|
83
|
-
| `infra.beeUrl` | The Bee node or gateway the chat is read through, and written through when `infra.writeUrl` is not set.
|
|
84
|
-
| `infra.writeUrl` | Optional. The Bee node or gateway the inbox writes go through, such as a write gateway that stamps them, when they go somewhere other than the reads.
|
|
85
|
-
| `infra.stamp` | The batch the inbox writes are stamped with. Left out for a gateway that stamps writes itself.
|
|
86
|
-
| `infra.via` | Required. Who pays for the inbox writes, `MessageVia.GATEWAY` or `MessageVia.OWN_STAMP
|
|
87
|
-
| `infra.gsocTopic` | The inbox's identifier string, the same for every chat.
|
|
88
|
-
| `infra.gsocResourceId` | The
|
|
89
|
-
| `infra.chatTopic` | The chat's topic
|
|
90
|
-
| `infra.chatAddress` | The server's feed owner address.
|
|
91
|
-
| `infra.pollingInterval` | The shortest wait between two polls, and the first wait after the gateway failed
|
|
92
|
-
| `infra.socReadTimeout` |
|
|
93
|
-
| `infra.feedReadTimeout` | The
|
|
94
|
-
| `infra.gsocWriteTimeout` |
|
|
95
|
-
| `infra.noteSlotMs` | The server's `NOTE_SLOT_MS`, 2,000 ms by default. It is part of every note's address, so a different value finds no notes and polls instead.
|
|
96
|
-
| `infra.noteHeartbeatMs` | The server's `NOTE_HEARTBEAT_MS`, 30,000 ms by default
|
|
99
|
+
| Setting | Meaning |
|
|
100
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| `user.privateKey` | Signs every message. A viewer that only reads may pass any key. |
|
|
102
|
+
| `user.nickname` | The name on every message, 1 to 20 characters. It may not read as an ENS name ending in `.eth`. Nothing can be sent without one. |
|
|
103
|
+
| `user.binding` | Optional. A signed-in viewer's Swarm ID chat binding, `{ msg, sig }`, for the chat address of `user.privateKey`. Every message then carries it. |
|
|
104
|
+
| `user.walletLink` | Optional, only with `user.binding`. The viewer's Swarm ID wallet link, `{ msg, wsig, asig }`, for the binding's account. Every message then carries it. |
|
|
105
|
+
| `infra.beeUrl` | The Bee node or gateway the chat is read through, and written through when `infra.writeUrl` is not set. |
|
|
106
|
+
| `infra.writeUrl` | Optional. The Bee node or gateway the inbox writes go through, such as a write gateway that stamps them, when they go somewhere other than the reads. |
|
|
107
|
+
| `infra.stamp` | The postage batch id the inbox writes are stamped with. Left out for a gateway that stamps writes itself. |
|
|
108
|
+
| `infra.via` | Required. Who pays for the inbox writes, `MessageVia.GATEWAY` or `MessageVia.OWN_STAMP`. Every message records it in `via`. |
|
|
109
|
+
| `infra.gsocTopic` | The inbox's identifier string, the same for every chat on one server. |
|
|
110
|
+
| `infra.gsocResourceId` | The inbox key every sender signs inbox writes with. The server's operator mines it so the inbox sits near the server's listening node. |
|
|
111
|
+
| `infra.chatTopic` | The chat's topic. Every message carries it, and the chat's feed is named by it. |
|
|
112
|
+
| `infra.chatAddress` | The server's feed owner address. |
|
|
113
|
+
| `infra.pollingInterval` | The shortest wait between two polls, and the first wait after the gateway failed. 1,000 ms by default. |
|
|
114
|
+
| `infra.socReadTimeout` | The time limit for one feed entry read. 5,000 ms by default. |
|
|
115
|
+
| `infra.feedReadTimeout` | The time limit for the newest-index lookup and for a history file download. 15,000 ms by default. A lookup that runs out of time reads the chat from index 0. |
|
|
116
|
+
| `infra.gsocWriteTimeout` | The time limit for one inbox write. 10,000 ms by default. |
|
|
117
|
+
| `infra.noteSlotMs` | The server's `NOTE_SLOT_MS`, 2,000 ms by default. It is part of every note's address, so a different value finds no notes and the reader polls instead. |
|
|
118
|
+
| `infra.noteHeartbeatMs` | The server's `NOTE_HEARTBEAT_MS`, 30,000 ms by default. It sets how far back an opening reader looks for a note. |
|
|
97
119
|
|
|
98
120
|
## Methods
|
|
99
121
|
|
|
@@ -106,7 +128,7 @@ await chat.stop();
|
|
|
106
128
|
| `retrySendMessage(message)` | Writes a failed or pending message again with its identical bytes. |
|
|
107
129
|
| `fetchPreviousMessages()` | Shows the history file before the oldest one shown, and resolves to its messages. |
|
|
108
130
|
| `hasPreviousMessages()` | Whether there is an older file to load. |
|
|
109
|
-
| `orderMessages(messages)` | Published messages in chat order, then pending ones by
|
|
131
|
+
| `orderMessages(messages)` | Published messages in chat order, then pending ones by the sender's clock. |
|
|
110
132
|
| `getStatus()` | `live`, `reconnecting`, `stalled`, or `null` before the first read. |
|
|
111
133
|
| `getAddress()` | The address of the key, which is every sent message's `address`. |
|
|
112
134
|
|
|
@@ -118,74 +140,109 @@ await chat.stop();
|
|
|
118
140
|
| `LOADING_PREVIOUS_MESSAGES` | `boolean` | Around a load of older messages. |
|
|
119
141
|
| `MESSAGE_RECEIVED` | `MessageData` | A message the feed or a history file shows. For a sender, this is what "sent" means. |
|
|
120
142
|
| `MESSAGE_REQUEST_INITIATED` | `MessageData` | A message signed and about to be written, with `index` -1. |
|
|
121
|
-
| `MESSAGE_REQUEST_UPLOADED` | `MessageData` |
|
|
122
|
-
| `MESSAGE_REQUEST_ERROR` | `MessageData` | Its resends ran out
|
|
143
|
+
| `MESSAGE_REQUEST_UPLOADED` | `MessageData` | The first write of the message that the node accepted. It is still pending. |
|
|
144
|
+
| `MESSAGE_REQUEST_ERROR` | `MessageData` | Its resends ran out and the feed never showed it. |
|
|
123
145
|
| `STATUS` | `FeedStatus` | `live`: the gateway answers. `reconnecting`: it does not. `stalled`: a message known to exist has not loaded for eight seconds. |
|
|
124
|
-
| `MESSAGE_SKIPPED` | `SkippedMessage` | A feed
|
|
146
|
+
| `MESSAGE_SKIPPED` | `SkippedMessage` | A feed entry or history row that is not shown, and why. |
|
|
125
147
|
| `ERROR` | `unknown` | A failure the chat recovers from on its own. |
|
|
126
148
|
| `CRITICAL_ERROR` | `unknown` | Opening failed three times in a row. It keeps trying. |
|
|
127
149
|
|
|
128
150
|
**The order of a sent message's events.** `MESSAGE_REQUEST_INITIATED` comes first, then `MESSAGE_REQUEST_UPLOADED`
|
|
129
151
|
once a write is accepted, then `MESSAGE_RECEIVED` when the feed shows it. The resends and the give-up wait while the
|
|
130
|
-
status is not `live`, so a message is not failed only because this viewer
|
|
131
|
-
be failed and then shown, for example when the server published it after the last resend
|
|
132
|
-
follow `MESSAGE_REQUEST_ERROR` for the same `id`, and
|
|
152
|
+
status is not `live`, so a message is not marked failed only because this viewer cannot see the feed. A message can
|
|
153
|
+
still be marked failed and then shown, for example when the server published it after the last resend. So
|
|
154
|
+
`MESSAGE_RECEIVED` may follow `MESSAGE_REQUEST_ERROR` for the same `id`, and the page keeps the received one.
|
|
155
|
+
|
|
156
|
+
A `MessageData` has these fields:
|
|
133
157
|
|
|
134
|
-
|
|
135
|
-
receive time, or the sender's clock while pending
|
|
136
|
-
-
|
|
137
|
-
|
|
138
|
-
|
|
158
|
+
- `id`, `type`, `message` (the text or emoji), `username` and `address`.
|
|
159
|
+
- `timestamp`: the server's receive time, or the sender's clock while pending.
|
|
160
|
+
- `targetMessageId`: the message a reply or reaction refers to.
|
|
161
|
+
- `index`: the feed index, -1 while pending.
|
|
162
|
+
- `signedMessage`: the message exactly as its sender signed it.
|
|
163
|
+
|
|
164
|
+
Everything else the message says, such as its topic, its signature, the sender's clock, `via` and the binding, is read
|
|
165
|
+
from `signedMessage`. A page that shows who sent a message hands `signedMessage` to `verifyChatMessage`. See
|
|
166
|
+
[Verifying](#verifying).
|
|
139
167
|
|
|
140
168
|
## The message, for servers and tools
|
|
141
169
|
|
|
142
|
-
`@solarpunkltd/swarm-chat-js/message` is the one place the message is built, signed and checked
|
|
143
|
-
|
|
170
|
+
`@solarpunkltd/swarm-chat-js/message` is the one place the message is built, signed and checked. The server imports it
|
|
171
|
+
rather than keeping its own copy. This entry point loads none of the reader.
|
|
144
172
|
|
|
145
173
|
```ts
|
|
146
174
|
import { parseChatMessage, MAX_MESSAGE_BYTES } from '@solarpunkltd/swarm-chat-js/message';
|
|
147
175
|
|
|
148
|
-
const check = parseChatMessage(payload, {
|
|
176
|
+
const check = parseChatMessage(payload, { origins: ['https://example.com'] });
|
|
149
177
|
if (check.ok) publish(check.message);
|
|
150
178
|
else count(check.reason);
|
|
151
179
|
```
|
|
152
180
|
|
|
153
|
-
A message is a JSON object of at most 3,072 bytes of UTF-8, its fields in this order:
|
|
154
|
-
|
|
155
|
-
| Field | Meaning
|
|
156
|
-
| -------- |
|
|
157
|
-
| `v` | `9
|
|
158
|
-
| `topic` | The chat's topic, 1 to 128 characters
|
|
159
|
-
| `id` | A random id, 32 lowercase hex characters
|
|
160
|
-
| `type` | `text`, `thread` (a reply, `target` is the parent) or `reaction` (`target` is the message, `text` the emoji)
|
|
161
|
-
| `target` | The id it refers to, or `""` for a text
|
|
162
|
-
| `text` | 1 to 500 characters
|
|
163
|
-
| `name` | 1 to 20 characters,
|
|
164
|
-
| `addr` | The sender's chat address, 40 lowercase hex characters without `0x`
|
|
165
|
-
| `ts` | The sender's clock in milliseconds
|
|
166
|
-
| `via` | Who paid for the inbox write: `gateway` (the gateway's stamp) or `own-stamp` (the sender's own)
|
|
167
|
-
| `idb` | Only for a signed-in sender: its Swarm ID chat binding, `{ msg, sig }`
|
|
168
|
-
| `wl` | Only beside `idb`, for a sender who shows an ENS name: its Swarm ID wallet link, `{ msg, wsig, asig }`
|
|
169
|
-
| `sig` | 130 lowercase hex characters of r, s and v, with v 27 or 28 and s in the lower half
|
|
170
|
-
|
|
171
|
-
Characters are counted as Unicode code points, so an emoji counts once
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
`
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
181
|
+
A message is a JSON object of at most 3,072 bytes of UTF-8, with its fields in this order:
|
|
182
|
+
|
|
183
|
+
| Field | Meaning |
|
|
184
|
+
| -------- | ------------------------------------------------------------------------------------------------------------ |
|
|
185
|
+
| `v` | `9`, the message format version |
|
|
186
|
+
| `topic` | The chat's topic, 1 to 128 characters |
|
|
187
|
+
| `id` | A random id, 32 lowercase hex characters |
|
|
188
|
+
| `type` | `text`, `thread` (a reply, `target` is the parent) or `reaction` (`target` is the message, `text` the emoji) |
|
|
189
|
+
| `target` | The id it refers to, or `""` for a text |
|
|
190
|
+
| `text` | 1 to 500 characters |
|
|
191
|
+
| `name` | The display name, 1 to 20 characters, with the limits in [Display names](#display-names) |
|
|
192
|
+
| `addr` | The sender's chat address, 40 lowercase hex characters without `0x` |
|
|
193
|
+
| `ts` | The sender's clock in milliseconds |
|
|
194
|
+
| `via` | Who paid for the inbox write: `gateway` (the gateway's stamp) or `own-stamp` (the sender's own) |
|
|
195
|
+
| `idb` | Only for a signed-in sender: its Swarm ID chat binding, `{ msg, sig }` |
|
|
196
|
+
| `wl` | Only beside `idb`, for a sender who shows an ENS name: its Swarm ID wallet link, `{ msg, wsig, asig }` |
|
|
197
|
+
| `sig` | 130 lowercase hex characters of r, s and v, with v 27 or 28 and s in the lower half |
|
|
198
|
+
|
|
199
|
+
Characters are counted as Unicode code points, so an emoji counts once. `countCharacters` counts the same way, for a
|
|
200
|
+
text box that shows a count.
|
|
201
|
+
|
|
202
|
+
The signed bytes are the UTF-8 of
|
|
203
|
+
`JSON.stringify([9, topic, id, type, target, text, name, addr, ts, via, idb?.msg ?? '', idb?.sig ?? ''])`. When the
|
|
204
|
+
message carries a wallet link, `wl.msg`, `wl.wsig` and `wl.asig` are appended to that array. The library signs with
|
|
205
|
+
bee-js's `PrivateKey.sign` and checks with `Signature.recoverPublicKey`, both over the raw bytes. So `via`, the binding
|
|
206
|
+
and the wallet link cannot be changed, added or removed without breaking the signature.
|
|
207
|
+
|
|
208
|
+
Every signature has a second valid spelling, with s replaced by n - s and v flipped, and bee-js reads any v other than
|
|
209
|
+
27 as 28. The check therefore accepts only the low s spelling with v 27 or 28, which is the one bee-js produces. One
|
|
210
|
+
message has exactly one valid signature.
|
|
211
|
+
|
|
212
|
+
### Display names
|
|
213
|
+
|
|
214
|
+
A name may not contain a Unicode format or control character (`\p{Cf}`, `\p{Cc}`), such as the right-to-left override
|
|
215
|
+
U+202E or a zero width space, because those can make one name look like another. The zero width non-joiner U+200C and
|
|
216
|
+
the zero width joiner U+200D are allowed, because Persian and Indic names and joined emoji need them. Variation
|
|
217
|
+
selectors such as U+FE0F are allowed too. Letters and marks that show as nothing are refused: the braille blank U+2800,
|
|
218
|
+
the Hangul fillers U+115F, U+1160, U+3164 and U+FFA0, the combining grapheme joiner U+034F and the Khmer inherent vowels
|
|
219
|
+
U+17B4 and U+17B5.
|
|
220
|
+
|
|
221
|
+
A name that reads as ending in `.eth` is refused, because a page may show a verified ENS name in the same place and a
|
|
222
|
+
chosen name could pass for one. The check builds a skeleton of the name, the name as a reader sees it:
|
|
223
|
+
|
|
224
|
+
- NFKD normalised, so fullwidth and other compatibility forms fold and accented letters lose their accents
|
|
225
|
+
- every character Unicode's confusables data maps to the full stop read as `.`, and the ideographic full stop U+3002
|
|
226
|
+
too, since ENS reads it as one
|
|
227
|
+
- Unicode default ignorable code points, the braille blank, and nonspacing and enclosing marks removed
|
|
228
|
+
- letters the confusables data maps to `e`, `t` or `h` read as those letters, such as Cyrillic `е`, `т`, `н` and Greek
|
|
229
|
+
`Ε`, `Τ`, `Η`
|
|
230
|
+
- lower-cased, trimmed, and stripped of trailing spaces and dots
|
|
231
|
+
|
|
232
|
+
The name is refused when the skeleton ends in `.eth`. So `alice.eth`, `Alice.ETH`, `alice.eth.`, `alice.еth`
|
|
233
|
+
(Cyrillic `е`), `alice٠eth` (Arabic-Indic zero) and `ALICE.ETH` are refused, while `alice eth`,
|
|
234
|
+
`alice-eth`, `Kenneth`, `Seth`, `Ethan` and `J.Ethan` pass. `displayNameProblem(name)` runs the same check for a
|
|
235
|
+
sign-up form.
|
|
179
236
|
|
|
180
237
|
### The Swarm ID chat binding
|
|
181
238
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
239
|
+
[Swarm ID](https://github.com/Solar-Punk-Ltd/solarpunk-id) is a separate identity project. A Swarm ID account gives each
|
|
240
|
+
site its own chat key, and nothing in that key says which account it came from. A chat binding is the account's
|
|
241
|
+
statement, signed with the account's own key, that one chat address on one site belongs to it. This library does not
|
|
242
|
+
derive chat keys and does not import Swarm ID. It only checks the bindings, and reads them exactly as Swarm ID's
|
|
243
|
+
`verifyChatBinding` does.
|
|
244
|
+
|
|
245
|
+
`msg` is four lines joined by `\n`:
|
|
189
246
|
|
|
190
247
|
```text
|
|
191
248
|
Swarm ID chat binding v1
|
|
@@ -194,20 +251,22 @@ Chat address: 0x<addr>
|
|
|
194
251
|
Date: 2026-10-14T12:00:00.000Z
|
|
195
252
|
```
|
|
196
253
|
|
|
197
|
-
The site is a bare https origin as a browser reports it
|
|
198
|
-
only with
|
|
199
|
-
|
|
200
|
-
|
|
254
|
+
- The site is a bare https origin as a browser reports it. Plain http is accepted only for `localhost`, `127.0.0.1` and
|
|
255
|
+
`[::1]`, and only with the `allowLocalHttp` option.
|
|
256
|
+
- The chat address is `0x` and 40 lowercase hex characters.
|
|
257
|
+
- The date is ISO 8601 UTC with milliseconds.
|
|
258
|
+
- `sig` is an EIP-191 `personal_sign` signature over `msg`: `0x` and 130 lowercase hex characters of r, s and v, with v
|
|
259
|
+
27 or 28 and s in the lower half. Recovering it gives the account's address.
|
|
201
260
|
|
|
202
261
|
### The Swarm ID wallet link
|
|
203
262
|
|
|
204
|
-
Swarm ID never tells a site
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
joined by `\n`:
|
|
263
|
+
Swarm ID never tells a site which wallet secures an account. A person who wants readers to see their ENS name turns on
|
|
264
|
+
"Show my ENS name on sites" in Swarm ID. Swarm ID then gives the site a wallet link: a statement, signed by the wallet
|
|
265
|
+
and countersigned by the account's own key, that the two belong to the same person. Swarm ID hands it over as
|
|
266
|
+
`{ message, walletSignature, accountSignature }`, and a message carries it as `{ msg, wsig, asig }`. This library reads
|
|
267
|
+
it exactly as Swarm ID's `verifyWalletLink` does.
|
|
268
|
+
|
|
269
|
+
`msg` is five lines joined by `\n`:
|
|
211
270
|
|
|
212
271
|
```text
|
|
213
272
|
Swarm ID wallet link v1
|
|
@@ -217,87 +276,92 @@ Wallet: 0x<the wallet>
|
|
|
217
276
|
Date: 2026-10-15T12:00:00.000Z
|
|
218
277
|
```
|
|
219
278
|
|
|
220
|
-
The second line
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
`asig` are EIP-191 `personal_sign` over `msg` by the wallet and by the account, each `0x` and
|
|
224
|
-
|
|
279
|
+
- The second line must match exactly. A wallet shows the person only the text it signs, so this sentence tells them
|
|
280
|
+
what the signature does.
|
|
281
|
+
- Both addresses are `0x` and 40 lowercase hex characters, and the date is ISO 8601 UTC with milliseconds.
|
|
282
|
+
- `wsig` and `asig` are EIP-191 `personal_sign` signatures over `msg` by the wallet and by the account, each `0x` and
|
|
283
|
+
130 lowercase hex characters, with v 27 or 28 and s in the lower half.
|
|
225
284
|
|
|
226
|
-
A message carries a link only beside its binding, and the link counts toward the 3,072 byte
|
|
285
|
+
A message carries a link only beside its binding, and the link counts toward the 3,072 byte limit. The largest message
|
|
227
286
|
with a binding and a link is 2,376 bytes.
|
|
228
287
|
|
|
229
288
|
### Verifying
|
|
230
289
|
|
|
231
|
-
`verifyChatMessage(value, {
|
|
232
|
-
|
|
233
|
-
recovers an account, and its site is `
|
|
234
|
-
`{ account, site, date }` for a signed-in sender and `null` for a
|
|
235
|
-
the reason `binding
|
|
236
|
-
|
|
237
|
-
When the message also carries a wallet link, the link's text must be
|
|
238
|
-
account, its wallet signature must recover its wallet and its account signature its account.
|
|
239
|
-
`wallet`, the checked wallet address. A link that fails refuses the message with the reason
|
|
240
|
-
starting `wallet link:`. The check looks nothing up. A page that shows an ENS name
|
|
241
|
-
lookup, such as
|
|
242
|
-
record
|
|
243
|
-
|
|
244
|
-
`
|
|
245
|
-
for one site
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
account
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
290
|
+
`verifyChatMessage(value, { origins, allowLocalHttp? })` checks a message's shape and signature. When the message carries
|
|
291
|
+
a binding, it also checks that the binding's text is the four lines above, that its chat address is the message's
|
|
292
|
+
`addr`, that its signature recovers an account, and that its site is one of `origins`. It returns
|
|
293
|
+
`{ ok: true, message, identity }`. `identity` is `{ account, site, date }` for a signed-in sender and `null` for a
|
|
294
|
+
guest. A message whose binding fails is refused with the reason `binding`, and is never shown as a guest.
|
|
295
|
+
|
|
296
|
+
When the message also carries a wallet link, the link's text must be the five lines above, its account must be the
|
|
297
|
+
binding's account, its wallet signature must recover its wallet, and its account signature must recover its account.
|
|
298
|
+
`identity` then also has `wallet`, the checked wallet address. A link that fails refuses the message with the reason
|
|
299
|
+
`binding`, with a detail starting `wallet link:`. The check looks nothing up. A page that shows an ENS name passes
|
|
300
|
+
`identity.wallet` to its own ENS lookup, such as Swarm ID's `resolveVerifiedEnsName`, which requires the wallet's reverse
|
|
301
|
+
record and the name's forward record to agree.
|
|
302
|
+
|
|
303
|
+
`origins` is required. It lists the bare origins of the sites whose chat this is, such as `https://example.com`, so a
|
|
304
|
+
binding signed for one site never passes in a chat that site is not part of. A chat served from several pages, such as
|
|
305
|
+
a domain and a copy of the same page on Swarm under an ENS name, lists every one, and each page then shows the
|
|
306
|
+
messages sent from the others as signed in. An empty list, or none from a JavaScript caller, refuses every bound
|
|
307
|
+
message as `binding`. The server passes its configured sites, and a page passes its own origin and the others it was
|
|
308
|
+
told of.
|
|
309
|
+
|
|
310
|
+
**What `identity.account` proves.** It proves only that whoever controls that Swarm ID account bound this chat key for
|
|
311
|
+
this site, on the date the binding names. It is not a verified person and not an approved account. One person can hold
|
|
312
|
+
many accounts, an account takes a minute to make, and a lost or shared account key binds just as well. A page that
|
|
313
|
+
wants more, such as a badge for known people or a list of accounts allowed to post, keeps that list itself and compares
|
|
314
|
+
`identity.account` with it.
|
|
315
|
+
|
|
316
|
+
The server runs this check, through `parseChatMessage`, on every payload. The reader in this library checks no
|
|
317
|
+
signatures, because the server already did and a check costs main-thread time on every message. A page that shows
|
|
318
|
+
identities runs `verifyChatMessage` in a Web Worker on each event's `MessageData.signedMessage`. That is the feed
|
|
319
|
+
entry's `msg`, or the message as it was signed while it is pending. `verifyChatMessage` never throws, and it uses no DOM
|
|
320
|
+
and no Node API.
|
|
259
321
|
|
|
260
322
|
### The own feed
|
|
261
323
|
|
|
262
324
|
A sender may also keep a copy of each message it sends in a feed of its own, so anyone can compare that feed with the
|
|
263
|
-
chat. The copies live in a feed owned by the sender's chat address `addr`, under the topic `ownFeedTopic(chatTopic)
|
|
264
|
-
|
|
265
|
-
own feed is. A copy is the signed payload itself. The message says nothing about a copy, so whether one
|
|
266
|
-
from that feed.
|
|
325
|
+
chat. The copies live in a feed owned by the sender's chat address `addr`, under the topic `ownFeedTopic(chatTopic)`.
|
|
326
|
+
That topic is `swarm-chat-own-feed-v1:` followed by the chat's topic, turned into a feed topic with `Topic.fromString`
|
|
327
|
+
as the chat's own feed is. A copy is the signed payload itself. The message says nothing about a copy, so whether one
|
|
328
|
+
exists is read from that feed.
|
|
329
|
+
|
|
330
|
+
The library does not write the copy. A page that keeps copies passes its own inbox write function, a `GsocWrite`, as
|
|
331
|
+
`new SwarmChat(settings, { write })`. That function receives the signed payload, the same bytes on every resend, and
|
|
332
|
+
writes the copy beside the inbox write.
|
|
267
333
|
|
|
268
|
-
|
|
269
|
-
`new SwarmChat(settings, { write })`. It is handed the signed payload, the same bytes on every resend, and writes the
|
|
270
|
-
copy beside the inbox write.
|
|
334
|
+
### Exports
|
|
271
335
|
|
|
272
336
|
| Export | Use |
|
|
273
337
|
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
274
|
-
| `parseChatMessage(bytes, {
|
|
275
|
-
| `verifyChatMessage(value, {
|
|
338
|
+
| `parseChatMessage(bytes, { origins })` | The server's check of a raw payload: byte limit, UTF-8, JSON, then `verifyChatMessage`. Never throws. |
|
|
339
|
+
| `verifyChatMessage(value, { origins })` | Shape, signature and binding of a parsed object, with the sender's identity. Never throws. |
|
|
276
340
|
| `displayNameProblem(name)` | `null` for a name a message may carry, else `'length'` (not 1 to 20 characters), `'invisible'` (a format or control character) or `'ens-like'` (reads as ending in `.eth`). |
|
|
277
341
|
| `createChatMessage(key, draft)` | Builds and signs, throwing a `ChatMessageError` for anything the check would refuse. `via` is required. |
|
|
278
342
|
| `MessageType`, `MessageVia` | The values of `type` and `via`. |
|
|
279
|
-
| `checkChatBinding(binding, addr, options?)`, `chatBindingSchema` | The binding check on its own and its shape. Its `
|
|
343
|
+
| `checkChatBinding(binding, addr, options?)`, `chatBindingSchema` | The binding check on its own, and its shape. Its `origins` is optional, for a sender checking its own binding, and left out accepts any site. |
|
|
280
344
|
| `checkWalletLink(link, account)`, `walletLinkSchema` | The wallet link check on its own, for the binding's account, and its shape. |
|
|
281
345
|
| `ownFeedTopic(chatTopic)`, `OWN_FEED_TOPIC_PREFIX` | The topic of a sender's own feed of copies. |
|
|
282
|
-
| `chatMessageSchema`, `hasValidSignature`, `signedBytes`, `encodeChatMessage` | The parts.
|
|
346
|
+
| `chatMessageSchema`, `hasValidSignature`, `signedBytes`, `encodeChatMessage` | The parts of the message check. |
|
|
283
347
|
| `feedEntrySchema`, `parseFeedEntry` | The feed entry, `{v, seq, at, msg, history}`. |
|
|
284
348
|
| `historyFileSchema`, `parseHistoryFile` | The history file, `{v, topic, fromSeq, toSeq, messages, prev}`. |
|
|
285
349
|
|
|
286
|
-
The feed entry and the history file carry `v` 9 like the message. A feed entry
|
|
287
|
-
the byte
|
|
350
|
+
The feed entry and the history file carry `v` 9, like the message. A feed entry is at most 4,096 bytes, enough for a
|
|
351
|
+
message at the byte limit inside the largest entry around it.
|
|
288
352
|
|
|
289
353
|
## Slot notes, for servers and tools
|
|
290
354
|
|
|
291
|
-
A note tells readers which feed entries exist, so they never ask for one that does not. Time slot `s` covers
|
|
292
|
-
`[s * slotMs, (s + 1) * slotMs)` in Unix milliseconds, `slotMs`
|
|
293
|
-
|
|
294
|
-
`<chat topic>/note/<slotMs>/<s>`, and is read through `GET /chunks/<address>`. Its payload is
|
|
295
|
-
`{"v":1,"newest":<the highest feed index written>,"writtenAt":<the server's clock in ms>}`, `newest` -1 for a chat
|
|
296
|
-
nothing written yet
|
|
355
|
+
A slot note tells readers which feed entries exist, so they never ask for one that does not. Time slot `s` covers
|
|
356
|
+
`[s * slotMs, (s + 1) * slotMs)` in Unix milliseconds, with `slotMs` 2,000 by default. The note of slot `s` is a single
|
|
357
|
+
owner chunk signed by the chat feed's key. Its identifier is keccak256 of the UTF-8 text
|
|
358
|
+
`<chat topic>/note/<slotMs>/<s>`, and it is read through `GET /chunks/<address>`. Its payload is
|
|
359
|
+
`{"v":1,"newest":<the highest feed index written>,"writtenAt":<the server's clock in ms>}`, with `newest` -1 for a chat
|
|
360
|
+
with nothing written yet. A payload that does not match this exact shape counts as no note.
|
|
297
361
|
|
|
298
|
-
The server writes a slot's note once the slot is over, when an entry landed since its last note
|
|
299
|
-
|
|
300
|
-
|
|
362
|
+
The server writes a slot's note once the slot is over, when an entry landed since its last note, or when a heartbeat
|
|
363
|
+
has passed since that note. An entry's own write has finished before any note names it. A note write that fails is not
|
|
364
|
+
retried at its address, because the next slot's note carries the same information.
|
|
301
365
|
|
|
302
366
|
| Export | Use |
|
|
303
367
|
| ---------------------------------------------------------------- | ------------------------------------------------------ |
|
|
@@ -307,105 +371,27 @@ that fails is not retried at its address: the next slot's note carries the same
|
|
|
307
371
|
| `DEFAULT_NOTE_SLOT_MS`, `DEFAULT_NOTE_HEARTBEAT_MS` | The defaults both sides share. |
|
|
308
372
|
|
|
309
373
|
**The reader's clock.** A reader schedules note reads by its own clock. Every note's `writtenAt` and every entry's `at`
|
|
310
|
-
was stamped before the reader received it, so the reader
|
|
311
|
-
notes are found
|
|
312
|
-
no note is found for longer than a heartbeat, it waits out the whole bound from then on,
|
|
313
|
-
clock that runs behind sees every stamp as old as it expects, so its messages arrive as late as
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
- The message is version 9, its `v` and the first element of its signed bytes both `9`, and so is every feed entry and
|
|
320
|
-
history file. A message, entry or history file of version 8 fails its shape check, whether or not it carries `own`,
|
|
321
|
-
so a reader and a server on 9.0 never read one that 8.x wrote, and the break shows as a version rather than as a
|
|
322
|
-
missing field.
|
|
323
|
-
- The message no longer carries `own`. It is gone from the shape, the signed bytes and the payload, and `infra.own` and
|
|
324
|
-
the draft's `own` are gone with it. Whether a sender keeps a copy is read from its own feed. A message that carries
|
|
325
|
-
`own`, which is every message 8.0 and 8.1 signed, is refused as `shape`, and so is a feed entry or history row that
|
|
326
|
-
holds one.
|
|
327
|
-
- A name with a Unicode format or control character, such as the right-to-left override U+202E or a zero width space,
|
|
328
|
-
is refused as `shape`, since it can make one name look like another. The zero width non-joiner U+200C and the zero
|
|
329
|
-
width joiner U+200D are allowed, because Persian and Indic names and joined emoji such as a family need them. The
|
|
330
|
-
blank letters and marks are refused with them, since they show as nothing and have no honest use in a name: the
|
|
331
|
-
braille blank U+2800, the Hangul fillers U+115F, U+1160, U+3164 and U+FFA0, the combining grapheme joiner U+034F and
|
|
332
|
-
the Khmer inherent vowels U+17B4 and U+17B5. Variation selectors such as U+FE0F stay allowed, since emoji need them.
|
|
333
|
-
- A name that reads as ending in `.eth` is refused as `shape`, since a page shows a verified ENS name where the
|
|
334
|
-
chosen name would be and a chosen one could pass for it. The check reads a skeleton of the name, the name as a
|
|
335
|
-
reader sees it: NFKD normalised, so fullwidth and other compatibility forms fold and an accented letter loses its
|
|
336
|
-
accent, with every character Unicode's confusables data maps to the full stop read as `.` (the Arabic-Indic zeros,
|
|
337
|
-
the Syriac full stops and the musical augmentation dot among them) and the ideographic full stop U+3002 too, since
|
|
338
|
-
ENS reads it as one, without the Unicode default ignorable code points, the braille blank or any nonspacing or
|
|
339
|
-
enclosing mark, with the letters the confusables data maps to `e`, `t` or `h` read as those letters (Cyrillic `е`,
|
|
340
|
-
`т`, `н` and `Т`, Armenian `հ` and Greek `Ε`, `Τ`, `Η` among them), lower-cased, trimmed, and stripped of trailing
|
|
341
|
-
spaces and dots. The name is refused when that skeleton ends in `.eth`. So `vitalik.eth`, `Vitalik.ETH`,
|
|
342
|
-
`vitalik.eth.`, `vitalik.еth` (Cyrillic `е`), `vitalik٠eth` (Arabic-Indic zero), `vitalik.eth` followed by U+FE0F,
|
|
343
|
-
`J.Eth` and `VITALIK.ETH` are all refused, while `vitalik eth`, `vitalik-eth`, `vitalik_eth`, `vitalik·eth`,
|
|
344
|
-
`Kenneth`, `Seth`, `Ethan` and `J.Ethan` pass.
|
|
345
|
-
- `SwarmChat` takes `user.walletLink`, `createChatMessage` takes `wl`, and `checkWalletLink` checks a link on its own.
|
|
346
|
-
- A server on 8.x refuses every version 9 message as `shape`. Update the server before any page sends one.
|
|
347
|
-
|
|
348
|
-
## Changes in 8.1
|
|
349
|
-
|
|
350
|
-
- `infra.own` signs every message with `own: true`, for a page whose write keeps a copy in the sender's own feed. Left
|
|
351
|
-
out, messages say `own: false` as in 8.0.
|
|
352
|
-
|
|
353
|
-
## Changes in 8.0
|
|
354
|
-
|
|
355
|
-
- The v8 message adds `via`, `own` and, for a signed-in sender, `idb`, its Swarm ID chat binding, all signed. A v7
|
|
356
|
-
message, entry or history file fails its shape check, and nothing reads both.
|
|
357
|
-
- `verifyChatMessage` replaces `checkChatMessage` and also checks the binding, returning the sender's `identity`.
|
|
358
|
-
Both it and `parseChatMessage` require `{ origin }`, the site whose chat this is. A failed binding is refused with the
|
|
359
|
-
new reason `binding`.
|
|
360
|
-
- `MAX_MESSAGE_BYTES` is 3,072, so a long text fits beside a binding.
|
|
361
|
-
- `createChatMessage` requires `via` and takes `own` and `idb`. `SwarmChat` requires `infra.via` and takes
|
|
362
|
-
`user.binding`.
|
|
363
|
-
- `ownFeedTopic` names the feed where a sender keeps its copies.
|
|
364
|
-
- `MessageData.signedMessage` carries the whole signed message, for a page to verify. `chatTopic`, `signature` and
|
|
365
|
-
`sentAt` are gone from `MessageData`, since `signedMessage` carries them as `topic`, `sig` and `ts`.
|
|
366
|
-
- The binding is recovered through `@ethersphere/core-sdk`, which bee-js 13 already installs, now a dependency here.
|
|
367
|
-
|
|
368
|
-
## Changes in 7.3
|
|
369
|
-
|
|
370
|
-
- A reader asks Bee for an address that came back missing no more than three times a minute, a feed slot and a note
|
|
371
|
-
alike. Polling the next slot every second, as a reader that found no note did, kept that slot on Bee's skip list,
|
|
372
|
-
so the first messages in a quiet room arrived 40 to 55 s late. A slot a note names is still read at once.
|
|
373
|
-
|
|
374
|
-
## Changes in 7.2
|
|
375
|
-
|
|
376
|
-
- Readers follow a chat by its slot notes and no longer ask for the next feed slot before it exists, so a new message
|
|
377
|
-
arrives in seconds rather than about forty. Opening needs no head lookup either. A server without notes is followed
|
|
378
|
-
as 7.1 followed it.
|
|
379
|
-
- The note protocol is exported from the root and from `./message` for the server. New settings `infra.noteSlotMs`
|
|
380
|
-
and `infra.noteHeartbeatMs`.
|
|
381
|
-
|
|
382
|
-
## Changes from 6.x
|
|
383
|
-
|
|
384
|
-
- Messages are signed over every field and verified by the server. 6.x signed four fields with a double prefix, and its
|
|
385
|
-
check passed forged messages.
|
|
386
|
-
- One inbox write per message. The write to the sender's own feed is gone, and so are `userTopic`, `additionalProps`,
|
|
387
|
-
and the `enveloped` setting.
|
|
388
|
-
- A bad message or history row is skipped, never a freeze. A slot no peer gives out is stepped past and read again.
|
|
389
|
-
- No signature check per message on the read path, which cost 6.x about 36 ms of main thread per message on every open.
|
|
390
|
-
- `sendMessage` takes three arguments, and a reaction resolves to `null`. `retrySendMessage` covers what
|
|
391
|
-
`retryBroadcastUserMessage` did.
|
|
392
|
-
- `stop` keeps listeners. New events: `STATUS`, `MESSAGE_SKIPPED` and `ERROR`. `CRITICAL_ERROR` means three failed
|
|
393
|
-
openings in a row, and opening keeps trying.
|
|
394
|
-
- `MessageData.index` is the feed index, `timestamp` is the server's receive time, and `sentAt` is new.
|
|
395
|
-
- The GSOC mining script moved to the server repository, whose operator mines the inbox key.
|
|
396
|
-
- The package is ESM first with a CommonJS build. The UMD bundle and the Node polyfills are gone.
|
|
374
|
+
was stamped by the server before the reader received it, so the reader can be at most `received - stamped` ahead of
|
|
375
|
+
the server. While notes are found, the reader trusts its own clock and moves reads earlier only when that bound proves
|
|
376
|
+
its clock runs behind. When no note is found for longer than a heartbeat, it waits out the whole bound from then on,
|
|
377
|
+
which is never early. A clock that runs behind sees every stamp as old as it expects, so its messages arrive as late as
|
|
378
|
+
it runs behind.
|
|
379
|
+
|
|
380
|
+
## Changes
|
|
381
|
+
|
|
382
|
+
Each release's changes are in [CHANGELOG.md](CHANGELOG.md).
|
|
397
383
|
|
|
398
384
|
## Development
|
|
399
385
|
|
|
400
|
-
Node 24 and pnpm 12,
|
|
386
|
+
Node 24 and pnpm 12, pinned by `.nvmrc` and `packageManager`.
|
|
401
387
|
|
|
402
388
|
```bash
|
|
403
389
|
pnpm install
|
|
404
390
|
pnpm test
|
|
405
391
|
```
|
|
406
392
|
|
|
407
|
-
`lint` (oxlint, type-aware), `format
|
|
408
|
-
|
|
393
|
+
CI runs `lint` (oxlint, type-aware), `format:check` (oxfmt), `typecheck`, `test` (vitest) and `build` (vite, with types
|
|
394
|
+
from `tsc`). `pnpm format` formats the code, and `pnpm pack` builds before it packs.
|
|
409
395
|
|
|
410
396
|
## License
|
|
411
397
|
|