@fluidframework/debugger 2.0.0-dev.2.3.0.115467 → 2.0.0-dev.4.1.0.148229

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.
Files changed (43) hide show
  1. package/.eslintrc.js +9 -12
  2. package/README.md +33 -33
  3. package/api-extractor.json +2 -2
  4. package/dist/fluidDebugger.d.ts +1 -2
  5. package/dist/fluidDebugger.d.ts.map +1 -1
  6. package/dist/fluidDebugger.js.map +1 -1
  7. package/dist/fluidDebuggerController.d.ts.map +1 -1
  8. package/dist/fluidDebuggerController.js +5 -2
  9. package/dist/fluidDebuggerController.js.map +1 -1
  10. package/dist/fluidDebuggerUi.d.ts.map +1 -1
  11. package/dist/fluidDebuggerUi.js +11 -5
  12. package/dist/fluidDebuggerUi.js.map +1 -1
  13. package/dist/messageSchema.js.map +1 -1
  14. package/dist/sanitize.js.map +1 -1
  15. package/dist/sanitizer.d.ts.map +1 -1
  16. package/dist/sanitizer.js +9 -10
  17. package/dist/sanitizer.js.map +1 -1
  18. package/lib/fluidDebugger.d.ts +1 -2
  19. package/lib/fluidDebugger.d.ts.map +1 -1
  20. package/lib/fluidDebugger.js.map +1 -1
  21. package/lib/fluidDebuggerController.d.ts.map +1 -1
  22. package/lib/fluidDebuggerController.js +5 -2
  23. package/lib/fluidDebuggerController.js.map +1 -1
  24. package/lib/fluidDebuggerUi.d.ts.map +1 -1
  25. package/lib/fluidDebuggerUi.js +11 -5
  26. package/lib/fluidDebuggerUi.js.map +1 -1
  27. package/lib/messageSchema.js.map +1 -1
  28. package/lib/sanitize.js.map +1 -1
  29. package/lib/sanitizer.d.ts.map +1 -1
  30. package/lib/sanitizer.js +9 -10
  31. package/lib/sanitizer.js.map +1 -1
  32. package/lib/test/types/validateDebuggerPrevious.generated.js +4 -4
  33. package/lib/test/types/validateDebuggerPrevious.generated.js.map +1 -1
  34. package/package.json +33 -37
  35. package/prettier.config.cjs +1 -1
  36. package/src/fluidDebugger.ts +30 -30
  37. package/src/fluidDebuggerController.ts +348 -322
  38. package/src/fluidDebuggerUi.ts +306 -281
  39. package/src/messageSchema.ts +367 -367
  40. package/src/sanitize.ts +29 -29
  41. package/src/sanitizer.ts +699 -643
  42. package/tsconfig.esnext.json +6 -6
  43. package/tsconfig.json +10 -15
package/src/sanitizer.ts CHANGED
@@ -20,35 +20,35 @@
20
20
 
21
21
  import * as Validator from "jsonschema";
22
22
  import { assert } from "@fluidframework/common-utils";
23
+ import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
23
24
  import {
24
- ISequencedDocumentMessage,
25
- } from "@fluidframework/protocol-definitions";
26
- import {
27
- attachContentsSchema,
28
- chunkedOpContentsSchema,
29
- joinContentsSchema,
30
- joinDataSchema,
31
- opContentsMapSchema,
32
- opContentsSchema,
33
- opContentsMergeTreeDeltaOpSchema,
34
- opContentsMergeTreeGroupOpSchema,
35
- opContentsRegisterCollectionSchema,
36
- proposeContentsSchema,
25
+ attachContentsSchema,
26
+ chunkedOpContentsSchema,
27
+ joinContentsSchema,
28
+ joinDataSchema,
29
+ opContentsMapSchema,
30
+ opContentsSchema,
31
+ opContentsMergeTreeDeltaOpSchema,
32
+ opContentsMergeTreeGroupOpSchema,
33
+ opContentsRegisterCollectionSchema,
34
+ proposeContentsSchema,
37
35
  } from "./messageSchema";
38
36
 
39
37
  enum TextType {
40
- Generic,
41
- Email,
42
- Name,
43
- FluidObject,
44
- MapKey,
38
+ Generic,
39
+ Email,
40
+ Name,
41
+ FluidObject,
42
+ MapKey,
45
43
  }
46
44
 
47
45
  // Workaround to jsonschema package not supporting "false" as a schema
48
46
  // that matches nothing
49
47
  const falseResult = {
50
- valid: false,
51
- toString: () => { return "Unmatched format"; },
48
+ valid: false,
49
+ toString: () => {
50
+ return "Unmatched format";
51
+ },
52
52
  };
53
53
 
54
54
  /**
@@ -58,630 +58,686 @@ const falseResult = {
58
58
  * size to the original message.
59
59
  */
