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.
- package/README.md +45 -91
- package/dist/assets/asset-pipeline.cjs +3 -1
- package/dist/assets/asset-pipeline.js +3 -1
- package/dist/engine/create-document-engine.cjs +16 -6
- package/dist/engine/create-document-engine.d.cts +7 -1
- package/dist/engine/create-document-engine.d.ts +7 -1
- package/dist/engine/create-document-engine.js +17 -7
- package/dist/engine/errors.d.cts +1 -1
- package/dist/engine/errors.d.ts +1 -1
- package/dist/export/render-export.cjs +3 -15
- package/dist/export/render-export.js +4 -15
- package/dist/history/apply-state.cjs +3 -1
- package/dist/history/apply-state.js +3 -1
- package/dist/history/create-history.cjs +4 -1
- package/dist/history/create-history.d.cts +1 -0
- package/dist/history/create-history.d.ts +1 -0
- package/dist/history/create-history.js +4 -1
- package/dist/history/history-stack.cjs +44 -8
- package/dist/history/history-stack.js +44 -9
- package/dist/index.cjs +4 -0
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/recovery/recovery-controller.cjs +30 -3
- package/dist/recovery/recovery-controller.js +30 -3
- package/dist/security/content-limits.cjs +86 -0
- package/dist/security/content-limits.d.cts +11 -0
- package/dist/security/content-limits.d.ts +11 -0
- package/dist/security/content-limits.js +84 -0
- package/dist/storage/verify-storage-adapter.cjs +125 -0
- package/dist/storage/verify-storage-adapter.d.cts +13 -0
- package/dist/storage/verify-storage-adapter.d.ts +13 -0
- package/dist/storage/verify-storage-adapter.js +125 -0
- package/dist/storage.cjs +2 -0
- package/dist/storage.d.cts +2 -1
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +2 -1
- package/package.json +5 -2
- package/schema/document-v1.schema.json +69 -0
|
@@ -1,27 +1,63 @@
|
|
|
1
1
|
//#region src/history/history-stack.ts
|
|
2
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
redoSteps.forEach(forget);
|
|
12
44
|
redoSteps.length = 0;
|
|
45
|
+
add(undoSteps, step);
|
|
13
46
|
},
|
|
14
|
-
takeUndo: () => undoSteps
|
|
15
|
-
takeRedo: () => redoSteps
|
|
16
|
-
returnUndo: (step) =>
|
|
17
|
-
returnRedo: (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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
redoSteps.forEach(forget);
|
|
12
44
|
redoSteps.length = 0;
|
|
45
|
+
add(undoSteps, step);
|
|
13
46
|
},
|
|
14
|
-
takeUndo: () => undoSteps
|
|
15
|
-
takeRedo: () => redoSteps
|
|
16
|
-
returnUndo: (step) =>
|
|
17
|
-
returnRedo: (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),
|
|
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
|
-
|
|
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(
|
|
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),
|
|
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
|
-
|
|
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(
|
|
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 };
|