fabricjs-document-engine 0.7.0 → 1.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.
Files changed (39) hide show
  1. package/README.md +45 -91
  2. package/dist/assets/asset-pipeline.cjs +3 -1
  3. package/dist/assets/asset-pipeline.js +3 -1
  4. package/dist/engine/create-document-engine.cjs +16 -6
  5. package/dist/engine/create-document-engine.d.cts +7 -1
  6. package/dist/engine/create-document-engine.d.ts +7 -1
  7. package/dist/engine/create-document-engine.js +17 -7
  8. package/dist/engine/errors.d.cts +1 -1
  9. package/dist/engine/errors.d.ts +1 -1
  10. package/dist/export/render-export.cjs +3 -15
  11. package/dist/export/render-export.js +4 -15
  12. package/dist/history/apply-state.cjs +3 -1
  13. package/dist/history/apply-state.js +3 -1
  14. package/dist/history/create-history.cjs +4 -1
  15. package/dist/history/create-history.d.cts +1 -0
  16. package/dist/history/create-history.d.ts +1 -0
  17. package/dist/history/create-history.js +4 -1
  18. package/dist/history/history-stack.cjs +44 -8
  19. package/dist/history/history-stack.js +44 -9
  20. package/dist/index.cjs +4 -0
  21. package/dist/index.d.cts +3 -2
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.js +2 -1
  24. package/dist/recovery/recovery-controller.cjs +30 -3
  25. package/dist/recovery/recovery-controller.js +30 -3
  26. package/dist/security/content-limits.cjs +86 -0
  27. package/dist/security/content-limits.d.cts +11 -0
  28. package/dist/security/content-limits.d.ts +11 -0
  29. package/dist/security/content-limits.js +84 -0
  30. package/dist/storage/verify-storage-adapter.cjs +125 -0
  31. package/dist/storage/verify-storage-adapter.d.cts +13 -0
  32. package/dist/storage/verify-storage-adapter.d.ts +13 -0
  33. package/dist/storage/verify-storage-adapter.js +125 -0
  34. package/dist/storage.cjs +2 -0
  35. package/dist/storage.d.cts +2 -1
  36. package/dist/storage.d.ts +2 -1
  37. package/dist/storage.js +2 -1
  38. package/package.json +5 -2
  39. package/schema/document-v1.schema.json +69 -0
@@ -1,27 +1,63 @@
1
1
  //#region src/history/history-stack.ts
