Skip to content

Commit 4fc2bbd

Browse files
committed
docs: add docs about redact history
1 parent c25facd commit 4fc2bbd

2 files changed

Lines changed: 205 additions & 0 deletions

File tree

pages/docs/tutorial/loro_doc.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,68 @@ const sinceFrontiers = doc.shallowSinceFrontiers();
248248

249249
Note: A shallow document only contains history after a certain version point. Operations before the shallow start point are not included, but the document remains fully functional for collaboration.
250250

251+
### Redacting Sensitive Content
252+
253+
Loro allows you to redact specific segments of document history while preserving the rest. This is particularly useful when:
254+
255+
1. A user accidentally pastes sensitive information (like passwords or API keys) into the document
256+
2. You need to remove just the sensitive part of the history while keeping older and newer edits intact
257+
3. You want to share document history with sensitive segments sanitized
258+
259+
Here's how to use the redaction functionality:
260+
261+
```typescript
262+
const doc = new LoroDoc();
263+
doc.setPeerId("1");
264+
265+
// Create some content to be redacted
266+
const text = doc.getText("text");
267+
text.insert(0, "Sensitive information");
268+
doc.commit();
269+
270+
const map = doc.getMap("map");
271+
map.set("password", "secret123");
272+
map.set("public", "public information");
273+
doc.commit();
274+
275+
// Export JSON updates
276+
const jsonUpdates = doc.exportJsonUpdates();
277+
278+
// Define version range to redact (redact the text content)
279+
const versionRange = {
280+
"1": [0, 21] // Redact the "Sensitive information"
281+
};
282+
283+
// Apply redaction
284+
const redactedJson = redactJsonUpdates(jsonUpdates, versionRange);
285+
286+
// Create a new document with redacted content
287+
const redactedDoc = new LoroDoc();
288+
redactedDoc.importJsonUpdates(redactedJson);
289+
290+
// The text content is now redacted with replacement characters
291+
console.log(redactedDoc.getText("text").toString());
292+
// Outputs: "���������������������"
293+
294+
// Map operations after the redacted range remain intact
295+
console.log(redactedDoc.getMap("map").get("password")); // "secret123"
296+
console.log(redactedDoc.getMap("map").get("public")); // "public information"
297+
```
298+
299+
Redaction applies these rules to preserve document structure while removing sensitive content:
300+
- Preserves delete and move operations
301+
- Replaces text insertion content with Unicode replacement characters '�'
302+
- Substitutes list and map insert values with null
303+
- Maintains structure of nested containers
304+
- Replaces text mark values with null
305+
- Preserves map keys and text annotation keys
306+
307+
Note that redaction doesn't remove the operations completely - it just replaces the sensitive content with placeholders. If you need to completely remove portions of history, see the section on shallow snapshots in the [Tips](./tips.md) section.
308+
309+
#### Important: Synchronization Considerations
310+
311+
Both redaction and shallow snapshots maintain future synchronization consistency, but your application is responsible for ensuring all peers get the sanitized version. Otherwise, old instances of the document with sensitive information will still exist on other peers.
312+
251313
## Event Subscription
252314

253315
Subscribe to changes in the document:

