Skip to content

Commit 848653e

Browse files
committed
docs: ready for release
1 parent a7d7811 commit 848653e

File tree

6 files changed

+271
-15
lines changed

6 files changed

+271
-15
lines changed

packages/loro-adaptors/README.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# loro-adaptors
22

3-
Shared adaptors that bridge the Loro protocol to `loro-crdt` documents and the ephemeral store.
3+
Adaptors that bridge the Loro protocol to `loro-crdt` documents and the ephemeral store. Includes an end‑to‑end encrypted adaptor for %ELO.
44

55
## Install
66

@@ -14,18 +14,19 @@ The websocket client (`loro-websocket`) speaks the binary wire protocol. These a
1414

1515
- `LoroAdaptor`: wraps a `LoroDoc` and streams local updates to the connection; applies remote updates on receipt
1616
- `LoroEphemeralAdaptor`: wraps an `EphemeralStore` for transient presence/cursor data
17+
- `EloLoroAdaptor`: wraps a `LoroDoc` and packages updates into %ELO containers with AES‑GCM; decrypts inbound containers and imports plaintext.
1718

1819
## Usage
1920

2021
```ts
21-
import { LoroWebsocketClient } from "loro-websocket/client";
22-
import { LoroAdaptor, LoroEphemeralAdaptor } from "loro-adaptors";
22+
import { LoroWebsocketClient } from "loro-websocket";
23+
import { LoroAdaptor, LoroEphemeralAdaptor, EloLoroAdaptor } from "loro-adaptors";
2324
import { LoroDoc, EphemeralStore } from "loro-crdt";
2425

2526
const client = new LoroWebsocketClient({ url: "ws://localhost:8787" });
2627
await client.waitConnected();
2728

28-
// Document state
29+
// Plain Loro document
2930
const doc = new LoroDoc();
3031
const docAdaptor = new LoroAdaptor(doc, { peerId: 1 });
3132
const roomDoc = await client.join({ roomId: "demo", crdtAdaptor: docAdaptor });
@@ -35,20 +36,33 @@ const eph = new EphemeralStore();
3536
const ephAdaptor = new LoroEphemeralAdaptor(eph);
3637
const roomEph = await client.join({ roomId: "demo", crdtAdaptor: ephAdaptor });
3738

38-
// Make edits
39+
// %ELO (end‑to‑end encrypted Loro)
40+
const key = new Uint8Array(32);
41+
const elo = new EloLoroAdaptor({ getPrivateKey: async () => ({ keyId: "k1", key }) });
42+
const secure = await client.join({ roomId: "secure-room", crdtAdaptor: elo });
43+
44+
// Edits
3945
doc.getText("content").insert(0, "hello");
4046
doc.commit();
4147

4248
// Cleanup
4349
await roomEph.destroy();
4450
await roomDoc.destroy();
51+
await secure.destroy();
4552
```
4653

4754
## API
4855

4956
- `new LoroAdaptor(doc?: LoroDoc, config?: { peerId?, recordTimestamp?, changeMergeInterval?, onUpdateError? })`
5057
- `new LoroEphemeralAdaptor(store?: EphemeralStore, config?: { timeout?, onUpdateError? })`
51-
- Helpers: `createLoroAdaptor`, `createLoroAdaptorFromDoc`, `createLoroEphemeralAdaptor`, `createLoroEphemeralAdaptorFromStore`
58+
- `new EloLoroAdaptor(docOrConfig: LoroDoc | { getPrivateKey, ivFactory?, onDecryptError?, onUpdateError? })`
59+
- `getPrivateKey: (keyId?) => Promise<{ keyId: string, key: CryptoKey | Uint8Array }>`
60+
- Optional `ivFactory()` for testing (12‑byte IV)
61+
- Helpers: `createLoroAdaptor`, `createLoroAdaptorFromDoc`, `createLoroEphemeralAdaptor`, `createLoroEphemeralAdaptorFromStore`, `createEloLoroAdaptor`
62+
63+
Notes (E2EE)
64+
- IV must be 12 bytes and unique per key. The `ivFactory` is for tests only.
65+
- The server never decrypts; it indexes plaintext headers to select backfill.
5266