2
- function createHistoryStack(limit) {
2
+ function bytesOfChange(change) {
3
+ let bytes = (change.order?.length ?? 0) * 8;
4
+ for (const [id, json] of change.objects) bytes += (id.length + (json?.length ?? 0)) * 2;
5
+ return bytes;
6
+ }
7
+ function estimateStepBytes(step) {
8
+ return bytesOfChange(step.before) + bytesOfChange(step.after) + step.label.length * 2;
9
+ }
10
+ function createHistoryStack(limits) {
3
11
  const undoSteps = [];
4
12
  const redoSteps = [];
5
- function pushWithinLimit(steps, step) {
13
+ const sizes = /* @__PURE__ */ new WeakMap();
14
+ let totalBytes = 0;
15
+ function sizeOf(step) {
16
+ let size = sizes.get(step);
17
+ if (size === void 0) {
18
+ size = estimateStepBytes(step);
19
+ sizes.set(step, size);
20
+ }
21
+ return size;
22
+ }
23
+ function forget(step) {
24
+ if (step) totalBytes -= sizeOf(step);
25
+ }
26
+ function stayWithinLimits() {
27
+ while (undoSteps.length > limits.steps) forget(undoSteps.shift());
28
+ while (redoSteps.length > limits.steps) forget(redoSteps.shift());
29
+ while (totalBytes > limits.bytes && undoSteps.length + redoSteps.length > 1) forget(undoSteps.length > 0 ? undoSteps.shift() : redoSteps.shift());
30
+ }
31
+ function add(steps, step) {
6
32
  steps.push(step);
7
- if (steps.length > limit) steps.shift();
33
+ totalBytes += sizeOf(step);
34
+ stayWithinLimits();
35
+ }
36
+ function take(steps) {
37
+ const step = steps.pop();
38
+ forget(step);
39
+ return step;
8
40
  }
9
41
  return {
10
42
  record(step) {
11
- pushWithinLimit(undoSteps, step);
43
+ redoSteps.forEach(forget);
12
44
  redoSteps.length = 0;
45
+ add(undoSteps, step);
13
46
  },
14
- takeUndo: () => undoSteps.pop(),
15
- takeRedo: () => redoSteps.pop(),
16
- returnUndo: (step) => pushWithinLimit(undoSteps, step),
17
- returnRedo: (step) => pushWithinLimit(redoSteps, step),
47
+ takeUndo: () => take(undoSteps),
48
+ takeRedo: () => take(redoSteps),
49
+ returnUndo: (step) => add(undoSteps, step),
50
+ returnRedo: (step) => add(redoSteps, step),
18
51
  undoLabels: () => undoSteps.map((step) => step.label).reverse(),
19
52
  redoLabels: () => redoSteps.map((step) => step.label).reverse(),
53
+ usedBytes: () => totalBytes,
20
54
  clear() {
21
55
  undoSteps.length = 0;
22
56
  redoSteps.length = 0;
57
+ totalBytes = 0;
23
58
  }
24
59
  };
25
60
  }
26
61
  //#endregion
27
62
  exports.createHistoryStack = createHistoryStack;
63
+ exports.estimateStepBytes = estimateStepBytes;
@@ -1,27 +1,62 @@
1
1
  //#region src/history/history-stack.ts
2
- function createHistoryStack(limit) {
2
+ function bytesOfChange(change) {
3
+ let bytes = (change.order?.length ?? 0) * 8;
4
+ for (const [id, json] of change.objects) bytes += (id.length + (json?.length ?? 0)) * 2;
5
+ return bytes;
6
+ }
7
+ function estimateStepBytes(step) {
8
+ return bytesOfChange(step.before) + bytesOfChange(step.after) + step.label.length * 2;
9
+ }
10
+ function createHistoryStack(limits) {
3
11
  const undoSteps = [];
4
12
  const redoSteps = [];
5
- function pushWithinLimit(steps, step) {
13
+ const sizes = /* @__PURE__ */ new WeakMap();
14
+ let totalBytes = 0;
15
+ function sizeOf(step) {
16
+ let size = sizes.get(step);
17
+ if (size === void 0) {
18
+ size = estimateStepBytes(step);
19
+ sizes.set(step, size);
20
+ }
21
+ return size;
22
+ }
23
+ function forget(step) {
24
+ if (step) totalBytes -= sizeOf(step);
25
+ }
26
+ function stayWithinLimits() {
27
+ while (undoSteps.length > limits.steps) forget(undoSteps.shift());
28
+ while (redoSteps.length > limits.steps) forget(redoSteps.shift());
29
+ while (totalBytes > limits.bytes && undoSteps.length + redoSteps.length > 1) forget(undoSteps.length > 0 ? undoSteps.shift() : redoSteps.shift());
30
+ }
31
+ function add(steps, step) {
6
32
  steps.push(step);
7
- if (steps.length > limit) steps.shift();
33
+ totalBytes += sizeOf(step);
34
+ stayWithinLimits();
35
+ }
36
+ function take(steps) {
37
+ const step = steps.pop();
38
+ forget(step);
39
+ return step;
8
40
  }
9
41
  return {
10
42
  record(step) {
11
- pushWithinLimit(undoSteps, step);
43
+ redoSteps.forEach(forget);
12
44
  redoSteps.length = 0;
45
+ add(undoSteps, step);
13
46
  },
14
- takeUndo: () => undoSteps.pop(),
15
- takeRedo: () => redoSteps.pop(),
16
- returnUndo: (step) => pushWithinLimit(undoSteps, step),
17
- returnRedo: (step) => pushWithinLimit(redoSteps, step),
47
+ takeUndo: () => take(undoSteps),
48
+ takeRedo: () => take(redoSteps),
49
+ returnUndo: (step) => add(undoSteps, step),
50
+ returnRedo: (step) => add(redoSteps, step),
18
51
  undoLabels: () => undoSteps.map((step) => step.label).reverse(),
19
52
  redoLabels: () => redoSteps.map((step) => step.label).reverse(),
53
+ usedBytes: () => totalBytes,
20
54
  clear() {
21
55
  undoSteps.length = 0;
22
56
  redoSteps.length = 0;
57
+ totalBytes = 0;
23
58
  }
24
59
  };
25
60
  }
26
61
  //#endregion
27
- export { createHistoryStack };
62
+ export { createHistoryStack, estimateStepBytes };
package/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_errors = require("./engine/errors.cjs");
3
+ const require_content_limits = require("./security/content-limits.cjs");
3
4
  const require_document_format = require("./document/document-format.cjs");
4
5
  const require_validate_document = require("./document/validate-document.cjs");
5
6
  const require_migrate_document = require("./migrations/migrate-document.cjs");
@@ -18,5 +19,8 @@ exports.createDocumentStateStore = require_document_state_store.createDocumentSt
18
19
  exports.detectSchemaVersion = require_migrate_document.detectSchemaVersion;
19
20
  exports.downloadExport = require_download_export.downloadExport;
20
21
  exports.isDocumentEngineError = require_errors.isDocumentEngineError;
22
+ exports.isSafeImageUrl = require_content_limits.isSafeImageUrl;
21
23
  exports.migrateDocument = require_migrate_document.migrateDocument;
24
+ exports.refuseUnsafeImageUrls = require_content_limits.refuseUnsafeImageUrls;
25
+ exports.secureDocument = require_content_limits.secureDocument;
22
26
  exports.validateDocument = require_validate_document.validateDocument;
package/dist/index.d.cts CHANGED
@@ -7,6 +7,7 @@ import { ExportPreflight, ExportProblem, ExportProblemCode } from "./export/pref
7
7
  import { NewDocumentOptions } from "./document/create-document.cjs";
8
8
  import { CustomObjectDefinition } from "./fabric/object-registry.cjs";
9
9
  import { HistoryOptions, HistoryState } from "./history/create-history.cjs";
10
+ import { ContentLimits, isSafeImageUrl, refuseUnsafeImageUrls, secureDocument } from "./security/content-limits.cjs";
10
11
  import { DocumentVersion, VersionKind, VersionOptions, VersionStorage, VersionSummary } from "./versions/document-version.cjs";
11
12
  import { RecoveryStore } from "./recovery/recovery-store.cjs";
12
13
  import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "./recovery/recovery-controller.cjs";
@@ -16,11 +17,11 @@ import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, Doc
16
17
  import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.cjs";
17
18
  import { SaveOptions, SaveRetryEvent, SaveState, SaveStatus } from "./save/save-controller.cjs";
18
19
  import { Unsubscribe } from "./engine/event-emitter.cjs";
19
- import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, ImportOptions, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.cjs";
20
+ import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, ImportOptions, LoadOptions, NewDocumentRequest, createDocumentEngine } from "./engine/create-document-engine.cjs";
20
21
  import { validateDocument } from "./document/validate-document.cjs";
21
22
  import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.cjs";
22
23
  import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.cjs";
23
24
  import { DownloadableExport, downloadExport } from "./export/download-export.cjs";
24
25
  import { Migration, MigrationContext, MigrationResult, detectSchemaVersion, migrateDocument } from "./migrations/migrate-document.cjs";
25
26
  import { DocumentState, DocumentStateStore, createDocumentStateStore } from "./state/document-state-store.cjs";
26
- export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentState, type DocumentStateStore, type DocumentStorage, type DocumentVersion, type DownloadableExport, type ExportArea, type ExportBackground, type ExportFormat, type ExportOptions, type ExportPreflight, type ExportProblem, type ExportProblemCode, type ExportRect, type ExportResult, type FabricDocument, type FontAsset, type FontLoader, type HistoryOptions, type HistoryState, type ImageAsset, type ImportOptions, type InterruptedLoad, type KeyboardShortcutOptions, type LoadOptions, type Migration, type MigrationContext, type MigrationResult, type NewDocumentOptions, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, type UploadRequest, type VersionKind, type VersionOptions, type VersionStorage, type VersionSummary, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, createDocumentStateStore, detectSchemaVersion, downloadExport, isDocumentEngineError, migrateDocument, validateDocument };
27
+ export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type ContentLimits, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentState, type DocumentStateStore, type DocumentStorage, type DocumentVersion, type DownloadableExport, type ExportArea, type ExportBackground, type ExportFormat, type ExportOptions, type ExportPreflight, type ExportProblem, type ExportProblemCode, type ExportRect, type ExportResult, type FabricDocument, type FontAsset, type FontLoader, type HistoryOptions, type HistoryState, type ImageAsset, type ImportOptions, type InterruptedLoad, type KeyboardShortcutOptions, type LoadOptions, type Migration, type MigrationContext, type MigrationResult, type NewDocumentOptions, type NewDocumentRequest, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, type UploadRequest, type VersionKind, type VersionOptions, type VersionStorage, type VersionSummary, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, createDocumentStateStore, detectSchemaVersion, downloadExport, isDocumentEngineError, isSafeImageUrl, migrateDocument, refuseUnsafeImageUrls, secureDocument, validateDocument };
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ import { ExportPreflight, ExportProblem, ExportProblemCode } from "./export/pref
7
7
  import { NewDocumentOptions } from "./document/create-document.js";
8
8
  import { CustomObjectDefinition } from "./fabric/object-registry.js";
9
9
  import { HistoryOptions, HistoryState } from "./history/create-history.js";
10
+ import { ContentLimits, isSafeImageUrl, refuseUnsafeImageUrls, secureDocument } from "./security/content-limits.js";
10
11
  import { DocumentVersion, VersionKind, VersionOptions, VersionStorage, VersionSummary } from "./versions/document-version.js";
11
12
  import { RecoveryStore } from "./recovery/recovery-store.js";
12
13
  import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "./recovery/recovery-controller.js";
@@ -16,11 +17,11 @@ import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, Doc
16
17
  import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.js";
17
18
  import { SaveOptions, SaveRetryEvent, SaveState, SaveStatus } from "./save/save-controller.js";
18
19
  import { Unsubscribe } from "./engine/event-emitter.js";
19
- import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, ImportOptions, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.js";
20
+ import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, ImportOptions, LoadOptions, NewDocumentRequest, createDocumentEngine } from "./engine/create-document-engine.js";
20
21
  import { validateDocument } from "./document/validate-document.js";