60
60
  class ChunkedOpProcessor {
61
- /**
62
- * Message references so we can replace their contents in-place. These can
63
- * be top-level chunkedOp messages, or top-level op messages with a chunkedOp
64
- * within the contents
65
- */
66
- private messages = new Array<any>();
67
- /**
68
- * The messages' parsed contents for processing. Should parallel the
69
- * messages member
70
- */
71
- private parsedMessageContents = new Array<any>();
72
- private writtenBack = false;
73
- /**
74
- * keep track of the total starting length to make sure we don't somehow end
75
- * up with more content than we started with (meaning we may not be able to
76
- * write it back)
77
- */
78
- private concatenatedLength = 0;
79
-
80
- constructor(
81
- readonly validateSchemaFn: (object: any, schema: any) => boolean,
82
- readonly debug: boolean,
83
- ) { }
84
-
85
- debugMsg(msg: any) {
86
- if (this.debug) {
87
- console.error(msg);
88
- }
89
- }
90
-
91
- addMessage(message: any): void {
92
- this.messages.push(message);
93
-
94
- let parsed;
95
- try {
96
- parsed = JSON.parse(message.contents);
97
- if (message.type === "op") {
98
- // nested within a regular op
99
- // need to go deeper to get the desired contents
100
- parsed = parsed.contents;
101
- }
102
- } catch (e) {
103
- this.debugMsg(e);
104
- this.debugMsg(message.contents);
105
- }
106
- this.validateSchemaFn(parsed, chunkedOpContentsSchema);
107
- this.parsedMessageContents.push(parsed);
108
- }
109
-
110
- hasAllMessages(): boolean {
111
- const lastMsgContents = this.parsedMessageContents[this.parsedMessageContents.length - 1];
112
- return lastMsgContents.chunkId !== undefined && lastMsgContents.chunkId === lastMsgContents.totalChunks;
113
- }
114
-
115
- /**
116
- * @returns The concatenated contents of all the messages parsed as json
117
- */
118
- getConcatenatedContents(): any {
119
- const contentsString = this.parsedMessageContents.reduce((previousValue: string, currentValue: any) => {
120
- return previousValue + (currentValue.contents as string);
121
- }, "");
122
-
123
- this.concatenatedLength = contentsString.length;
124
- try {
125
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
126
- return JSON.parse(contentsString);
127
- } catch (e) {
128
- this.debugMsg(contentsString);
129
- this.debugMsg(e);
130
- return undefined;
131
- }
132
- }
133
-
134
- /**
135
- * Write back sanitized contents into the messages. The contents are
136
- * stringified, split up, and written in place to the messages that
137
- * were added earlier. The number of messages is preserved.
138
- * @param contents - Sanitized contents to write back
139
- */
140
- writeSanitizedContents(contents: any): void {
141
- // Write back a chunk size equal to the original
142
- const chunkSize = this.parsedMessageContents[0].contents.length;
143
-
144
- let stringified: string;
145
- try {
146
- stringified = JSON.stringify(contents);
147
- assert(stringified.length <= this.concatenatedLength,
148
- 0x089 /* "Stringified length of chunk contents > total starting length" */);
149
- } catch (e) {
150
- this.debugMsg(e);
151
- throw e;
152
- }
153
-
154
- for (let i = 0; i < this.messages.length; i++) {
155
- const substring = stringified.substring(i * chunkSize, (i + 1) * chunkSize);
156
-
157
- const parsedContents = this.parsedMessageContents[i];
158
- parsedContents.contents = substring;
159
- const message = this.messages[i];
160
-
161
- let stringifiedParsedContents;
162
- try {
163
- // for nested chunkedOps, we need to recreate the extra nesting layer
164
- // we removed earlier when adding the message
165
- if (message.type === "op") {
166
- const nestingLayer = {
167
- type: "chunkedOp",
168
- contents: parsedContents,
169
- };
170
- stringifiedParsedContents = JSON.stringify(nestingLayer);
171
- } else {
172
- stringifiedParsedContents = JSON.stringify(parsedContents);
173
- }
174
- } catch (e) {
175
- this.debugMsg(e);
176
- }
177
-
178
- message.contents = stringifiedParsedContents;
179
- }
180
-
181
- this.writtenBack = true;
182
- }
183
-
184
- reset(): void {
185
- assert(this.writtenBack, 0x08a /* "resetting ChunkedOpProcessor that never wrote back its contents" */);
186
- this.messages = new Array<any>();
187
- this.parsedMessageContents = new Array<any>();
188
- this.writtenBack = false;
189
- this.concatenatedLength = 0;
190
- }
191
-
192
- isPendingProcessing(): boolean {
193
- return this.messages.length !== 0;
194
- }
61
+ /**
62
+ * Message references so we can replace their contents in-place. These can
63
+ * be top-level chunkedOp messages, or top-level op messages with a chunkedOp
64
+ * within the contents
65
+ */
66
+ private messages = new Array<any>();
67
+ /**
68
+ * The messages' parsed contents for processing. Should parallel the
69
+ * messages member
70
+ */
71
+ private parsedMessageContents = new Array<any>();
72
+ private writtenBack = false;
73
+ /**
74
+ * keep track of the total starting length to make sure we don't somehow end
75
+ * up with more content than we started with (meaning we may not be able to
76
+ * write it back)
77
+ */
78
+ private concatenatedLength = 0;
79
+
80
+ constructor(
81
+ readonly validateSchemaFn: (object: any, schema: any) => boolean,
82
+ readonly debug: boolean,
83
+ ) {}
84
+
85
+ debugMsg(msg: any) {
86
+ if (this.debug) {
87
+ console.error(msg);
88
+ }
89
+ }
90
+
91
+ addMessage(message: any): void {
92
+ this.messages.push(message);
93
+
94
+ let parsed;
95
+ try {
96
+ parsed = JSON.parse(message.contents);
97
+ if (message.type === "op") {
98
+ // nested within a regular op
99
+ // need to go deeper to get the desired contents
100
+ parsed = parsed.contents;
101
+ }
102
+ } catch (e) {
103
+ this.debugMsg(e);
104
+ this.debugMsg(message.contents);
105
+ }
106
+ this.validateSchemaFn(parsed, chunkedOpContentsSchema);
107
+ this.parsedMessageContents.push(parsed);
108
+ }
109
+
110
+ hasAllMessages(): boolean {
111
+ const lastMsgContents = this.parsedMessageContents[this.parsedMessageContents.length - 1];
112
+ return (
113
+ lastMsgContents.chunkId !== undefined &&
114
+ lastMsgContents.chunkId === lastMsgContents.totalChunks
115
+ );
116
+ }
117
+
118
+ /**
119
+ * @returns The concatenated contents of all the messages parsed as json
120
+ */
121
+ getConcatenatedContents(): any {
122
+ const contentsString = this.parsedMessageContents.reduce(
123
+ (previousValue: string, currentValue: any) => {
124
+ return previousValue + (currentValue.contents as string);
125
+ },
126
+ "",
127
+ );
128
+
129
+ this.concatenatedLength = contentsString.length;
130
+ try {
131
+ return JSON.parse(contentsString);
132
+ } catch (e) {
133
+ this.debugMsg(contentsString);
134
+ this.debugMsg(e);
135
+ return undefined;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Write back sanitized contents into the messages. The contents are
141
+ * stringified, split up, and written in place to the messages that
142
+ * were added earlier. The number of messages is preserved.
143
+ * @param contents - Sanitized contents to write back
144
+ */
145
+ writeSanitizedContents(contents: any): void {
146
+ // Write back a chunk size equal to the original
147
+ const chunkSize = this.parsedMessageContents[0].contents.length;
148
+
149
+ let stringified: string;
150
+ try {
151
+ stringified = JSON.stringify(contents);
152
+ assert(
153
+ stringified.length <= this.concatenatedLength,
154
+ 0x089 /* "Stringified length of chunk contents > total starting length" */,
155
+ );
156
+ } catch (e) {
157
+ this.debugMsg(e);
158
+ throw e;
159
+ }
160
+
161
+ for (let i = 0; i < this.messages.length; i++) {
162
+ const substring = stringified.substring(i * chunkSize, (i + 1) * chunkSize);
163
+
164
+ const parsedContents = this.parsedMessageContents[i];
165
+ parsedContents.contents = substring;
166
+ const message = this.messages[i];
167
+
168
+ let stringifiedParsedContents;
169
+ try {
170
+ // for nested chunkedOps, we need to recreate the extra nesting layer
171
+ // we removed earlier when adding the message
172
+ if (message.type === "op") {
173
+ const nestingLayer = {
174
+ type: "chunkedOp",
175
+ contents: parsedContents,
176
+ };
177
+ stringifiedParsedContents = JSON.stringify(nestingLayer);
178
+ } else {
179
+ stringifiedParsedContents = JSON.stringify(parsedContents);
180
+ }
181
+ } catch (e) {
182
+ this.debugMsg(e);
183
+ }
184
+
185
+ message.contents = stringifiedParsedContents;
186
+ }
187
+
188
+ this.writtenBack = true;
189
+ }
190
+
191
+ reset(): void {
192
+ assert(
193
+ this.writtenBack,
194
+ 0x08a /* "resetting ChunkedOpProcessor that never wrote back its contents" */,
195
+ );
196
+ this.messages = new Array<any>();
197
+ this.parsedMessageContents = new Array<any>();
198
+ this.writtenBack = false;
199
+ this.concatenatedLength = 0;
200
+ }
201
+
202
+ isPendingProcessing(): boolean {
203
+ return this.messages.length !== 0;
204
+ }
195
205
  }
196
206
 
197
207
  export class Sanitizer {
198
- readonly validator = new Validator.Validator();
199
- // Represents the keys used to store Fluid object identifiers, snapshot info,
200
- // and other string fields that should not be replaced in contents blobs to
201
- // ensure the messages are still usable
202
- readonly defaultExcludedKeys = new Set<string>();
203
- // Represents the keys used by merge-tree ops their "seg" property, where other
204
- // keys represent user information
205
- readonly mergeTreeExcludedKeys = new Set<string>();
206
- // Map of user information to what it was replaced with. Used to ensure the same
207
- // data have the same replacements
208
- readonly replacementMap = new Map<string, string>();
209
-
210
- /**
211
- * Validate that the provided message matches the provided schema.
212
- * For a full scrub, warn and continue (scrubber should fully sanitize unexpected
213
- * fields for ops), otherwise throw an error because we cannot be sure user
214
- * information is being sufficiently sanitized.
215
- */
216
- objectMatchesSchema = (object: any, schema: any): boolean => {
217
- const result = schema === false ? falseResult : this.validator.validate(object, schema);
218
- if (!result.valid) {
219
- const errorMsg = `Bad msg fmt:\n${result.toString()}\n${JSON.stringify(object, undefined, 2)}`;
220
-
221
- if (this.fullScrub || this.noBail) {
222
- this.debugMsg(errorMsg);
223
- } else {
224
- throw new Error(errorMsg);
225
- }
226
- }
227
- return result.valid;
228
- };
229
-
230
- readonly chunkProcessor = new ChunkedOpProcessor(this.objectMatchesSchema, this.debug);
231
-
232
- constructor(
233
- readonly messages: ISequencedDocumentMessage[],
234
- readonly fullScrub: boolean,
235
- readonly noBail: boolean,
236
- readonly debug: boolean = false,
237
- ) {
238
- this.defaultExcludedKeys.add("type");
239
- this.defaultExcludedKeys.add("id");
240
- this.defaultExcludedKeys.add("pkg");
241
- this.defaultExcludedKeys.add("snapshotFormatVersion");
242
- this.defaultExcludedKeys.add("packageVersion");
243
- this.mergeTreeExcludedKeys.add("nodeType");
244
- }
245
-
246
- debugMsg(msg: any) {
247
- if (this.debug) {
248
- console.error(msg);
249
- }
250
- }
251
-
252
- isFluidObjectKey(key: string): boolean {
253
- return key === "type" || key === "id";
254
- }
255
-
256
- getRandomText(len: number): string {
257
- let str = "";
258
- while (str.length < len) {
259
- str = str + Math.random().toString(36).substring(2);
260
- }
261
- return str.substr(0, len);
262
- }
263
-
264
- readonly wordTokenRegex = /\S+/g;
265
-
266
- readonly replaceRandomTextFn = (match: string): string => {
267
- if (this.replacementMap.has(match)) {
268
- return this.replacementMap.get(match)!;
269
- }
270
-
271
- const replacement = this.getRandomText(match.length);
272
- this.replacementMap.set(match, replacement);
273
- return replacement;
274
- };
275
-
276
- /**
277
- * Replace text with garbage. FluidObject types are not replaced when not under
278
- * full scrub mode. All other text is replaced consistently.
279
- */
280
- replaceText(input?: string, type: TextType = TextType.Generic): string | undefined {
281
- if (input === undefined) {
282
- return undefined;
283
- }
284
-
285
- if (type === TextType.FluidObject) {
286
- if (this.replacementMap.has(input)) {
287
- return this.replacementMap.get(input)!;
288
- }
289
-
290
- const replacement = this.fullScrub ? this.getRandomText(input.length) : input;
291
-
292
- this.replacementMap.set(input, replacement);
293
- return replacement;
294
- }
295
-
296
- return input.replace(this.wordTokenRegex, this.replaceRandomTextFn);
297
- }
298
-
299
- replaceArray(input: any[]): any[] {
300
- for (let i = 0; i < input.length; i++) {
301
- const value = input[i];
302
- if (typeof value === "string") {
303
- input[i] = this.replaceText(value);
304
- } else if (Array.isArray(value)) {
305
- input[i] = this.replaceArray(value);
306
- } else if (typeof value === "object") {
307
- input[i] = this.replaceObject(value);
308
- }
309
- }
310
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
311
- return input;
312
- }
313
-
314
- /**
315
- * (sort of) recurses down the values of a JSON object to sanitize all its strings
316
- * (only checks strings, arrays, and objects)
317
- * @param input - The object to sanitize
318
- * @param excludedKeys - object keys for which to skip replacement when not in fullScrub
319
- */
320
- // eslint-disable-next-line @typescript-eslint/ban-types
321
- replaceObject(input: object | null, excludedKeys: Set<string> = this.defaultExcludedKeys): object | null {
322
- // File might contain actual nulls
323
- if (input === null || input === undefined) {
324
- return input;
325
- }
326
-
327
- const keys = Object.keys(input);
328
- keys.forEach((key) => {
329
- if (this.fullScrub || !excludedKeys.has(key)) {
330
- const value = input[key];
331
- if (typeof value === "string") {
332
- input[key] = this.replaceText(
333
- value,
334
- this.isFluidObjectKey(key) ? TextType.FluidObject : TextType.Generic,
335
- );
336
- } else if (Array.isArray(value)) {
337
- input[key] = this.replaceArray(value);
338
- } else if (typeof value === "object") {
339
- input[key] = this.replaceObject(value, excludedKeys);
340
- }
341
- }
342
- });
343
- return input;
344
- }
345
-
346
- /**
347
- * Replacement on an unknown type or a parsed root level object
348
- * without a key
349
- * @param input - The object to sanitize
350
- * @param excludedKeys - object keys for which to skip replacement when not in fullScrub
351
- */
352
- replaceAny(input: any, excludedKeys: Set<string> = this.defaultExcludedKeys): any {
353
- if (input === null || input === undefined) {
354
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
355
- return input;
356
- }
357
-
358
- if (typeof input === "string") {
359
- return this.replaceText(input);
360
- } else if (Array.isArray(input)) {
361
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
362
- return this.replaceArray(input);
363
- } else if (typeof input === "object") {
364
- return this.replaceObject(input, excludedKeys);
365
- }
366
-
367
- // Don't run replacement on any other types
368
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
369
- return input;
370
- }
371
-
372
- fixJoin(message: any) {
373
- if (!this.objectMatchesSchema(message.contents, joinContentsSchema)) {
374
- message.contents = this.replaceAny(message.contents);
375
- }
376
-
377
- try {
378
- let data = JSON.parse(message.data);
379
- if (!this.objectMatchesSchema(data, joinDataSchema)) {
380
- data = this.replaceAny(data);
381
- } else {
382
- const user = data.detail.user;
383
- user.id = this.replaceText(user.id, TextType.Email);
384
- user.email = this.replaceText(user.email, TextType.Email);
385
- user.name = this.replaceText(user.name, TextType.Name);
386
- }
387
-
388
- message.data = JSON.stringify(data);
389
- } catch (e) {
390
- this.debugMsg(e);
391
- }
392
- }
393
-
394
- fixPropose(message: any) {
395
- if (!this.objectMatchesSchema(message.contents, proposeContentsSchema)) {
396
- message.contents = this.replaceAny(message.contents);
397
- } else {
398
- if (typeof message.contents === "string") {
399
- try {
400
- const data = JSON.parse(message.contents);
401
- if (this.fullScrub) {
402
- const pkg = data.value?.package;
403
- if (pkg?.name) {
404
- pkg.name = this.replaceText(pkg.name, TextType.FluidObject);
405
- }
406
- if (Array.isArray(pkg?.fluid?.browser?.umd?.files)) {
407
- pkg.fluid.browser.umd.files = this.replaceArray(pkg.fluid.browser.umd.files);
408
- }
409
- }
410
- } catch (e) {
411
- this.debugMsg(e);
412
- }
413
- } else {
414
- if (this.fullScrub) {
415
- message.contents.value = this.replaceText(message.contents.value, TextType.FluidObject);
416
- }
417
- }
418
- }
419
- }
420
-
421
- fixAttachEntries(entries: any[]) {
422
- entries.forEach((element) => {
423
- // Tree type
424
- if (element.value.entries) {
425
- this.fixAttachEntries(element.value.entries);
426
- } else {
427
- // Blob (leaf) type
428
- try {
429
- if (typeof element.value.contents === "string") {
430
- let data = JSON.parse(element.value.contents);
431
- data = this.replaceObject(data);
432
- element.value.contents = JSON.stringify(data);
433
- }
434
- } catch (e) {
435
- this.debugMsg(e);
436
- }
437
- }
438
- });
439
- }
440
-
441
- /**
442
- * Fix the content of an attach in place
443
- * @param contents - contents object to fix
444
- */
445
- fixAttachContents(contents: any): any {
446
- assert(typeof contents === "object", 0x08b /* "Unexpected type on contents for fix of an attach!" */);
447
- if (!this.objectMatchesSchema(contents, attachContentsSchema)) {
448
- this.replaceObject(contents);
449
- } else {
450
- if (this.fullScrub) {
451
- contents.id = this.replaceText(contents.id, TextType.FluidObject);
452
- contents.type = this.replaceText(contents.type, TextType.FluidObject);
453
- }
454
-
455
- this.fixAttachEntries(contents.snapshot.entries);
456
- }
457
- }
458
-
459
- /**
460
- * Fix an attach message at the root level or a ContainerMessageType attach. Attach
461
- * messages found within an op message should instead have their contents parsed out
462
- * and sent to fixAttachContents.
463
- * @param message - The attach message to fix
464
- * @param withinOp - If the message is from within an op message (as opposed to being
465
- * an attach message at the root level). Root level attach messages have "snapshot"
466
- * under a "contents" key, whereas attach messages from within an op message have it
467
- * under a "content" key
468
- */
469
- fixAttach(message: any) {
470
- // Handle case where contents is stringified json
471
- if (typeof message.contents === "string") {
472
- try {
473
- const data = JSON.parse(message.contents);
474
- this.fixAttachContents(data);
475
- message.contents = JSON.stringify(data);
476
- } catch (e) {
477
- this.debugMsg(e);
478
- return;
479
- }
480
- } else {
481
- this.fixAttachContents(message.contents);
482
- }
483
- }
484
-
485
- fixDeltaOp(deltaOp: any) {
486
- deltaOp.seg = typeof deltaOp.seg === "string"
487
- ? this.replaceText(deltaOp.seg)
488
- : this.replaceObject(deltaOp.seg, this.mergeTreeExcludedKeys);
489
- }
490
-
491
- /**
492
- * Fix the contents object for an op message. Does not do extra type handling. Does
493
- * not handle special container message types like "attach", "component", and
494
- * "chunkedOp" (these should be handled by the caller)
495
- * @param contents - The contents object for an op message. If it was a string in the
496
- * message, it must have been converted to an object first
497
- */
498
- fixOpContentsObject(contents: any) {
499
- // do replacement
500
- if (!this.objectMatchesSchema(contents, opContentsSchema)) {
501
- this.replaceAny(contents);
502
- } else {
503
- if (this.fullScrub) {
504
- contents.address = this.replaceText(contents.address, TextType.FluidObject);
505
- }
506
-
507
- const innerContent = contents.contents.content;
508
- assert(innerContent !== undefined, 0x08c /* "innerContent for fixing op contents is undefined!" */);
509
- if (contents.contents.type === "attach") {
510
- // attach op
511
- // handle case where inner content is stringified json
512
- if (typeof contents.contents.content === "string") {
513
- try {
514
- const data = JSON.parse(contents.contents.content);
515
- this.fixAttachContents(data);
516
- contents.contents.content = JSON.stringify(data);
517
- } catch (e) {
518
- this.debugMsg(e);
519
- }
520
- } else {
521
- this.fixAttachContents(contents.contents.content);
522
- }
523
- } else if (this.validator.validate(innerContent, opContentsMapSchema).valid) {
524
- // map op
525
- if (this.fullScrub) {
526
- innerContent.address = this.replaceText(innerContent.address, TextType.FluidObject);
527
- innerContent.contents.key = this.replaceText(innerContent.contents.key, TextType.MapKey);
528
- }
529
- if (innerContent.contents.value !== undefined) {
530
- innerContent.contents.value.value = this.replaceAny(innerContent.contents.value.value);
531
- }
532
- } else if (this.validator.validate(innerContent, opContentsMergeTreeGroupOpSchema).valid) {
533
- // merge tree group op
534
- if (this.fullScrub) {
535
- innerContent.address = this.replaceText(innerContent.address, TextType.FluidObject);
536
- }
537
- innerContent.contents.ops.forEach((deltaOp) => {
538
- this.fixDeltaOp(deltaOp);
539
- });
540
- } else if (this.validator.validate(innerContent, opContentsMergeTreeDeltaOpSchema).valid) {
541
- // merge tree delta op
542
- if (this.fullScrub) {
543
- innerContent.address = this.replaceText(innerContent.address, TextType.FluidObject);
544
- }
545
- this.fixDeltaOp(innerContent.contents);
546
- } else if (this.validator.validate(innerContent, opContentsRegisterCollectionSchema).valid) {
547
- // register collection op
548
- if (this.fullScrub) {
549
- innerContent.address = this.replaceText(innerContent.address, TextType.FluidObject);
550
- innerContent.contents.key = this.replaceText(innerContent.contents.key, TextType.MapKey);
551
- }
552
- if (innerContent.contents.value !== undefined) {
553
- innerContent.contents.value.value = this.replaceAny(innerContent.contents.value.value);
554
- }
555
- } else {
556
- // message contents don't match any known op format
557
- this.objectMatchesSchema(contents, false);
558
- }
559
- }
560
- }
561
-
562
- fixOp(message: any) {
563
- // handle case where contents is stringified json
564
- let msgContents;
565
- if (typeof message.contents === "string") {
566
- try {
567
- msgContents = JSON.parse(message.contents);
568
- } catch (e) {
569
- this.debugMsg(e);
570
- return;
571
- }
572
- } else {
573
- msgContents = message.contents;
574
- }
575
-
576
- // handle container message types
577
- switch (msgContents.type) {
578
- case "attach": {
579
- // this one is like a regular attach op, except its contents aren't nested as deep
580
- // run fixAttach directly and return
581
- this.fixAttach(msgContents);
582
- break;
583
- }
584
- case "component": {
585
- // this one functionally nests its contents one layer deeper
586
- // bring up the contents object and continue as usual
587
- this.fixOpContentsObject(msgContents.contents);
588
- break;
589
- }
590
- case "chunkedOp": {
591
- // this is a (regular?) op split into multiple parts due to size, e.g. because it
592
- // has an attached image, and where the chunkedOp is within the top-level op's contents
593
- // (as opposed to being at the top-level). The contents of the chunks need to be
594
- // concatenated to form the complete stringified json object
595
- // Early return here to skip re-stringify because no changes are made until the last
596
- // chunk, and the ChunkedOpProcessor will handle everything at that point
597
- return this.fixChunkedOp(message);
598
- }
599
- case "blobAttach": {
600
- // TODO: handle this properly once blob api is used
601
- this.debugMsg("TODO: blobAttach ops are skipped/unhandled");
602
- return;
603
- }
604
- default: {
605
- // A regular op
606
- this.fixOpContentsObject(msgContents);
607
- }
608
- }
609
-
610
- // re-stringify the json if needed
611
- if (typeof message.contents === "string") {
612
- try {
613
- message.contents = JSON.stringify(msgContents);
614
- } catch (e) {
615
- this.debugMsg(e);
616
- return;
617
- }
618
- }
619
- }
620
-
621
- /**
622
- * @param message - The top-level chunkedOp message or a top-level op message
623
- * with a chunkedOp inside its contents
624
- */
625
- fixChunkedOp(message: any) {
626
- this.chunkProcessor.addMessage(message);
627
- if (!this.chunkProcessor.hasAllMessages()) {
628
- return;
629
- }
630
-
631
- const contents = this.chunkProcessor.getConcatenatedContents();
632
- this.fixOpContentsObject(contents);
633
-
634
- this.chunkProcessor.writeSanitizedContents(contents);
635
- this.chunkProcessor.reset();
636
- }
637
-
638
- sanitize(): ISequencedDocumentMessage[] {
639
- let seq = 0;
640
-
641
- try {
642
- this.messages.map((message) => {
643
- seq = message.sequenceNumber;
644
- // message types from protocol-definitions' protocol.ts
645
- switch (message.type) {
646
- case "join": {
647
- this.fixJoin(message);
648
- break;
649
- }
650
- case "propose": {
651
- this.fixPropose(message);
652
- break;
653
- }
654
- case "attach": {
655
- this.fixAttach(message);
656
- break;
657
- }
658
- case "op": {
659
- this.fixOp(message);
660
- break;
661
- }
662
- case "chunkedOp": {
663
- this.fixChunkedOp(message);
664
- break;
665
- }
666
- case "noop":
667
- case "leave":
668
- case "noClient":
669
- case "summarize":
670
- case "summaryAck":
671
- case "summaryNack":
672
- break;
673
- default:
674
- this.debugMsg(`Unexpected op type ${message.type}`);
675
- }
676
- });
677
-
678
- // make sure we don't miss an incomplete chunked op at the end
679
- assert(!this.chunkProcessor.isPendingProcessing(), 0x08d /* "After sanitize, pending incomplete ops!" */);
680
- } catch (error) {
681
- this.debugMsg(`Error while processing sequenceNumber ${seq}`);
682
- throw error;
683
- }
684
-
685
- return this.messages;
686
- }
208
+ readonly validator = new Validator.Validator();
209
+ // Represents the keys used to store Fluid object identifiers, snapshot info,
210
+ // and other string fields that should not be replaced in contents blobs to
211
+ // ensure the messages are still usable
212
+ readonly defaultExcludedKeys = new Set<string>();
213
+ // Represents the keys used by merge-tree ops their "seg" property, where other
214
+ // keys represent user information
215
+ readonly mergeTreeExcludedKeys = new Set<string>();
216
+ // Map of user information to what it was replaced with. Used to ensure the same
217
+ // data have the same replacements
218
+ readonly replacementMap = new Map<string, string>();
219
+
220
+ /**
221
+ * Validate that the provided message matches the provided schema.
222
+ * For a full scrub, warn and continue (scrubber should fully sanitize unexpected
223
+ * fields for ops), otherwise throw an error because we cannot be sure user
224
+ * information is being sufficiently sanitized.
225
+ */
226
+ objectMatchesSchema = (object: any, schema: any): boolean => {
227
+ const result = schema === false ? falseResult : this.validator.validate(object, schema);
228
+ if (!result.valid) {
229
+ const errorMsg = `Bad msg fmt:\n${result.toString()}\n${JSON.stringify(
230
+ object,
231
+ undefined,
232
+ 2,
233
+ )}`;
234
+
235
+ if (this.fullScrub || this.noBail) {
236
+ this.debugMsg(errorMsg);
237
+ } else {
238
+ throw new Error(errorMsg);
239
+ }
240
+ }
241
+ return result.valid;
242
+ };
243
+
244
+ readonly chunkProcessor = new ChunkedOpProcessor(this.objectMatchesSchema, this.debug);
245
+
246
+ constructor(
247
+ readonly messages: ISequencedDocumentMessage[],
248
+ readonly fullScrub: boolean,
249
+ readonly noBail: boolean,
250
+ readonly debug: boolean = false,
251
+ ) {
252
+ this.defaultExcludedKeys.add("type");
253
+ this.defaultExcludedKeys.add("id");
254
+ this.defaultExcludedKeys.add("pkg");
255
+ this.defaultExcludedKeys.add("snapshotFormatVersion");
256
+ this.defaultExcludedKeys.add("packageVersion");
257
+ this.mergeTreeExcludedKeys.add("nodeType");
258
+ }
259
+
260
+ debugMsg(msg: any) {
261
+ if (this.debug) {
262
+ console.error(msg);
263
+ }
264
+ }
265
+
266
+ isFluidObjectKey(key: string): boolean {
267
+ return key === "type" || key === "id";
268
+ }
269
+
270
+ getRandomText(len: number): string {
271
+ let str = "";
272
+ while (str.length < len) {
273
+ str = str + Math.random().toString(36).substring(2);
274
+ }
275
+ return str.substr(0, len);
276
+ }
277
+
278
+ readonly wordTokenRegex = /\S+/g;
279
+
280
+ readonly replaceRandomTextFn = (match: string): string => {
281
+ if (this.replacementMap.has(match)) {
282
+ return this.replacementMap.get(match)!;
283
+ }
284
+
285
+ const replacement = this.getRandomText(match.length);
286
+ this.replacementMap.set(match, replacement);
287
+ return replacement;
288
+ };
289
+
290
+ /**
291
+ * Replace text with garbage. FluidObject types are not replaced when not under
292
+ * full scrub mode. All other text is replaced consistently.
293
+ */
294
+ replaceText(input?: string, type: TextType = TextType.Generic): string | undefined {
295
+ if (input === undefined) {
296
+ return undefined;
297
+ }
298
+
299
+ if (type === TextType.FluidObject) {
300
+ if (this.replacementMap.has(input)) {
301
+ return this.replacementMap.get(input)!;
302
+ }
303
+
304
+ const replacement = this.fullScrub ? this.getRandomText(input.length) : input;
305
+
306
+ this.replacementMap.set(input, replacement);
307
+ return replacement;
308
+ }
309
+
310
+ return input.replace(this.wordTokenRegex, this.replaceRandomTextFn);
311
+ }
312
+
313
+ replaceArray(input: any[]): any[] {
314
+ for (let i = 0; i < input.length; i++) {
315
+ const value = input[i];
316
+ if (typeof value === "string") {
317
+ input[i] = this.replaceText(value);
318
+ } else if (Array.isArray(value)) {
319
+ input[i] = this.replaceArray(value);
320
+ } else if (typeof value === "object") {
321
+ input[i] = this.replaceObject(value);
322
+ }
323
+ }
324
+ return input;
325
+ }
326
+
327
+ /**
328
+ * (sort of) recurses down the values of a JSON object to sanitize all its strings
329
+ * (only checks strings, arrays, and objects)
330
+ * @param input - The object to sanitize
331
+ * @param excludedKeys - object keys for which to skip replacement when not in fullScrub
332
+ */
333
+ replaceObject(
334
+ input: object | null,
335
+ excludedKeys: Set<string> = this.defaultExcludedKeys,
336
+ ): object | null {
337
+ // File might contain actual nulls
338
+ if (input === null || input === undefined) {
339
+ return input;
340
+ }
341
+
342
+ const keys = Object.keys(input);
343
+ keys.forEach((key) => {
344
+ if (this.fullScrub || !excludedKeys.has(key)) {
345
+ const value = input[key];
346
+ if (typeof value === "string") {
347
+ input[key] = this.replaceText(
348
+ value,
349
+ this.isFluidObjectKey(key) ? TextType.FluidObject : TextType.Generic,
350
+ );
351
+ } else if (Array.isArray(value)) {
352
+ input[key] = this.replaceArray(value);
353
+ } else if (typeof value === "object") {
354
+ input[key] = this.replaceObject(value, excludedKeys);
355
+ }
356
+ }
357
+ });
358
+ return input;
359
+ }
360
+
361
+ /**
362
+ * Replacement on an unknown type or a parsed root level object
363
+ * without a key
364
+ * @param input - The object to sanitize
365
+ * @param excludedKeys - object keys for which to skip replacement when not in fullScrub
366
+ */
367
+ replaceAny(input: any, excludedKeys: Set<string> = this.defaultExcludedKeys): any {
368
+ if (input === null || input === undefined) {
369
+ return input;
370
+ }
371
+
372
+ if (typeof input === "string") {
373
+ return this.replaceText(input);
374
+ } else if (Array.isArray(input)) {
375
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
376
+ return this.replaceArray(input);
377
+ } else if (typeof input === "object") {
378
+ return this.replaceObject(input, excludedKeys);
379
+ }
380
+
381
+ // Don't run replacement on any other types
382
+ return input;
383
+ }
384
+
385
+ fixJoin(message: any) {
386
+ if (!this.objectMatchesSchema(message.contents, joinContentsSchema)) {
387
+ message.contents = this.replaceAny(message.contents);
388
+ }
389
+
390
+ try {
391
+ let data = JSON.parse(message.data);
392
+ if (!this.objectMatchesSchema(data, joinDataSchema)) {
393
+ data = this.replaceAny(data);
394
+ } else {
395
+ const user = data.detail.user;
396
+ user.id = this.replaceText(user.id, TextType.Email);
397
+ user.email = this.replaceText(user.email, TextType.Email);
398
+ user.name = this.replaceText(user.name, TextType.Name);
399
+ }
400
+
401
+ message.data = JSON.stringify(data);
402
+ } catch (e) {
403
+ this.debugMsg(e);
404
+ }
405
+ }
406
+
407
+ fixPropose(message: any) {
408
+ if (!this.objectMatchesSchema(message.contents, proposeContentsSchema)) {
409
+ message.contents = this.replaceAny(message.contents);
410
+ } else {
411
+ if (typeof message.contents === "string") {
412
+ try {
413
+ const data = JSON.parse(message.contents);
414
+ if (this.fullScrub) {
415
+ const pkg = data.value?.package;
416
+ if (pkg?.name) {
417
+ pkg.name = this.replaceText(pkg.name, TextType.FluidObject);
418
+ }
419
+ if (Array.isArray(pkg?.fluid?.browser?.umd?.files)) {
420
+ pkg.fluid.browser.umd.files = this.replaceArray(
421
+ pkg.fluid.browser.umd.files,
422
+ );
423
+ }
424
+ }
425
+ } catch (e) {
426
+ this.debugMsg(e);
427
+ }
428
+ } else {
429
+ if (this.fullScrub) {
430
+ message.contents.value = this.replaceText(
431
+ message.contents.value,
432
+ TextType.FluidObject,
433
+ );
434
+ }
435
+ }
436
+ }
437
+ }
438
+
439
+ fixAttachEntries(entries: any[]) {
440
+ entries.forEach((element) => {
441
+ // Tree type
442
+ if (element.value.entries) {
443
+ this.fixAttachEntries(element.value.entries);
444
+ } else {
445
+ // Blob (leaf) type
446
+ try {
447
+ if (typeof element.value.contents === "string") {
448
+ let data = JSON.parse(element.value.contents);
449
+ data = this.replaceObject(data);
450
+ element.value.contents = JSON.stringify(data);
451
+ }
452
+ } catch (e) {
453
+ this.debugMsg(e);
454
+ }
455
+ }
456
+ });
457
+ }
458
+
459
+ /**
460
+ * Fix the content of an attach in place
461
+ * @param contents - contents object to fix
462
+ */
463
+ fixAttachContents(contents: any): any {
464
+ assert(
465
+ typeof contents === "object",
466
+ 0x08b /* "Unexpected type on contents for fix of an attach!" */,
467
+ );
468
+ if (!this.objectMatchesSchema(contents, attachContentsSchema)) {
469
+ this.replaceObject(contents);
470
+ } else {
471
+ if (this.fullScrub) {
472
+ contents.id = this.replaceText(contents.id, TextType.FluidObject);
473
+ contents.type = this.replaceText(contents.type, TextType.FluidObject);
474
+ }
475
+
476
+ this.fixAttachEntries(contents.snapshot.entries);
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Fix an attach message at the root level or a ContainerMessageType attach. Attach
482
+ * messages found within an op message should instead have their contents parsed out
483
+ * and sent to fixAttachContents.
484
+ * @param message - The attach message to fix
485
+ * @param withinOp - If the message is from within an op message (as opposed to being
486
+ * an attach message at the root level). Root level attach messages have "snapshot"
487
+ * under a "contents" key, whereas attach messages from within an op message have it
488
+ * under a "content" key
489
+ */
490
+ fixAttach(message: any) {
491
+ // Handle case where contents is stringified json
492
+ if (typeof message.contents === "string") {
493
+ try {
494
+ const data = JSON.parse(message.contents);
495
+ this.fixAttachContents(data);
496
+ message.contents = JSON.stringify(data);
497
+ } catch (e) {
498
+ this.debugMsg(e);
499
+ return;
500
+ }
501
+ } else {
502
+ this.fixAttachContents(message.contents);
503
+ }
504
+ }
505
+
506
+ fixDeltaOp(deltaOp: any) {
507
+ deltaOp.seg =
508
+ typeof deltaOp.seg === "string"
509
+ ? this.replaceText(deltaOp.seg)
510
+ : this.replaceObject(deltaOp.seg, this.mergeTreeExcludedKeys);
511
+ }
512
+
513
+ /**
514
+ * Fix the contents object for an op message. Does not do extra type handling. Does
515
+ * not handle special container message types like "attach", "component", and
516
+ * "chunkedOp" (these should be handled by the caller)
517
+ * @param contents - The contents object for an op message. If it was a string in the
518
+ * message, it must have been converted to an object first
519
+ */
520
+ fixOpContentsObject(contents: any) {
521
+ // do replacement
522
+ if (!this.objectMatchesSchema(contents, opContentsSchema)) {
523
+ this.replaceAny(contents);
524
+ } else {
525
+ if (this.fullScrub) {
526
+ contents.address = this.replaceText(contents.address, TextType.FluidObject);
527
+ }
528
+
529
+ const innerContent = contents.contents.content;
530
+ assert(
531
+ innerContent !== undefined,
532
+ 0x08c /* "innerContent for fixing op contents is undefined!" */,
533
+ );
534
+ if (contents.contents.type === "attach") {
535
+ // attach op
536
+ // handle case where inner content is stringified json
537
+ if (typeof contents.contents.content === "string") {
538
+ try {
539
+ const data = JSON.parse(contents.contents.content);
540
+ this.fixAttachContents(data);
541
+ contents.contents.content = JSON.stringify(data);
542
+ } catch (e) {
543
+ this.debugMsg(e);
544
+ }
545
+ } else {
546
+ this.fixAttachContents(contents.contents.content);
547
+ }
548
+ } else if (this.validator.validate(innerContent, opContentsMapSchema).valid) {
549
+ // map op
550
+ if (this.fullScrub) {
551
+ innerContent.address = this.replaceText(
552
+ innerContent.address,
553
+ TextType.FluidObject,
554
+ );
555
+ innerContent.contents.key = this.replaceText(
556
+ innerContent.contents.key,
557
+ TextType.MapKey,
558
+ );
559
+ }
560
+ if (innerContent.contents.value !== undefined) {
561
+ innerContent.contents.value.value = this.replaceAny(
562
+ innerContent.contents.value.value,
563
+ );
564
+ }
565
+ } else if (
566
+ this.validator.validate(innerContent, opContentsMergeTreeGroupOpSchema).valid
567
+ ) {
568
+ // merge tree group op
569
+ if (this.fullScrub) {
570
+ innerContent.address = this.replaceText(
571
+ innerContent.address,
572
+ TextType.FluidObject,
573
+ );
574
+ }
575
+ innerContent.contents.ops.forEach((deltaOp) => {
576
+ this.fixDeltaOp(deltaOp);
577
+ });
578
+ } else if (
579
+ this.validator.validate(innerContent, opContentsMergeTreeDeltaOpSchema).valid
580
+ ) {
581
+ // merge tree delta op
582
+ if (this.fullScrub) {
583
+ innerContent.address = this.replaceText(
584
+ innerContent.address,
585
+ TextType.FluidObject,
586
+ );
587
+ }
588
+ this.fixDeltaOp(innerContent.contents);
589
+ } else if (
590
+ this.validator.validate(innerContent, opContentsRegisterCollectionSchema).valid
591
+ ) {
592
+ // register collection op
593
+ if (this.fullScrub) {
594
+ innerContent.address = this.replaceText(
595
+ innerContent.address,
596
+ TextType.FluidObject,
597
+ );
598
+ innerContent.contents.key = this.replaceText(
599
+ innerContent.contents.key,
600
+ TextType.MapKey,
601
+ );
602
+ }
603
+ if (innerContent.contents.value !== undefined) {
604
+ innerContent.contents.value.value = this.replaceAny(
605
+ innerContent.contents.value.value,
606
+ );
607
+ }
608
+ } else {
609
+ // message contents don't match any known op format
610
+ this.objectMatchesSchema(contents, false);
611
+ }
612
+ }
613
+ }
614
+
615
+ fixOp(message: any) {
616
+ // handle case where contents is stringified json
617
+ let msgContents;
618
+ if (typeof message.contents === "string") {
619
+ try {
620
+ msgContents = JSON.parse(message.contents);
621
+ } catch (e) {
622
+ this.debugMsg(e);
623
+ return;
624
+ }
625
+ } else {
626
+ msgContents = message.contents;
627
+ }
628
+
629
+ // handle container message types
630
+ switch (msgContents.type) {
631
+ case "attach": {
632
+ // this one is like a regular attach op, except its contents aren't nested as deep
633
+ // run fixAttach directly and return
634
+ this.fixAttach(msgContents);
635
+ break;
636
+ }
637
+ case "component": {
638
+ // this one functionally nests its contents one layer deeper
639
+ // bring up the contents object and continue as usual
640
+ this.fixOpContentsObject(msgContents.contents);
641
+ break;
642
+ }
643
+ case "chunkedOp": {
644
+ // this is a (regular?) op split into multiple parts due to size, e.g. because it
645
+ // has an attached image, and where the chunkedOp is within the top-level op's contents
646
+ // (as opposed to being at the top-level). The contents of the chunks need to be
647
+ // concatenated to form the complete stringified json object
648
+ // Early return here to skip re-stringify because no changes are made until the last
649
+ // chunk, and the ChunkedOpProcessor will handle everything at that point
650
+ return this.fixChunkedOp(message);
651
+ }
652
+ case "blobAttach": {
653
+ // TODO: handle this properly once blob api is used
654
+ this.debugMsg("TODO: blobAttach ops are skipped/unhandled");
655
+ return;
656
+ }
657
+ default: {
658
+ // A regular op
659
+ this.fixOpContentsObject(msgContents);
660
+ }
661
+ }
662
+
663
+ // re-stringify the json if needed
664
+ if (typeof message.contents === "string") {
665
+ try {
666
+ message.contents = JSON.stringify(msgContents);
667
+ } catch (e) {
668
+ this.debugMsg(e);
669
+ return;
670
+ }
671
+ }
672
+ }
673
+
674
+ /**
675
+ * @param message - The top-level chunkedOp message or a top-level op message
676
+ * with a chunkedOp inside its contents
677
+ */
678
+ fixChunkedOp(message: any) {
679
+ this.chunkProcessor.addMessage(message);
680
+ if (!this.chunkProcessor.hasAllMessages()) {
681
+ return;
682
+ }
683
+
684
+ const contents = this.chunkProcessor.getConcatenatedContents();
685
+ this.fixOpContentsObject(contents);
686
+
687
+ this.chunkProcessor.writeSanitizedContents(contents);
688
+ this.chunkProcessor.reset();
689
+ }
690
+
691
+ sanitize(): ISequencedDocumentMessage[] {
692
+ let seq = 0;
693
+
694
+ try {
695
+ this.messages.map((message) => {
696
+ seq = message.sequenceNumber;
697
+ // message types from protocol-definitions' protocol.ts
698
+ switch (message.type) {
699
+ case "join": {
700
+ this.fixJoin(message);
701
+ break;
702
+ }
703
+ case "propose": {
704
+ this.fixPropose(message);
705
+ break;
706
+ }
707
+ case "attach": {
708
+ this.fixAttach(message);
709
+ break;
710
+ }
711
+ case "op": {
712
+ this.fixOp(message);
713
+ break;
714
+ }
715
+ case "chunkedOp": {
716
+ this.fixChunkedOp(message);
717
+ break;
718
+ }
719
+ case "noop":
720
+ case "leave":
721
+ case "noClient":
722
+ case "summarize":
723
+ case "summaryAck":
724
+ case "summaryNack":
725
+ break;
726
+ default:
727
+ this.debugMsg(`Unexpected op type ${message.type}`);
728
+ }
729
+ });
730
+
731
+ // make sure we don't miss an incomplete chunked op at the end
732
+ assert(
733
+ !this.chunkProcessor.isPendingProcessing(),
734
+ 0x08d /* "After sanitize, pending incomplete ops!" */,
735
+ );
736
+ } catch (error) {
737
+ this.debugMsg(`Error while processing sequenceNumber ${seq}`);
738
+ throw error;
739
+ }
740
+
741
+ return this.messages;
742
+ }
687
743
  }