pages/docs/tutorial/tips.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,146 @@ doc.import(docB.export({ mode: "update" }));
7676
2. If it's impossible to initialize all child containers when the map container is initialized, prefer initializing them at the root level rather than as nested containers.
7777
- You can use `doc.getMap("user." + userId)` instead of `doc.getMap("user").getOrCreateContainer(userId, new LoroMap())` to avoid this problem.
7878
</details>
79+
80+
---
81+
82+
##### Use redaction to safely share document history
83+
84+
There are times when users might accidentally paste sensitive information (like API keys, passwords, or personal data) into a collaborative document. When this happens, you need a way to remove just that sensitive content from the document history without compromising the rest of the document's integrity.
85+
86+
<details>
87+
<summary>How to safely redact sensitive content</summary>
88+
89+
Loro provides a `redactJsonUpdates` function that allows you to selectively redact operations within specific version ranges.
90+
91+
For example, if a user accidentally pastes a password or API key into a document:
92+
93+
```typescript
94+
const doc = new LoroDoc();
95+
doc.setPeerId("1");
96+
97+
// Create some content to be redacted
98+
const text = doc.getText("text");
99+
text.insert(0, "Sensitive information");
100+
doc.commit();
101+
102+
const map = doc.getMap("map");
103+
map.set("password", "secret123");
104+
map.set("public", "public information");
105+
doc.commit();
106+
107+
// Export JSON updates
108+
const jsonUpdates = doc.exportJsonUpdates();
109+
110+
// Define version range to redact (redact the text content)
111+
const versionRange = {
112+
"1": [0, 21] // Redact the "Sensitive information"
113+
};
114+
115+
// Apply redaction
116+
const redactedJson = redactJsonUpdates(jsonUpdates, versionRange);
117+
118+
// Create a new document with redacted content
119+
const redactedDoc = new LoroDoc();
120+
redactedDoc.importJsonUpdates(redactedJson);
121+
122+
// The text content is now redacted with replacement characters
123+
console.log(redactedDoc.getText("text").toString());
124+
// Outputs: "���������������������"
125+
126+
// You can also redact specific map entries
127+
const versionRange2 = {
128+
"1": [21, 22] // Redact the "secret123" password
129+
};
130+
131+
const redactedJson2 = redactJsonUpdates(jsonUpdates, versionRange2);
132+
const redactedDoc2 = new LoroDoc();
133+
redactedDoc2.importJsonUpdates(redactedJson2);
134+
135+
console.log(redactedDoc2.getMap("map").get("password")); // null
136+
console.log(redactedDoc2.getMap("map").get("public")); // "public information"
137+
```
138+
139+
This approach is safer than manually editing document content because:
140+
141+
1. It maintains document structure and CRDT consistency
142+
2. It keeps key metadata like operation IDs and dependencies intact
143+
3. It allows concurrent editing to continue working after redaction
144+
4. It selectively redacts only specific operations, not the entire document
145+
146+
The redaction process follows these rules:
147+
- Preserves delete, tree move, and list move operations
148+
- Replaces text insertion content with Unicode replacement characters '�'
149+
- Substitutes list and map insert values with null
150+
- Maintains structure of child containers
151+
- Replaces text mark values with null
152+
- Preserves map keys and text annotation keys
153+
154+
**Important**: Your application needs to ensure that all peers receive the redacted version, otherwise the original document with sensitive information will still exist on other peers.
155+
156+
</details>
157+
158+
---
159+
160+
##### Use shallow snapshots to completely remove old history
161+
162+
When you need to completely remove ALL history older than a certain version point, shallow snapshots provide the solution.
163+
164+
<details>
165+
<summary>How to remove old history with shallow snapshots</summary>
166+
167+
Shallow snapshots create a new document that preserves the current state but completely eliminates all history before a specified point, similar to Git's shallow clone functionality.
168+
169+
```typescript
170+
const doc = new LoroDoc();
171+
doc.setPeerId("1");
172+
173+
// Old history - will be completely removed
174+
const text = doc.getText("text");
175+
text.insert(0, "This document has a long history with many edits");
176+
doc.commit();
177+
text.insert(0, "Including some potentially sensitive information. ");
178+
doc.commit();
179+
180+
// More recent history - will be preserved
181+
text.delete(11, 55); // Remove the middle part
182+
text.insert(11, "with sanitized history");
183+
doc.commit();
184+
185+
// Create a sanitized version that removes ALL history before current point
186+
const sanitizedSnapshot = doc.export({
187+
mode: "shallow-snapshot",
188+
frontiers: doc.oplogFrontiers()
189+
});
190+
191+
// Create a new document from the sanitized snapshot
192+
const sanitizedDoc = new LoroDoc();
193+
sanitizedDoc.import(sanitizedSnapshot);
194+
195+
// The document has the final state
196+
console.log(sanitizedDoc.getText("text").toString());
197+
// Outputs: "Including with sanitized history"
198+
199+
// But ALL history before the snapshot point is completely removed
200+
console.log(sanitizedDoc.isShallow()); // true
201+
console.log(sanitizedDoc.shallowSinceFrontiers()); // Shows the starting point
202+
```
203+
204+
This approach is useful for:
205+
206+
1. Completely removing all old history that might contain various sensitive information
207+
2. Significantly reducing document size by eliminating unnecessary history
208+
3. Creating clean document instances after certain milestones
209+
4. Ensuring old operations cannot be recovered or examined
210+
211+
Compared to redaction:
212+
- Shallow snapshots completely remove all operations before a version point
213+
- Redaction selectively replaces just specific content with placeholders
214+
215+
**Important**: While both methods maintain future synchronization consistency, your application must distribute the sanitized document to all peers. Otherwise, the original document with sensitive information will still exist on other clients.
216+
217+
**When to use each approach**:
218+
- Use **redaction** when you need to sanitize specific operations (like an accidental password paste) while preserving older history
219+
- Use **shallow snapshots** when you want to completely eliminate all history before a certain point
220+
221+
</details>

0 commit comments

Comments
 (0)