21
22
  import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
22
23
  import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
23
24
  import { DownloadableExport, downloadExport } from "./export/download-export.js";
24
25
  import { Migration, MigrationContext, MigrationResult, detectSchemaVersion, migrateDocument } from "./migrations/migrate-document.js";
25
26
  import { DocumentState, DocumentStateStore, createDocumentStateStore } from "./state/document-state-store.js";
26
- export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentState, type DocumentStateStore, type DocumentStorage, type DocumentVersion, type DownloadableExport, type ExportArea, type ExportBackground, type ExportFormat, type ExportOptions, type ExportPreflight, type ExportProblem, type ExportProblemCode, type ExportRect, type ExportResult, type FabricDocument, type FontAsset, type FontLoader, type HistoryOptions, type HistoryState, type ImageAsset, type ImportOptions, type InterruptedLoad, type KeyboardShortcutOptions, type LoadOptions, type Migration, type MigrationContext, type MigrationResult, type NewDocumentOptions, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, type UploadRequest, type VersionKind, type VersionOptions, type VersionStorage, type VersionSummary, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, createDocumentStateStore, detectSchemaVersion, downloadExport, isDocumentEngineError, migrateDocument, validateDocument };
27
+ export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type ContentLimits, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentState, type DocumentStateStore, type DocumentStorage, type DocumentVersion, type DownloadableExport, type ExportArea, type ExportBackground, type ExportFormat, type ExportOptions, type ExportPreflight, type ExportProblem, type ExportProblemCode, type ExportRect, type ExportResult, type FabricDocument, type FontAsset, type FontLoader, type HistoryOptions, type HistoryState, type ImageAsset, type ImportOptions, type InterruptedLoad, type KeyboardShortcutOptions, type LoadOptions, type Migration, type MigrationContext, type MigrationResult, type NewDocumentOptions, type NewDocumentRequest, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, type UploadRequest, type VersionKind, type VersionOptions, type VersionStorage, type VersionSummary, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, createDocumentStateStore, detectSchemaVersion, downloadExport, isDocumentEngineError, isSafeImageUrl, migrateDocument, refuseUnsafeImageUrls, secureDocument, validateDocument };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { DocumentEngineError, createConflictError, isDocumentEngineError } from "./engine/errors.js";
2
+ import { isSafeImageUrl, refuseUnsafeImageUrls, secureDocument } from "./security/content-limits.js";
2
3
  import { CURRENT_SCHEMA_VERSION } from "./document/document-format.js";
