@lupinum/board-core 1.0.0-beta.1 → 1.0.0-beta.2
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/dist/{chunk-45V474TA.js → chunk-A7SXOVDB.js} +49 -7
- package/dist/helpers/json.d.ts +9 -3
- package/dist/index.js +102 -48
- package/dist/internal.js +1 -1
- package/package.json +1 -1
|
@@ -124,7 +124,29 @@ var BoardDestroyedError = class extends BoardError {
|
|
|
124
124
|
};
|
|
125
125
|
|
|
126
126
|
// src/helpers/json.ts
|
|
127
|
-
|
|
127
|
+
var DEFAULT_MAX_JSON_DEPTH = 64;
|
|
128
|
+
var DEFAULT_MAX_JSON_VALUES = 25e4;
|
|
129
|
+
var DEFAULT_MAX_JSON_STRING_CHARACTERS = 8e6;
|
|
130
|
+
function createJsonValueBudget() {
|
|
131
|
+
return {
|
|
132
|
+
remainingValues: DEFAULT_MAX_JSON_VALUES,
|
|
133
|
+
remainingStringCharacters: DEFAULT_MAX_JSON_STRING_CHARACTERS
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function consumeBudget(budget, path, stringCharacters = 0) {
|
|
137
|
+
budget.remainingValues -= 1;
|
|
138
|
+
budget.remainingStringCharacters -= stringCharacters;
|
|
139
|
+
if (budget.remainingValues < 0 || budget.remainingStringCharacters < 0) {
|
|
140
|
+
throw new BoardInputError(`${path} exceeds the supported JSON size.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function freezeJsonValue(value, path, ancestors = /* @__PURE__ */ new Set(), budget = createJsonValueBudget(), depth = 0) {
|
|
144
|
+
if (depth > DEFAULT_MAX_JSON_DEPTH) {
|
|
145
|
+
throw new BoardInputError(
|
|
146
|
+
`${path} exceeds the supported JSON nesting depth.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
consumeBudget(budget, path, typeof value === "string" ? value.length : 0);
|
|
128
150
|
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
129
151
|
return value;
|
|
130
152
|
}
|
|
@@ -161,7 +183,13 @@ function freezeJsonValue(value, path, ancestors = /* @__PURE__ */ new Set()) {
|
|
|
161
183
|
);
|
|
162
184
|
}
|
|
163
185
|
entries2.push(
|
|
164
|
-
freezeJsonValue(
|
|
186
|
+
freezeJsonValue(
|
|
187
|
+
descriptor.value,
|
|
188
|
+
`${path}[${index}]`,
|
|
189
|
+
ancestors,
|
|
190
|
+
budget,
|
|
191
|
+
depth + 1
|
|
192
|
+
)
|
|
165
193
|
);
|
|
166
194
|
}
|
|
167
195
|
return Object.freeze(entries2);
|
|
@@ -181,9 +209,19 @@ function freezeJsonValue(value, path, ancestors = /* @__PURE__ */ new Set()) {
|
|
|
181
209
|
`${path}.${key} must be an enumerable data property.`
|
|
182
210
|
);
|
|
183
211
|
}
|
|
212
|
+
budget.remainingStringCharacters -= key.length;
|
|
213
|
+
if (budget.remainingStringCharacters < 0) {
|
|
214
|
+
throw new BoardInputError(`${path} exceeds the supported JSON size.`);
|
|
215
|
+
}
|
|
184
216
|
entries.push([
|
|
185
217
|
key,
|
|
186
|
-
freezeJsonValue(
|
|
218
|
+
freezeJsonValue(
|
|
219
|
+
descriptor.value,
|
|
220
|
+
`${path}.${key}`,
|
|
221
|
+
ancestors,
|
|
222
|
+
budget,
|
|
223
|
+
depth + 1
|
|
224
|
+
)
|
|
187
225
|
]);
|
|
188
226
|
}
|
|
189
227
|
return Object.freeze(Object.fromEntries(entries));
|
|
@@ -191,17 +229,20 @@ function freezeJsonValue(value, path, ancestors = /* @__PURE__ */ new Set()) {
|
|
|
191
229
|
ancestors.delete(value);
|
|
192
230
|
}
|
|
193
231
|
}
|
|
194
|
-
function freezeJsonObject(value, path) {
|
|
195
|
-
const cloned = freezeJsonValue(value, path);
|
|
232
|
+
function freezeJsonObject(value, path, budget = createJsonValueBudget()) {
|
|
233
|
+
const cloned = freezeJsonValue(value, path, /* @__PURE__ */ new Set(), budget);
|
|
196
234
|
if (Array.isArray(cloned) || cloned === null || typeof cloned !== "object") {
|
|
197
235
|
throw new BoardInputError(`${path} must be a JSON object.`);
|
|
198
236
|
}
|
|
199
237
|
return cloned;
|
|
200
238
|
}
|
|
201
|
-
function collectJsonObjectExtras(value, knownKeys, path = "JSON object") {
|
|
239
|
+
function collectJsonObjectExtras(value, knownKeys, path = "JSON object", budget = createJsonValueBudget()) {
|
|
202
240
|
return Object.freeze(
|
|
203
241
|
Object.fromEntries(
|
|
204
|
-
Object.entries(value).filter(([key]) => !knownKeys.has(key)).map(([key, entry]) => [
|
|
242
|
+
Object.entries(value).filter(([key]) => !knownKeys.has(key)).map(([key, entry]) => [
|
|
243
|
+
key,
|
|
244
|
+
freezeJsonValue(entry, `${path}.${key}`, /* @__PURE__ */ new Set(), budget)
|
|
245
|
+
])
|
|
205
246
|
)
|
|
206
247
|
);
|
|
207
248
|
}
|
|
@@ -289,6 +330,7 @@ export {
|
|
|
289
330
|
BoardNotFoundError,
|
|
290
331
|
BoardConflictError,
|
|
291
332
|
BoardDestroyedError,
|
|
333
|
+
createJsonValueBudget,
|
|
292
334
|
freezeJsonValue,
|
|
293
335
|
freezeJsonObject,
|
|
294
336
|
collectJsonObjectExtras,
|
package/dist/helpers/json.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { JsonObject, JsonValue } from '../types.js';
|
|
2
|
+
export interface JsonValueBudget {
|
|
3
|
+
remainingValues: number;
|
|
4
|
+
remainingStringCharacters: number;
|
|
5
|
+
}
|
|
6
|
+
/** Create one shared resource budget for an untrusted JSON input boundary. */
|
|
7
|
+
export declare function createJsonValueBudget(): JsonValueBudget;
|
|
2
8
|
/** Clone, validate, and freeze a JSON-compatible value at an input boundary. */
|
|
3
|
-
export declare function freezeJsonValue(value: unknown, path: string, ancestors?: Set<object
|
|
9
|
+
export declare function freezeJsonValue(value: unknown, path: string, ancestors?: Set<object>, budget?: JsonValueBudget, depth?: number): JsonValue;
|
|
4
10
|
/** Clone, validate, and freeze a JSON object at an input boundary. */
|
|
5
|
-
export declare function freezeJsonObject(value: unknown, path: string): JsonObject;
|
|
11
|
+
export declare function freezeJsonObject(value: unknown, path: string, budget?: JsonValueBudget): JsonObject;
|
|
6
12
|
/** Retain unrecognized JSON fields without allowing them to override known fields. */
|
|
7
|
-
export declare function collectJsonObjectExtras(value: Readonly<Record<string, unknown>>, knownKeys: ReadonlySet<string>, path?: string): JsonObject;
|
|
13
|
+
export declare function collectJsonObjectExtras(value: Readonly<Record<string, unknown>>, knownKeys: ReadonlySet<string>, path?: string, budget?: JsonValueBudget): JsonObject;
|
package/dist/index.js
CHANGED
|
@@ -6,13 +6,14 @@ import {
|
|
|
6
6
|
BoardNotFoundError,
|
|
7
7
|
assertInternalBoardPlugin,
|
|
8
8
|
collectJsonObjectExtras,
|
|
9
|
+
createJsonValueBudget,
|
|
9
10
|
freezeClone,
|
|
10
11
|
freezeJsonObject,
|
|
11
12
|
readonlyMapView,
|
|
12
13
|
readonlySetView,
|
|
13
14
|
registerBoardInteractionAdapter,
|
|
14
15
|
sameArray
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-A7SXOVDB.js";
|
|
16
17
|
|
|
17
18
|
// src/math.ts
|
|
18
19
|
function clamp(value, min, max) {
|
|
@@ -699,6 +700,7 @@ function validateState(state, grid, context) {
|
|
|
699
700
|
}
|
|
700
701
|
zIndexes.add(node.zIndex);
|
|
701
702
|
}
|
|
703
|
+
validateParentCycles(state, push);
|
|
702
704
|
for (const id of state.selection.values()) {
|
|
703
705
|
if (!state.nodes.has(id)) {
|
|
704
706
|
push("selection.exists", `Selected node ${id} does not exist.`);
|
|
@@ -755,28 +757,26 @@ function validateNodeParent(node, state, push) {
|
|
|
755
757
|
`Node ${node.id} parent must be type "group", got "${parent.type}".`
|
|
756
758
|
);
|
|
757
759
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
);
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
if (!walk.parentId) {
|
|
777
|
-
break;
|
|
760
|
+
}
|
|
761
|
+
function validateParentCycles(state, push) {
|
|
762
|
+
const complete = /* @__PURE__ */ new Set();
|
|
763
|
+
for (const start of state.nodes.keys()) {
|
|
764
|
+
if (complete.has(start)) continue;
|
|
765
|
+
const path = [];
|
|
766
|
+
const pathIndexes = /* @__PURE__ */ new Map();
|
|
767
|
+
let current = start;
|
|
768
|
+
while (current !== void 0 && !complete.has(current)) {
|
|
769
|
+
const cycleStart = pathIndexes.get(current);
|
|
770
|
+
if (cycleStart !== void 0) {
|
|
771
|
+
const cycle = [...path.slice(cycleStart), current].join(" -> ");
|
|
772
|
+
push("node.parentId", `Cycle detected in parent chain: ${cycle}.`);
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
pathIndexes.set(current, path.length);
|
|
776
|
+
path.push(current);
|
|
777
|
+
current = state.nodes.get(current)?.parentId;
|
|
778
778
|
}
|
|
779
|
-
|
|
779
|
+
for (const id of path) complete.add(id);
|
|
780
780
|
}
|
|
781
781
|
}
|
|
782
782
|
|
|
@@ -1543,6 +1543,9 @@ var JSON_CANVAS_SIDES = /* @__PURE__ */ new Set([
|
|
|
1543
1543
|
]);
|
|
1544
1544
|
var JSON_CANVAS_EDGE_ENDS = /* @__PURE__ */ new Set(["none", "arrow"]);
|
|
1545
1545
|
var JSON_CANVAS_BACKGROUND_STYLES = /* @__PURE__ */ new Set(["cover", "ratio", "repeat"]);
|
|
1546
|
+
var MAX_DOCUMENT_NODES = 1e4;
|
|
1547
|
+
var MAX_DOCUMENT_EDGES = 2e4;
|
|
1548
|
+
var MAX_DOCUMENT_SELECTION = 1e4;
|
|
1546
1549
|
var LEGACY_BOARD_METADATA_KEY = "x-vue-board";
|
|
1547
1550
|
var DOCUMENT_FIELDS = /* @__PURE__ */ new Set([
|
|
1548
1551
|
"nodes",
|
|
@@ -1855,12 +1858,6 @@ function validateDocumentMetadata(metadata) {
|
|
|
1855
1858
|
`Invalid board document: metadata for edge "${id}" has invalid data.`
|
|
1856
1859
|
);
|
|
1857
1860
|
}
|
|
1858
|
-
if (edge.data !== void 0) {
|
|
1859
|
-
freezeJsonObject(
|
|
1860
|
-
edge.data,
|
|
1861
|
-
`Invalid board document: metadata for edge "${id}" data`
|
|
1862
|
-
);
|
|
1863
|
-
}
|
|
1864
1861
|
}
|
|
1865
1862
|
}
|
|
1866
1863
|
}
|
|
@@ -1873,6 +1870,19 @@ function normalizeDocumentForImport(raw) {
|
|
|
1873
1870
|
if (!Array.isArray(parsed.nodes)) {
|
|
1874
1871
|
throw new BoardInputError("Invalid board document: missing nodes array.");
|
|
1875
1872
|
}
|
|
1873
|
+
if (parsed.nodes.length > MAX_DOCUMENT_NODES) {
|
|
1874
|
+
throw new BoardInputError(
|
|
1875
|
+
`Invalid board document: nodes exceed the supported limit of ${MAX_DOCUMENT_NODES}.`
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
if (parsed.edges !== void 0 && !Array.isArray(parsed.edges)) {
|
|
1879
|
+
throw new BoardInputError("Invalid board document: edges must be an array.");
|
|
1880
|
+
}
|
|
1881
|
+
if (parsed.edges && parsed.edges.length > MAX_DOCUMENT_EDGES) {
|
|
1882
|
+
throw new BoardInputError(
|
|
1883
|
+
`Invalid board document: edges exceed the supported limit of ${MAX_DOCUMENT_EDGES}.`
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1876
1886
|
for (const key of [
|
|
1877
1887
|
"camera",
|
|
1878
1888
|
"grid",
|
|
@@ -1888,10 +1898,29 @@ function normalizeDocumentForImport(raw) {
|
|
|
1888
1898
|
}
|
|
1889
1899
|
}
|
|
1890
1900
|
const rawMetadata = getDocumentMetadata(parsed);
|
|
1901
|
+
if (isRecord(rawMetadata)) {
|
|
1902
|
+
if (Array.isArray(rawMetadata.selection) && rawMetadata.selection.length > MAX_DOCUMENT_SELECTION) {
|
|
1903
|
+
throw new BoardInputError(
|
|
1904
|
+
`Invalid board document: selection exceeds the supported limit of ${MAX_DOCUMENT_SELECTION}.`
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
if (isRecord(rawMetadata.nodes) && Reflect.ownKeys(rawMetadata.nodes).length > MAX_DOCUMENT_NODES) {
|
|
1908
|
+
throw new BoardInputError(
|
|
1909
|
+
`Invalid board document: node metadata exceeds the supported limit of ${MAX_DOCUMENT_NODES}.`
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
if (isRecord(rawMetadata.edges) && Reflect.ownKeys(rawMetadata.edges).length > MAX_DOCUMENT_EDGES) {
|
|
1913
|
+
throw new BoardInputError(
|
|
1914
|
+
`Invalid board document: edge metadata exceeds the supported limit of ${MAX_DOCUMENT_EDGES}.`
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1891
1918
|
validateDocumentMetadata(rawMetadata);
|
|
1919
|
+
const jsonBudget = createJsonValueBudget();
|
|
1892
1920
|
const metadata = rawMetadata === void 0 ? void 0 : freezeJsonObject(
|
|
1893
1921
|
rawMetadata,
|
|
1894
|
-
"Invalid board document: board metadata"
|
|
1922
|
+
"Invalid board document: board metadata",
|
|
1923
|
+
jsonBudget
|
|
1895
1924
|
);
|
|
1896
1925
|
const seenNodes = /* @__PURE__ */ new Set();
|
|
1897
1926
|
const nodes = parsed.nodes.map((node) => {
|
|
@@ -1917,7 +1946,8 @@ function normalizeDocumentForImport(raw) {
|
|
|
1917
1946
|
}
|
|
1918
1947
|
const normalized = freezeJsonObject(
|
|
1919
1948
|
node,
|
|
1920
|
-
`Invalid board document: node "${String(node.id ?? "?")}"
|
|
1949
|
+
`Invalid board document: node "${String(node.id ?? "?")}"`,
|
|
1950
|
+
jsonBudget
|
|
1921
1951
|
);
|
|
1922
1952
|
validateJsonCanvasNodeFields(normalized);
|
|
1923
1953
|
if (seenNodes.has(normalized.id)) {
|
|
@@ -1930,11 +1960,6 @@ function normalizeDocumentForImport(raw) {
|
|
|
1930
1960
|
});
|
|
1931
1961
|
let edges;
|
|
1932
1962
|
if (parsed.edges !== void 0) {
|
|
1933
|
-
if (!Array.isArray(parsed.edges)) {
|
|
1934
|
-
throw new BoardInputError(
|
|
1935
|
-
"Invalid board document: edges must be an array."
|
|
1936
|
-
);
|
|
1937
|
-
}
|
|
1938
1963
|
const seenEdges = /* @__PURE__ */ new Set();
|
|
1939
1964
|
edges = parsed.edges.map((edge) => {
|
|
1940
1965
|
if (!isRecord(edge)) {
|
|
@@ -1988,7 +2013,8 @@ function normalizeDocumentForImport(raw) {
|
|
|
1988
2013
|
}
|
|
1989
2014
|
return freezeJsonObject(
|
|
1990
2015
|
{ ...edge, id, fromNode, toNode },
|
|
1991
|
-
`Invalid board document: edge "${id}"
|
|
2016
|
+
`Invalid board document: edge "${id}"`,
|
|
2017
|
+
jsonBudget
|
|
1992
2018
|
);
|
|
1993
2019
|
});
|
|
1994
2020
|
}
|
|
@@ -1996,7 +2022,8 @@ function normalizeDocumentForImport(raw) {
|
|
|
1996
2022
|
...collectJsonObjectExtras(
|
|
1997
2023
|
parsedRecord,
|
|
1998
2024
|
DOCUMENT_FIELDS,
|
|
1999
|
-
"Invalid board document"
|
|
2025
|
+
"Invalid board document",
|
|
2026
|
+
jsonBudget
|
|
2000
2027
|
),
|
|
2001
2028
|
nodes,
|
|
2002
2029
|
...edges !== void 0 ? { edges } : {},
|
|
@@ -2033,12 +2060,13 @@ function documentToSnapshot(document) {
|
|
|
2033
2060
|
jsonNodeToBoardNode(node, metadata?.nodes?.[node.id], index)
|
|
2034
2061
|
)
|
|
2035
2062
|
);
|
|
2063
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
2036
2064
|
const gridSettings = {
|
|
2037
2065
|
...DEFAULT_GRID,
|
|
2038
2066
|
...metadata?.grid ?? {}
|
|
2039
2067
|
};
|
|
2040
2068
|
const selection = Array.isArray(metadata?.selection) ? metadata.selection.filter(
|
|
2041
|
-
(id) => typeof id === "string" &&
|
|
2069
|
+
(id) => typeof id === "string" && nodeIds.has(id)
|
|
2042
2070
|
).map((id) => id) : [];
|
|
2043
2071
|
const nextZIndex = metadata?.nextZIndex ?? nodes.reduce((max, node) => Math.max(max, node.zIndex), 0) + 1;
|
|
2044
2072
|
const camera = { ...DEFAULT_CAMERA, ...metadata?.camera ?? {} };
|
|
@@ -3002,11 +3030,23 @@ function createBoardEngine(options = {}) {
|
|
|
3002
3030
|
notifySnapGuidesChanged();
|
|
3003
3031
|
return idMap;
|
|
3004
3032
|
}
|
|
3033
|
+
const reservedIds = new Set(state.nodes.keys());
|
|
3034
|
+
for (const rawNode of snapshotNodes) {
|
|
3035
|
+
let id = rawNode.id;
|
|
3036
|
+
while (reservedIds.has(id)) id = createNodeId();
|
|
3037
|
+
reservedIds.add(id);
|
|
3038
|
+
idMap.set(rawNode.id, id);
|
|
3039
|
+
}
|
|
3005
3040
|
for (const rawNode of snapshotNodes) {
|
|
3006
3041
|
const node = normalizeExistingNode(rawNode);
|
|
3007
|
-
const id =
|
|
3008
|
-
|
|
3009
|
-
|
|
3042
|
+
const id = idMap.get(node.id);
|
|
3043
|
+
const parentId = node.parentId ? idMap.get(node.parentId) ?? node.parentId : void 0;
|
|
3044
|
+
state.nodes.set(id, {
|
|
3045
|
+
...node,
|
|
3046
|
+
id,
|
|
3047
|
+
parentId,
|
|
3048
|
+
zIndex: state.nextZIndex++
|
|
3049
|
+
});
|
|
3010
3050
|
}
|
|
3011
3051
|
state.jsonCanvas = mergeJsonCanvasExtras(
|
|
3012
3052
|
state.jsonCanvas,
|
|
@@ -3677,13 +3717,27 @@ function createBoardEngine(options = {}) {
|
|
|
3677
3717
|
return runCommand("pasteData", [payload, offset], () => {
|
|
3678
3718
|
const inputs = options.clipboard?.deserialize(payload) ?? null;
|
|
3679
3719
|
if (!inputs || inputs.length === 0) return null;
|
|
3680
|
-
const
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3720
|
+
const stagedNodes = new Map(state.nodes);
|
|
3721
|
+
let nextZIndex = state.nextZIndex;
|
|
3722
|
+
const created = inputs.map((input) => {
|
|
3723
|
+
const normalized = normalizeNodeInput(
|
|
3724
|
+
{
|
|
3725
|
+
...input,
|
|
3726
|
+
x: input.x ?? offset.x,
|
|
3727
|
+
y: input.y ?? offset.y
|
|
3728
|
+
},
|
|
3729
|
+
{
|
|
3730
|
+
nodes: stagedNodes,
|
|
3731
|
+
grid,
|
|
3732
|
+
constraints: nodeConstraints,
|
|
3733
|
+
nextZIndex
|
|
3734
|
+
}
|
|
3735
|
+
);
|
|
3736
|
+
nextZIndex = normalized.nextZIndex;
|
|
3737
|
+
stagedNodes.set(normalized.node.id, normalized.node);
|
|
3738
|
+
return normalized.node;
|
|
3739
|
+
});
|
|
3740
|
+
state.nextZIndex = nextZIndex;
|
|
3687
3741
|
for (const node of created) {
|
|
3688
3742
|
state.nodes.set(node.id, node);
|
|
3689
3743
|
emit("node:created", materializeNode2(node));
|
package/dist/internal.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lupinum/board-core",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
4
|
"description": "Headless node-based board engine for spatial editors, diagramming tools, and whiteboard-style interfaces.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Lupinum OG <info@lupinum.com> (https://lupinum.com)",
|