5367
## Development
5468

packages/loro-adaptors/package.json

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "loro-adaptors",
3-
"version": "1.0.0",
4-
"description": "Shared CRDT adaptors for Loro protocol",
3+
"version": "0.1.0",
4+
"author": "Loro Team",
55
"main": "dist/index.js",
66
"type": "module",
77
"scripts": {
@@ -26,19 +26,30 @@
2626
"keywords": [
2727
"loro",
2828
"crdt",
29-
"adaptors",
29+
"adaptor",
3030
"websocket",
31-
"collaborative-editing"
31+
"collaborative-editing",
32+
"presence",
33+
"ephemeral",
34+
"e2ee"
3235
],
3336
"repository": {
3437
"type": "git",
35-
"url": "https://github.com/loro-dev/loro-protocol",
38+
"url": "https://github.com/loro-dev/protocol",
3639
"directory": "packages/loro-adaptors"
3740
},
41+
"bugs": {
42+
"url": "https://github.com/loro-dev/protocol/issues"
43+
},
44+
"homepage": "https://github.com/loro-dev/protocol/tree/main/packages/loro-adaptors",
3845
"license": "MIT",
3946
"engines": {
4047
"node": ">=18.0.0"
4148
},
49+
"sideEffects": false,
50+
"publishConfig": {
51+
"access": "public"
52+
},
4253
"exports": {
4354
".": {
4455
"types": "./dist/index.d.ts",

packages/loro-protocol/README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# loro-protocol
2+
3+
Binary wire protocol for Loro CRDTs (TypeScript). Provides compact encoders/decoders for all protocol messages, bytes utilities, and helpers for the %ELO end-to-end encrypted flow (AES‑GCM with 12‑byte IV and exact-header AAD).
4+
5+
## Install
6+
7+
```bash
8+
pnpm add loro-protocol
9+
```
10+
11+
## Features
12+
13+
- Message encoding/decoding for Join/DocUpdate/Fragments/Errors/Leave
14+
- 256 KiB message size guard with fragmentation support at higher layers
15+
- Bytes helpers (`BytesWriter`, `BytesReader`) with varUint/varBytes/varString
16+
- %ELO helpers: container codec, record header parsing, AES‑GCM encrypt/decrypt
17+
18+
See `protocol.md` and `protocol-e2ee.md` at the repo root for the ground‑truth spec.
19+
20+
## Usage
21+
22+
Encode and decode messages:
23+
24+
```ts
25+
import { encode, decode, CrdtType, MessageType } from "loro-protocol";
26+
27+
const join = encode({
28+
type: MessageType.JoinRequest,
29+
crdt: CrdtType.Loro,
30+
roomId: new TextEncoder().encode("room-1"),
31+
auth: new Uint8Array(),
32+
version: new Uint8Array(),
33+
});
34+
35+
const msg = decode(join);
36+
console.log(msg.type); // 0x00 JoinRequest
37+
```
38+
39+
%ELO encrypted records (AES‑GCM):
40+
41+
```ts
42+
import {
43+
EloRecordKind,
44+
encryptSnapshot,
45+
encryptDeltaSpan,
46+
decryptEloRecord,
47+
encodeEloContainer,
48+
decodeEloContainer,
49+
parseEloRecordHeader,
50+
} from "loro-protocol";
51+
52+
// Encrypt a snapshot
53+
const key = crypto.getRandomValues(new Uint8Array(32));
54+
const plaintext = new Uint8Array([1, 2, 3]);
55+
const { record } = await encryptSnapshot(plaintext, { vv: [], keyId: "k1" }, key);
56+
const container = encodeEloContainer([record]);
57+
58+
// Later, parse and decrypt
59+
const [rec] = decodeEloContainer(container);
60+
const parsed = parseEloRecordHeader(rec);
61+
const out = await decryptEloRecord(rec, async () => key);
62+
console.log(parsed.kind === EloRecordKind.Snapshot, out.plaintext);
63+
```
64+
65+
Notes
66+
- The server can parse %ELO headers to index/backfill but never decrypts `ct`.
67+
- IV must be exactly 12 bytes and unique per key; AAD is the exact encoded header.
68+
69+
## API Surface
70+
71+
- Encoding/decoding: `encode(msg)`, `decode(buf)`, `tryDecode(buf)`
72+
- Bytes: `BytesWriter`, `BytesReader`
73+
- %ELO: `encodeEloContainer`, `decodeEloContainer`, `parseEloRecordHeader`, `encryptSnapshot`, `encryptDeltaSpan`, `decryptEloRecord`
74+
75+
## Node/Web Compatibility
76+
77+
%ELO crypto uses Web Crypto (`globalThis.crypto.subtle`). Node 18+ provides it via `globalThis.crypto`.
78+
79+
## License
80+
81+
MIT
82+

packages/loro-protocol/package.json

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,45 @@
11
{
22
"name": "loro-protocol",
3-
"version": "0.0.0",
3+
"version": "0.1.0",
44
"private": false,
5-
"description": "Loro protocol TypeScript library",
5+
"description": "Compact binary wire protocol for collaborative CRDTs (Loro/Yjs), with encoders/decoders and bytes utilities",
6+
"author": "Loro Team",
67
"license": "MIT",
78
"type": "module",
89
"main": "dist/index.cjs",
910
"module": "dist/index.js",
1011
"types": "dist/index.d.ts",
12+
"keywords": [
13+
"loro",
14+
"crdt",
15+
"protocol",
16+
"encoding",
17+
"decoding",
18+
"binary",
19+
"websocket",
20+
"fragmentation",
21+
"e2ee",
22+
"aes-gcm",
23+
"webcrypto",
24+
"yjs",
25+
"awareness"
26+
],
27+
"repository": {
28+
"type": "git",
29+
"url": "https://github.com/loro-dev/protocol",
30+
"directory": "packages/loro-protocol"
31+
},
32+
"bugs": {
33+
"url": "https://github.com/loro-dev/protocol/issues"
34+
},
35+
"homepage": "https://github.com/loro-dev/protocol/tree/main/packages/loro-protocol",
36+
"sideEffects": false,
37+
"publishConfig": {
38+
"access": "public"
39+
},
40+
"engines": {
41+
"node": ">=18.0.0"
42+
},
1143
"exports": {
1244
".": {
1345
"types": "./dist/index.d.ts",

packages/loro-websocket/README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# loro-websocket
2+
3+
WebSocket client and a minimal SimpleServer for syncing Loro CRDTs. Supports message fragmentation/reassembly (≤256 KiB), connection‑scoped keepalive ("ping"/"pong"), permission hooks, optional persistence hooks, and routing of %ELO end‑to‑end encrypted updates.
4+
5+
## Install
6+
7+
```bash
8+
pnpm add loro-websocket loro-adaptors loro-protocol
9+
# plus peer dep in your app
10+
pnpm add loro-crdt
11+
```
12+
13+
## Client
14+
15+
```ts
16+
// In Node, provide a WebSocket implementation
17+
import { WebSocket } from "ws";
18+
(globalThis as any).WebSocket = WebSocket as unknown as typeof globalThis.WebSocket;
19+
20+
import { LoroWebsocketClient } from "loro-websocket";
21+
import { createLoroAdaptor } from "loro-adaptors";
22+
23+
const client = new LoroWebsocketClient({ url: "ws://localhost:8787" });
24+
await client.waitConnected();
25+
26+
const adaptor = createLoroAdaptor({ peerId: 1 });
27+
const room = await client.join({ roomId: "demo", crdtAdaptor: adaptor });
28+
29+
// Edit
30+
const text = adaptor.getDoc().getText("content");
31+
text.insert(0, "Hello, Loro!");
32+
adaptor.getDoc().commit();
33+
34+
await room.destroy();
35+
```
36+
37+
%ELO (end‑to‑end encrypted Loro) using `EloLoroAdaptor`:
38+
39+
```ts
40+
import { LoroWebsocketClient } from "loro-websocket";
41+
import { EloLoroAdaptor } from "loro-adaptors";
42+
43+
const key = new Uint8Array(32); key[0] = 1;
44+
const client = new LoroWebsocketClient({ url: "ws://localhost:8787" });
45+
await client.waitConnected();
46+
47+
const adaptor = new EloLoroAdaptor({ getPrivateKey: async () => ({ keyId: "k1", key }) });
48+
const room = await client.join({ roomId: "secure-room", crdtAdaptor: adaptor });
49+
50+
adaptor.getDoc().getText("t").insert(0, "secret");
51+
adaptor.getDoc().commit();
52+
```
53+
54+
## SimpleServer
55+
56+
```ts
57+
import { SimpleServer } from "loro-websocket/server";
58+
59+
const server = new SimpleServer({
60+
port: 8787,
61+
authenticate: async (_roomId, _crdt, auth) => {
62+
// return "read" | "write" | null
63+
return new TextDecoder().decode(auth) === "readonly" ? "read" : "write";
64+
},
65+
onLoadDocument: async (_roomId, _crdt) => null,
66+
onSaveDocument: async (_roomId, _crdt, _data) => {},
67+
saveInterval: 60_000,
68+
});
69+
await server.start();
70+
```
71+
72+
## Behavior
73+
74+
- Fragmentation: oversize `DocUpdate` payloads are split into `DocUpdateFragmentHeader` + `DocUpdateFragment` frames and reassembled.
75+
- Keepalive: text frames "ping" and "pong" are connection‑scoped and bypass the envelope.
76+
- Permissions: pass an `authenticate` hook to return `"read" | "write" | null` per join.
77+
- Persistence: optionally `onLoadDocument`/`onSaveDocument` snapshots for `%LOR` documents.
78+
- %ELO: server indexes plaintext headers to backfill encrypted deltas; ciphertext is not decrypted.
79+
80+
## Node/Web Compatibility
81+
82+
- Client works in browsers (native WebSocket) and Node (supply `globalThis.WebSocket`).
83+
- %ELO crypto relies on Web Crypto via `loro-protocol` helpers (Node 18+ provides it).
84+
85+
## License
86+
87+
MIT
88+

packages/loro-websocket/package.json

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,42 @@
11
{
22
"name": "loro-websocket",
3-
"version": "0.0.0",
3+
"version": "0.1.0",
44
"private": false,
5-
"description": "WebSocket client and server for Loro",
5+
"description": "WebSocket client and SimpleServer for syncing CRDTs base on loro-protocol",
6+
"author": "Loro Team",
67
"license": "MIT",
78
"type": "module",
89
"main": "dist/client/index.cjs",
910
"module": "dist/client/index.js",
1011
"types": "dist/client/index.d.ts",
12+
"keywords": [
13+
"loro",
14+
"crdt",
15+
"websocket",
16+
"client",
17+
"server",
18+
"sync",
19+
"realtime",
20+
"fragmentation",
21+
"keepalive",
22+
"e2ee"
23+
],
24+
"repository": {
25+
"type": "git",
26+
"url": "https://github.com/loro-dev/protocol",
27+
"directory": "packages/loro-websocket"
28+
},
29+
"bugs": {
30+
"url": "https://github.com/loro-dev/protocol/issues"
31+
},
32+
"homepage": "https://github.com/loro-dev/protocol/tree/main/packages/loro-websocket",
33+
"sideEffects": false,
34+
"publishConfig": {
35+
"access": "public"
36+
},
37+
"engines": {
38+
"node": ">=18.0.0"
39+
},
1140
"exports": {
1241
".": {
1342
"types": "./dist/client/index.d.ts",

0 commit comments

Comments
 (0)