package platform import ( "encoding/json" "errors" "math" "reflect" "sort" "strings" ) const canvasModelFormat = 1 const canvasModelProtocol = 2 var errCanvasModel = errors.New("invalid_canvas_model") var errModelConflict = errors.New("canvas_model_field_conflict") type modelParent struct { CanvasID string `json:"canvasId"` EntryElementID string `json:"entryElementId"` } type modelCanvas struct { ID string `json:"id"` Parent *modelParent `json:"parent"` } type canvasModel struct { Root string Canvases map[string]modelCanvas Elements map[string]map[string]any Tombstones map[string]int64 Versions map[string]int64 } type modelView struct { Format int `json:"documentFormatVersion"` ProjectID string `json:"projectId"` Canvases []modelCanvas `json:"canvases"` Elements []map[string]any `json:"elements"` Tombstones map[string]int64 `json:"tombstones"` Versions map[string]int64 `json:"versions"` Complete bool `json:"complete"` Removed []string `json:"removedCanvasIds"` } type modelTarget struct { ID string `json:"id"` Incarnation int64 `json:"incarnation"` } type modelChange struct { Kind string `json:"kind"` ID string `json:"id,omitempty"` Target modelTarget `json:"target,omitempty"` Field string `json:"field,omitempty"` Before json.RawMessage `json:"before"` After json.RawMessage `json:"after"` BeforePresent *bool `json:"beforePresent,omitempty"` AfterPresent *bool `json:"afterPresent,omitempty"` } type modelOperation struct { ID string `json:"id"` Changes []modelChange `json:"changes"` } type modelBatch struct { ProtocolVersion int `json:"protocol_version"` RequestID string `json:"request_id"` Expected map[string]int64 `json:"expected_versions"` Operations []modelOperation `json:"operations"` } func emptyModel(root string) canvasModel { return canvasModel{Root: root, Canvases: map[string]modelCanvas{}, Elements: map[string]map[string]any{}, Tombstones: map[string]int64{}, Versions: map[string]int64{}} } func modelString(value any) string { text, _ := value.(string); return text } func modelInt(value any) int64 { number, ok := value.(float64) if !ok || number < 1 || number > 9007199254740991 || math.Trunc(number) != number { return 0 } return int64(number) } func modelEqual(left, right any) bool { // Round-trip numeric Go structs and JSON records to the same JSON value domain. a, ea := json.Marshal(left) b, eb := json.Marshal(right) if ea != nil || eb != nil { return false } var av, bv any return json.Unmarshal(a, &av) == nil && json.Unmarshal(b, &bv) == nil && reflect.DeepEqual(av, bv) } func (model canvasModel) view(participants map[string]int64) modelView { view := modelView{Format: canvasModelFormat, ProjectID: model.Root, Canvases: []modelCanvas{}, Elements: []map[string]any{}, Tombstones: map[string]int64{}, Versions: map[string]int64{}, Complete: participants == nil, Removed: []string{}} for _, id := range sortedKeys(model.Canvases) { if participants != nil { if _, exists := participants[id]; !exists { continue } } view.Canvases = append(view.Canvases, model.Canvases[id]) view.Versions[id] = model.Versions[id] } for _, id := range sortedKeys(model.Elements) { element := model.Elements[id] if participants != nil { if _, exists := participants[modelString(element["ownerCanvasId"])]; !exists { continue } } view.Elements = append(view.Elements, element) } // Tombstones are small identity facts, not payloads; include them for safe incarnation checks. for id, incarnation := range model.Tombstones { view.Tombstones[id] = incarnation } for _, id := range sortedKeys(model.Versions) { if participants != nil { if _, exists := participants[id]; !exists { continue } } view.Versions[id] = model.Versions[id] if _, exists := model.Canvases[id]; !exists { view.Removed = append(view.Removed, id) } } return view } func sortedKeys[T any](values map[string]T) []string { keys := make([]string, 0, len(values)) for key := range values { keys = append(keys, key) } sort.Strings(keys) return keys } func cloneModel(model canvasModel) canvasModel { raw, _ := json.Marshal(model) var copy canvasModel _ = json.Unmarshal(raw, ©) return copy } func modelAncestors(model canvasModel, id string) ([]string, error) { path, seen := []string{}, map[string]bool{} for id != "" { canvas, exists := model.Canvases[id] if !exists || seen[id] || len(path) > 8 { return nil, errCanvasModel } seen[id] = true path = append(path, id) id = "" if canvas.Parent != nil { id = canvas.Parent.CanvasID } } return path, nil } func modelCommonCanvas(model canvasModel, left, right string) (string, error) { a, err := modelAncestors(model, left) if err != nil { return "", err } b, err := modelAncestors(model, right) if err != nil { return "", err } seen := map[string]bool{} for _, id := range a { seen[id] = true } for _, id := range b { if seen[id] { return id, nil } } return "", errCanvasModel } func validateCanvasModel(model canvasModel) error { if !uuidSlugPattern.MatchString(model.Root) || len(model.Canvases) == 0 || len(model.Canvases) > 256 || len(model.Elements) > 128000 { return errCanvasModel } roots, entries, counts := 0, map[string]bool{}, map[string][2]int{} for id, canvas := range model.Canvases { if id != canvas.ID || !uuidSlugPattern.MatchString(id) { return errCanvasModel } if _, err := modelAncestors(model, id); err != nil { return err } if canvas.Parent == nil { roots++ if id != model.Root { return errCanvasModel } continue } entry := model.Elements[canvas.Parent.EntryElementID] if entry == nil || entry["structure"] != "node" || entry["ownerCanvasId"] != canvas.Parent.CanvasID || entries[canvas.Parent.EntryElementID] { return errCanvasModel } entries[canvas.Parent.EntryElementID] = true } if roots != 1 { return errCanvasModel } for id, incarnation := range model.Tombstones { if !canvasID.MatchString(id) || incarnation < 1 || incarnation > 9007199254740991 { return errCanvasModel } } for id, element := range model.Elements { owner, component := modelString(element["ownerCanvasId"]), modelString(element["componentId"]) _, canvasExists := model.Canvases[owner] incarnation, version := modelInt(element["incarnation"]), modelInt(element["dataVersion"]) data, dataOK := element["data"].(map[string]any) if !canvasID.MatchString(id) || element["id"] != id || !canvasExists || incarnation == 0 || incarnation <= model.Tombstones[id] || version == 0 || component == "" || len(component) > 128 || !dataOK || data == nil { return errCanvasModel } encoded, err := json.Marshal(element) if err != nil || len(encoded) > 1<<20 { return errCanvasModel } count := counts[owner] switch element["structure"] { case "node": count[0]++ geometry, ok := element["geometry"].(map[string]any) if !ok || !canvasNumber(geometry["x"], -1e6, 1e6) || !canvasNumber(geometry["y"], -1e6, 1e6) || !canvasNumber(geometry["width"], 0.000001, 1e6) || !canvasNumber(geometry["height"], 0.000001, 1e6) || !canvasNumber(geometry["rotation"], 0, math.Nextafter(360, 0)) { return errCanvasModel } if version == 1 && canvasChoice(component, "text", "image", "script", "plain-text", "plain-image", "icon", "frame") { if !canvasString(data["title"], 300) || !canvasString(data["text"], 100000) || component == "frame" && geometry["rotation"] != float64(0) { return errCanvasModel } if ai, exists := data["ai"]; exists && !validCanvasAI(ai, component) { return errCanvasModel } if asset, exists := data["assetId"]; exists && (!canvasChoice(component, "image", "plain-image") || !uuidSlugPattern.MatchString(modelString(asset))) { return errCanvasModel } } case "connection": count[1]++ source, sourceOK := element["source"].(map[string]any) target, targetOK := element["target"].(map[string]any) if !sourceOK || !targetOK { return errCanvasModel } a, b := model.Elements[modelString(source["elementId"])], model.Elements[modelString(target["elementId"])] if a == nil || b == nil || a["structure"] != "node" || b["structure"] != "node" { return errCanvasModel } for _, endpoint := range []map[string]any{source, target} { if port, exists := endpoint["portId"]; exists && (!canvasString(port, 256) || modelString(port) == "") { return errCanvasModel } } common, err := modelCommonCanvas(model, modelString(a["ownerCanvasId"]), modelString(b["ownerCanvasId"])) if err != nil || common != owner { return errCanvasModel } default: return errCanvasModel } if count[0] > 500 || count[1] > 2000 { return errGraphCapacity } counts[owner] = count } return nil } // Pure transaction reducer. Unknown component data/envelope fields are retained. func applyModelOperations(initial canvasModel, operations []modelOperation) (canvasModel, map[string]bool, error) { model, touched, seen := cloneModel(initial), map[string]bool{}, map[string]bool{} if len(operations) == 0 || len(operations) > 2500 { return model, nil, errCanvasModel } changeCount := 0 for _, operation := range operations { if !uuidSlugPattern.MatchString(operation.ID) || seen[operation.ID] || len(operation.Changes) == 0 { return model, nil, errCanvasModel } seen[operation.ID] = true for _, change := range operation.Changes { changeCount++ if changeCount > 10000 { return model, nil, errGraphCapacity } var before, after any if json.Unmarshal(change.Before, &before) != nil || json.Unmarshal(change.After, &after) != nil { return model, nil, errCanvasModel } switch change.Kind { case "field": element := model.Elements[change.Target.ID] if element == nil || modelInt(element["incarnation"]) != change.Target.Incarnation { return model, nil, errModelConflict } touched[modelString(element["ownerCanvasId"])] = true if strings.HasPrefix(change.Field, "data.") { key := strings.TrimPrefix(change.Field, "data.") if key == "" || canvasChoice(key, "__proto__", "constructor", "prototype") { return model, nil, errCanvasModel } data := element["data"].(map[string]any) prior, present := data[key] if present != (change.BeforePresent == nil || *change.BeforePresent) || !modelEqual(prior, before) { return model, nil, errModelConflict } if change.AfterPresent != nil && !*change.AfterPresent { delete(data, key) } else { data[key] = after } } else { if !canvasChoice(change.Field, "geometry", "ownerCanvasId", "source", "target", "path") { return model, nil, errCanvasModel } if !modelEqual(element[change.Field], before) { return model, nil, errModelConflict } element[change.Field] = after if change.Field == "ownerCanvasId" { touched[modelString(after)] = true } } case "element": if !canvasID.MatchString(change.ID) || !modelEqual(model.Elements[change.ID], before) { return model, nil, errModelConflict } if prior := model.Elements[change.ID]; prior != nil { touched[modelString(prior["ownerCanvasId"])] = true } if after == nil { if prior := model.Elements[change.ID]; prior != nil { model.Tombstones[change.ID] = modelInt(prior["incarnation"]) } delete(model.Elements, change.ID) } else { record, ok := after.(map[string]any) if !ok || record["id"] != change.ID { return model, nil, errCanvasModel } if before == nil && modelInt(record["incarnation"]) != model.Tombstones[change.ID]+1 { return model, nil, errModelConflict } if prior := model.Elements[change.ID]; prior != nil && modelInt(record["incarnation"]) != modelInt(prior["incarnation"]) { return model, nil, errModelConflict } model.Elements[change.ID] = record touched[modelString(record["ownerCanvasId"])] = true } case "canvas": prior, exists := model.Canvases[change.ID] var priorValue any if exists { priorValue = prior } if !uuidSlugPattern.MatchString(change.ID) || !modelEqual(priorValue, before) { return model, nil, errModelConflict } touched[change.ID] = true if prior.Parent != nil { touched[prior.Parent.CanvasID] = true } if after == nil { delete(model.Canvases, change.ID) } else { var canvas modelCanvas if json.Unmarshal(change.After, &canvas) != nil || canvas.ID != change.ID { return model, nil, errCanvasModel } model.Canvases[change.ID] = canvas if canvas.Parent != nil { touched[canvas.Parent.CanvasID] = true } } default: return model, nil, errCanvasModel } } } if err := validateCanvasModel(model); err != nil { return model, nil, err } // Endpoint owners are read participants, guarding against delete/restore races in child canvases. for _, operation := range operations { for _, change := range operation.Changes { if change.Kind == "element" && string(change.Before) == "null" || change.Kind == "field" && (change.Field == "source" || change.Field == "target") { id := change.ID if change.Kind == "field" { id = change.Target.ID } element := model.Elements[id] if element != nil && element["structure"] == "connection" { for _, field := range []string{"source", "target"} { endpoint := element[field].(map[string]any) touched[modelString(model.Elements[modelString(endpoint["elementId"])]["ownerCanvasId"])] = true } } } } } for id := range touched { model.Versions[id] = initial.Versions[id] + 1 } return model, touched, nil }