3
4
  import { validateDocument } from "./document/validate-document.js";
4
5
  import { detectSchemaVersion, migrateDocument } from "./migrations/migrate-document.js";
@@ -7,4 +8,4 @@ import { bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
7
8
  import { bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
8
9
  import { downloadExport } from "./export/download-export.js";
9
10
  import { createDocumentStateStore } from "./state/document-state-store.js";
10
- export { CURRENT_SCHEMA_VERSION, DocumentEngineError, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, createDocumentStateStore, detectSchemaVersion, downloadExport, isDocumentEngineError, migrateDocument, validateDocument };
11
+ export { CURRENT_SCHEMA_VERSION, DocumentEngineError, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, createDocumentStateStore, detectSchemaVersion, downloadExport, isDocumentEngineError, isSafeImageUrl, migrateDocument, refuseUnsafeImageUrls, secureDocument, validateDocument };
@@ -1,5 +1,25 @@
1
1
  const require_asset_pipeline = require("../assets/asset-pipeline.cjs");
2
2
  //#region src/recovery/recovery-controller.ts
3
+ async function toStoredFiles(files) {
4
+ const entries = await Promise.all(Object.entries(files).map(async ([url, blob]) => [url, {
5
+ type: blob.type,
6
+ bytes: await blob.arrayBuffer()
7
+ }]));
8
+ return Object.fromEntries(entries);
9
+ }
10
+ function fromStoredFiles(files) {
11
+ const restored = {};
12
+ for (const [url, file] of Object.entries(files ?? {})) restored[url] = file instanceof Blob ? file : new Blob([file.bytes], { type: file.type });
13
+ return restored;
14
+ }
15
+ function fromStoredRecord(value) {
16
+ if (!isRecoveryRecord(value)) return void 0;
17
+ const stored = value;
18
+ return {
19
+ ...stored,
20
+ files: fromStoredFiles(stored.files)
21
+ };
22
+ }
3
23
  const documentKeyPrefix = "document:";
4
24
  const unloadKeyPrefix = "unload:";
5
25
  const loadingKey = "loading";
@@ -79,7 +99,10 @@ function createRecoveryController(options) {
79
99
  if (!options.shouldWrite()) return;
80
100
  const document = options.createDocument();
81
101
  const record = createRecord(document, await captureTabOnlyFiles(document));
82
- await store.set(documentKey(document.id), record);
102
+ await store.set(documentKey(document.id), {
103
+ ...record,
104
+ files: await toStoredFiles(record.files)
105
+ });
83
106
  options.onCheckpoint(record);
84
107
  }
85
108
  function writeCheckpointBeforeThePageCloses() {
@@ -91,12 +114,15 @@ function createRecoveryController(options) {
91
114
  return;
92
115
  }
93
116
  const record = createRecord(document, Object.fromEntries(readyFiles));
94
- store.set(documentKey(document.id), record).then(() => options.onCheckpoint(record), options.onError);
117
+ toStoredFiles(record.files).then((files) => store.set(documentKey(document.id), {
118
+ ...record,
119
+ files
120
+ })).then(() => options.onCheckpoint(record), options.onError);
95
121
  }
96
122
  async function read(documentId) {
97
123
  await queue;
98
124
  const [checkpoint, unloadCopy] = await Promise.all([store.get(documentKey(documentId)), store.get(unloadKey(documentId))]);
99
- return newerRecord(isRecoveryRecord(checkpoint) ? checkpoint : void 0, isRecoveryRecord(unloadCopy) ? unloadCopy : void 0);
125
+ return newerRecord(fromStoredRecord(checkpoint), fromStoredRecord(unloadCopy));
100
126
  }
101
127
  function cancel() {
102
128
  if (timer !== void 0) clearTimeout(timer);
@@ -123,6 +149,7 @@ function createRecoveryController(options) {
123
149
  }, interval);
124
150
  },
125
151
  flush,
152
+ writeNow: writeCheckpointBeforeThePageCloses,
126
153
  cancel,
127
154
  remove: (documentId) => enqueue(async () => {
128
155
  await store.delete(unloadKey(documentId));
@@ -1,5 +1,25 @@
1
1
  import { rewriteUrls } from "../assets/asset-pipeline.js";
2
2
  //#region src/recovery/recovery-controller.ts
3
+ async function toStoredFiles(files) {
4
+ const entries = await Promise.all(Object.entries(files).map(async ([url, blob]) => [url, {
5
+ type: blob.type,
6
+ bytes: await blob.arrayBuffer()
7
+ }]));
8
+ return Object.fromEntries(entries);
9
+ }
10
+ function fromStoredFiles(files) {
11
+ const restored = {};
12
+ for (const [url, file] of Object.entries(files ?? {})) restored[url] = file instanceof Blob ? file : new Blob([file.bytes], { type: file.type });
13
+ return restored;
14
+ }
15
+ function fromStoredRecord(value) {
16
+ if (!isRecoveryRecord(value)) return void 0;
17
+ const stored = value;
18
+ return {
19
+ ...stored,
20
+ files: fromStoredFiles(stored.files)
21
+ };
22
+ }
3
23
  const documentKeyPrefix = "document:";
4
24
  const unloadKeyPrefix = "unload:";
5
25
  const loadingKey = "loading";
@@ -79,7 +99,10 @@ function createRecoveryController(options) {
79
99
  if (!options.shouldWrite()) return;
80
100
  const document = options.createDocument();
81
101
  const record = createRecord(document, await captureTabOnlyFiles(document));
82
- await store.set(documentKey(document.id), record);
102
+ await store.set(documentKey(document.id), {
103
+ ...record,
104
+ files: await toStoredFiles(record.files)
105
+ });
83
106
  options.onCheckpoint(record);
84
107
  }
85
108
  function writeCheckpointBeforeThePageCloses() {
@@ -91,12 +114,15 @@ function createRecoveryController(options) {
91
114
  return;
92
115
  }
93
116
  const record = createRecord(document, Object.fromEntries(readyFiles));
94
- store.set(documentKey(document.id), record).then(() => options.onCheckpoint(record), options.onError);
117
+ toStoredFiles(record.files).then((files) => store.set(documentKey(document.id), {
118
+ ...record,
119
+ files
120
+ })).then(() => options.onCheckpoint(record), options.onError);
95
121
  }
96
122
  async function read(documentId) {
97
123
  await queue;
98
124
  const [checkpoint, unloadCopy] = await Promise.all([store.get(documentKey(documentId)), store.get(unloadKey(documentId))]);
99
- return newerRecord(isRecoveryRecord(checkpoint) ? checkpoint : void 0, isRecoveryRecord(unloadCopy) ? unloadCopy : void 0);
125
+ return newerRecord(fromStoredRecord(checkpoint), fromStoredRecord(unloadCopy));
100
126
  }
101
127
  function cancel() {
102
128
  if (timer !== void 0) clearTimeout(timer);
@@ -123,6 +149,7 @@ function createRecoveryController(options) {
123
149
  }, interval);
124
150
  },
125
151
  flush,
152
+ writeNow: writeCheckpointBeforeThePageCloses,
126
153
  cancel,
127
154
  remove: (documentId) => enqueue(async () => {
128
155
  await store.delete(unloadKey(documentId));
@@ -0,0 +1,86 @@
1
+ const require_asset_references = require("../assets/asset-references.cjs");
2
+ const require_errors = require("../engine/errors.cjs");
3
+ //#region src/security/content-limits.ts
4
+ const dangerousKeys = /* @__PURE__ */ new Set([
5
+ "__proto__",
6
+ "constructor",
7
+ "prototype"
8
+ ]);
9
+ function isSafeImageUrl(url) {
10
+ const trimmed = url.trim().toLowerCase();
11
+ const scheme = /^([a-z][a-z0-9+.-]*):/.exec(trimmed)?.[1];
12
+ if (scheme === void 0) return true;
13
+ if (scheme === "http" || scheme === "https" || scheme === "blob") return true;
14
+ if (scheme === "data") return trimmed.startsWith("data:image/");
15
+ return false;
16
+ }
17
+ function withoutDangerousKeys(value) {
18
+ return JSON.parse(JSON.stringify(value), (key, nested) => dangerousKeys.has(key) ? void 0 : nested);
19
+ }
20
+ function deepestNesting(value) {
21
+ let deepest = 0;
22
+ const pending = [{
23
+ value,
24
+ depth: 1
25
+ }];
26
+ while (pending.length > 0) {
27
+ const { value: current, depth } = pending.pop();
28
+ if (typeof current !== "object" || current === null) continue;
29
+ deepest = Math.max(deepest, depth);
30
+ for (const child of Object.values(current)) pending.push({
31
+ value: child,
32
+ depth: depth + 1
33
+ });
34
+ }
35
+ return deepest;
36
+ }
37
+ function countObjects(objects) {
38
+ let count = 0;
39
+ const pending = [...objects];
40
+ while (pending.length > 0) {
41
+ const object = pending.pop();
42
+ count += 1;
43
+ if (Array.isArray(object.objects)) pending.push(...object.objects);
44
+ if (object.clipPath && typeof object.clipPath === "object") pending.push(object.clipPath);
45
+ }
46
+ return count;
47
+ }
48
+ function unsafe(path, message) {
49
+ return {
50
+ code: "UNSAFE_DOCUMENT",
51
+ path,
52
+ message
53
+ };
54
+ }
55
+ function secureDocument(input, limits = {}) {
56
+ if (typeof input !== "object" || input === null) return input;
57
+ const maxDepth = limits.maxDepth ?? 100;
58
+ const maxObjects = limits.maxObjects ?? 5e4;
59
+ const document = withoutDangerousKeys(input);
60
+ const issues = [];
61
+ const depth = deepestNesting(document);
62
+ if (depth > maxDepth) issues.push(unsafe("", `the document is nested ${depth} levels deep, the limit is ${maxDepth}`));
63
+ if (Array.isArray(document.objects) && issues.length === 0) {
64
+ const count = countObjects(document.objects.filter((object) => typeof object === "object" && object !== null));
65
+ if (count > maxObjects) issues.push(unsafe("objects", `the document has ${count} objects, the limit is ${maxObjects}`));
66
+ }
67
+ refuseWhenUnsafe(issues);
68
+ return document;
69
+ }
70
+ function refuseWhenUnsafe(issues) {
71
+ if (issues.length === 0) return;
72
+ throw new require_errors.DocumentEngineError("UNSAFE_DOCUMENT", `The document was refused: ${issues.map((issue) => issue.message).join("; ")}`, { issues });
73
+ }
74
+ function refuseUnsafeImageUrls(objects, isAllowedUrl = isSafeImageUrl) {
75
+ const issues = [];
76
+ for (const reference of require_asset_references.findImageReferences(objects)) {
77
+ if (isAllowedUrl(reference.url)) continue;
78
+ const where = reference.objectId ? `object ${reference.objectId}` : "an object";
79
+ issues.push(unsafe("objects", `${where} uses the image address "${reference.url.slice(0, 60)}", which is not allowed`));
80
+ }
81
+ refuseWhenUnsafe(issues);
82
+ }
83
+ //#endregion
84
+ exports.isSafeImageUrl = isSafeImageUrl;
85
+ exports.refuseUnsafeImageUrls = refuseUnsafeImageUrls;
86
+ exports.secureDocument = secureDocument;
@@ -0,0 +1,11 @@
1
+ import { SerializedFabricObject } from "../document/document-format.cjs";
2
+ //#region src/security/content-limits.d.ts
3
+ export interface ContentLimits {
4
+ maxObjects?: number;
5
+ maxDepth?: number;
6
+ isAllowedUrl?: (url: string) => boolean;
7
+ }
8
+ export declare function isSafeImageUrl(url: string): boolean;
9
+ export declare function secureDocument(input: unknown, limits?: ContentLimits): unknown;
10
+ export declare function refuseUnsafeImageUrls(objects: readonly SerializedFabricObject[], isAllowedUrl?: (url: string) => boolean): void;
11
+ //#endregion
@@ -0,0 +1,11 @@
1
+ import { SerializedFabricObject } from "../document/document-format.js";
2
+ //#region src/security/content-limits.d.ts
3
+ export interface ContentLimits {
4
+ maxObjects?: number;
5
+ maxDepth?: number;
6
+ isAllowedUrl?: (url: string) => boolean;
7
+ }
8
+ export declare function isSafeImageUrl(url: string): boolean;
9
+ export declare function secureDocument(input: unknown, limits?: ContentLimits): unknown;
10
+ export declare function refuseUnsafeImageUrls(objects: readonly SerializedFabricObject[], isAllowedUrl?: (url: string) => boolean): void;
11
+ //#endregion
@@ -0,0 +1,84 @@
1
+ import { findImageReferences } from "../assets/asset-references.js";
2
+ import { DocumentEngineError } from "../engine/errors.js";
3
+ //#region src/security/content-limits.ts
4
+ const dangerousKeys = /* @__PURE__ */ new Set([
5
+ "__proto__",
6
+ "constructor",
7
+ "prototype"
8
+ ]);
9
+ function isSafeImageUrl(url) {
10
+ const trimmed = url.trim().toLowerCase();
11
+ const scheme = /^([a-z][a-z0-9+.-]*):/.exec(trimmed)?.[1];
12
+ if (scheme === void 0) return true;
13
+ if (scheme === "http" || scheme === "https" || scheme === "blob") return true;
14
+ if (scheme === "data") return trimmed.startsWith("data:image/");
15
+ return false;
16
+ }
17
+ function withoutDangerousKeys(value) {
18
+ return JSON.parse(JSON.stringify(value), (key, nested) => dangerousKeys.has(key) ? void 0 : nested);
19
+ }
20
+ function deepestNesting(value) {
21
+ let deepest = 0;
22
+ const pending = [{
23
+ value,
24
+ depth: 1
25
+ }];
26
+ while (pending.length > 0) {
27
+ const { value: current, depth } = pending.pop();
28
+ if (typeof current !== "object" || current === null) continue;
29
+ deepest = Math.max(deepest, depth);
30
+ for (const child of Object.values(current)) pending.push({
31
+ value: child,
32
+ depth: depth + 1
33
+ });
34
+ }
35
+ return deepest;
36
+ }
37
+ function countObjects(objects) {
38
+ let count = 0;
39
+ const pending = [...objects];
40
+ while (pending.length > 0) {
41
+ const object = pending.pop();
42
+ count += 1;
43
+ if (Array.isArray(object.objects)) pending.push(...object.objects);
44
+ if (object.clipPath && typeof object.clipPath === "object") pending.push(object.clipPath);
45
+ }
46
+ return count;
47
+ }
48
+ function unsafe(path, message) {
49
+ return {
50
+ code: "UNSAFE_DOCUMENT",
51
+ path,
52
+ message
53
+ };
54
+ }
55
+ function secureDocument(input, limits = {}) {
56
+ if (typeof input !== "object" || input === null) return input;
57
+ const maxDepth = limits.maxDepth ?? 100;
58
+ const maxObjects = limits.maxObjects ?? 5e4;
59
+ const document = withoutDangerousKeys(input);
60
+ const issues = [];
61
+ const depth = deepestNesting(document);
62
+ if (depth > maxDepth) issues.push(unsafe("", `the document is nested ${depth} levels deep, the limit is ${maxDepth}`));
63
+ if (Array.isArray(document.objects) && issues.length === 0) {
64
+ const count = countObjects(document.objects.filter((object) => typeof object === "object" && object !== null));
65
+ if (count > maxObjects) issues.push(unsafe("objects", `the document has ${count} objects, the limit is ${maxObjects}`));
66
+ }
67
+ refuseWhenUnsafe(issues);
68
+ return document;
69
+ }
70
+ function refuseWhenUnsafe(issues) {
71
+ if (issues.length === 0) return;
72
+ throw new DocumentEngineError("UNSAFE_DOCUMENT", `The document was refused: ${issues.map((issue) => issue.message).join("; ")}`, { issues });
73
+ }
74
+ function refuseUnsafeImageUrls(objects, isAllowedUrl = isSafeImageUrl) {
75
+ const issues = [];
76
+ for (const reference of findImageReferences(objects)) {
77
+ if (isAllowedUrl(reference.url)) continue;
78
+ const where = reference.objectId ? `object ${reference.objectId}` : "an object";
79
+ issues.push(unsafe("objects", `${where} uses the image address "${reference.url.slice(0, 60)}", which is not allowed`));
80
+ }
81
+ refuseWhenUnsafe(issues);
82
+ }
83
+ //#endregion
84
+ export { isSafeImageUrl, refuseUnsafeImageUrls, secureDocument };