@kubuild/core 0.6.0 → 0.8.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/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ __export(index_exports, {
30
30
  DocumentHistoryManager: () => DocumentHistoryManager,
31
31
  HistoryEngine: () => HistoryEngine,
32
32
  MigrationRegistry: () => MigrationRegistry,
33
+ NODE_ID_REFERENCE_KEYS: () => NODE_ID_REFERENCE_KEYS,
33
34
  RuntimeStateStore: () => RuntimeStateStore,
34
35
  addArtboard: () => addArtboard,
35
36
  applyFieldTransform: () => applyFieldTransform,
@@ -37,7 +38,9 @@ __export(index_exports, {
37
38
  buildSampleVariablesFromCatalog: () => buildSampleVariablesFromCatalog,
38
39
  calculateChecksum: () => calculateChecksum,
39
40
  canMigrate: () => canMigrate,
41
+ canonicalizeTextPropsInPlace: () => canonicalizeTextPropsInPlace,
40
42
  checkZipBomb: () => checkZipBomb,
43
+ cloneNodeTreeWithFreshIds: () => cloneNodeTreeWithFreshIds,
41
44
  cloneTemplateAsPage: () => cloneTemplateAsPage,
42
45
  cloneTreeWithNewIds: () => cloneTreeWithNewIds,
43
46
  collectArtboardReferenceNodes: () => collectArtboardReferenceNodes,
@@ -54,6 +57,7 @@ __export(index_exports, {
54
57
  createPageArtboard: () => createPageArtboard,
55
58
  createRuntimeStore: () => createRuntimeStore,
56
59
  createTemplateRecord: () => createTemplateRecord,
60
+ createTrackingRelayHandler: () => createTrackingRelayHandler,
57
61
  deepClone: () => deepClone,
58
62
  defaultIdGenerator: () => defaultIdGenerator,
59
63
  defaultMigrationRegistry: () => defaultMigrationRegistry,
@@ -113,6 +117,7 @@ __export(index_exports, {
113
117
  previewImportPackage: () => previewImportPackage,
114
118
  remapAssetReferences: () => remapAssetReferences,
115
119
  remapDocumentAssetReferences: () => remapDocumentAssetReferences,
120
+ remapNodeReferences: () => remapNodeReferences,
116
121
  removeArtboard: () => removeArtboard,
117
122
  removeNode: () => removeNode,
118
123
  renameArtboard: () => renameArtboard,
@@ -120,6 +125,7 @@ __export(index_exports, {
120
125
  resolveBinding: () => resolveBinding,
121
126
  resolveBindingValue: () => resolveBindingValue,
122
127
  resolvePropertyPath: () => resolvePropertyPath,
128
+ sanitizeDocumentTracking: () => sanitizeDocumentTracking,
123
129
  sanitizeFilename: () => sanitizeFilename,
124
130
  sanitizeHtml: () => sanitizeHtml,
125
131
  sanitizeUrl: () => sanitizeUrl,
@@ -130,6 +136,8 @@ __export(index_exports, {
130
136
  sendTikTokEventsApi: () => sendTikTokEventsApi,
131
137
  setActiveArtboard: () => setActiveArtboard,
132
138
  sha256Sync: () => sha256Sync,
139
+ stripDocumentTrackingSecretsInPlace: () => stripDocumentTrackingSecretsInPlace,
140
+ stripTrackingSecretsInPlace: () => stripTrackingSecretsInPlace,
133
141
  ungroupNodeFrame: () => ungroupNodeFrame,
134
142
  updateActions: () => updateActions,
135
143
  updateAnimation: () => updateAnimation,
@@ -137,6 +145,7 @@ __export(index_exports, {
137
145
  updateFormConfig: () => updateFormConfig,
138
146
  updateProps: () => updateProps,
139
147
  updateStyle: () => updateStyle,
148
+ updateTheme: () => updateTheme,
140
149
  validateDocument: () => validateDocument,
141
150
  validateDocumentSecurity: () => validateDocumentSecurity,
142
151
  validateFieldValue: () => validateFieldValue,
@@ -155,7 +164,7 @@ var import_schema = require("@kubuild/schema");
155
164
  function createBlankDocument(title = "Untitled Page") {
156
165
  return {
157
166
  schema: import_schema.SCHEMA_NAME,
158
- version: "1.0.0",
167
+ version: import_schema.CURRENT_SCHEMA_VERSION,
159
168
  metadata: {
160
169
  title,
161
170
  description: "",
@@ -468,26 +477,84 @@ function defaultIdGenerator(oldId, existingIds) {
468
477
  }
469
478
  return candidate;
470
479
  }
471
- function cloneTreeWithNewIds(root, idGenerator, existingIds) {
480
+ var NODE_ID_REFERENCE_KEYS = /* @__PURE__ */ new Set([
481
+ "modalId",
482
+ "modalNodeId",
483
+ "targetModalId",
484
+ "targetNodeId",
485
+ "nodeId",
486
+ "formId",
487
+ "targetId"
488
+ ]);
489
+ var ANCHOR_REFERENCE_KEYS = /* @__PURE__ */ new Set(["url", "href"]);
490
+ function remapReferenceValue(key, value, idMap) {
491
+ if (typeof value !== "string") return value;
492
+ if (NODE_ID_REFERENCE_KEYS.has(key)) {
493
+ return idMap.get(value) ?? idMap.get(value.trim()) ?? value;
494
+ }
495
+ if (ANCHOR_REFERENCE_KEYS.has(key) && value.startsWith("#")) {
496
+ const mapped = idMap.get(value.slice(1));
497
+ return mapped ? `#${mapped}` : value;
498
+ }
499
+ return value;
500
+ }
501
+ function remapReferenceRecord(record, idMap) {
502
+ for (const [key, value] of Object.entries(record)) {
503
+ record[key] = remapReferenceValue(key, value, idMap);
504
+ }
505
+ }
506
+ function remapStepReferences(steps, idMap) {
507
+ if (!Array.isArray(steps)) return;
508
+ for (const step of steps) {
509
+ if (step.payload && typeof step.payload === "object") {
510
+ remapReferenceRecord(step.payload, idMap);
511
+ }
512
+ remapStepReferences(step.onSuccess, idMap);
513
+ remapStepReferences(step.onError, idMap);
514
+ }
515
+ }
516
+ function remapNodeReferences(node, idMap) {
517
+ if (node.props && typeof node.props === "object") {
518
+ remapReferenceRecord(node.props, idMap);
519
+ const legacyAction = node.props.action;
520
+ if (legacyAction && typeof legacyAction === "object" && legacyAction.payload && typeof legacyAction.payload === "object" && !Array.isArray(legacyAction.payload)) {
521
+ remapReferenceRecord(legacyAction.payload, idMap);
522
+ }
523
+ }
524
+ if (node.formConfig && typeof node.formConfig.formId === "string") {
525
+ node.formConfig.formId = idMap.get(node.formConfig.formId) ?? node.formConfig.formId;
526
+ }
527
+ if (Array.isArray(node.actions)) {
528
+ for (const pipeline of node.actions) {
529
+ remapStepReferences(pipeline.steps, idMap);
530
+ }
531
+ }
532
+ }
533
+ function cloneNodeTreeWithFreshIds(root, idGen) {
472
534
  const idMap = /* @__PURE__ */ new Map();
473
- const idGen = idGenerator || ((oldId) => defaultIdGenerator(oldId, existingIds));
535
+ const clonedNodes = [];
474
536
  function cloneRecursive(node) {
475
- const newId = idGen(node.id);
537
+ const newId = idGen(node.id, node);
476
538
  idMap.set(node.id, newId);
477
- const clonedProps = node.props ? deepClone(node.props) : void 0;
478
- const clonedStyles = node.styles ? deepClone(node.styles) : void 0;
479
- const clonedChildren = node.children ? node.children.map((child) => cloneRecursive(child)) : [];
480
- return {
539
+ const { children, ...rest } = node;
540
+ const cloned = {
541
+ ...deepClone(rest),
481
542
  id: newId,
482
- type: node.type,
483
- ...clonedProps ? { props: clonedProps } : {},
484
- ...clonedStyles ? { styles: clonedStyles } : {},
485
- children: clonedChildren
543
+ children: children ? children.map((child) => cloneRecursive(child)) : []
486
544
  };
545
+ clonedNodes.push(cloned);
546
+ return cloned;
487
547
  }
488
548
  const clonedNode = cloneRecursive(root);
549
+ for (const cloned of clonedNodes) {
550
+ remapNodeReferences(cloned, idMap);
551
+ }
489
552
  return { clonedNode, idMap };
490
553
  }
554
+ function cloneTreeWithNewIds(root, idGenerator, existingIds) {
555
+ const idGen = idGenerator || ((oldId) => defaultIdGenerator(oldId, existingIds));
556
+ return cloneNodeTreeWithFreshIds(root, (oldId) => idGen(oldId));
557
+ }
491
558
 
492
559
  // src/document/commands.ts
493
560
  var import_schema3 = require("@kubuild/schema");
@@ -1029,6 +1096,53 @@ function replaceNode(document, params) {
1029
1096
  }
1030
1097
  };
1031
1098
  }
1099
+ function updateTheme(document, params) {
1100
+ const { theme, merge = true } = params;
1101
+ const newDoc = deepClone(document);
1102
+ const previousTheme = newDoc.theme ? deepClone(newDoc.theme) : void 0;
1103
+ if (theme === null) {
1104
+ delete newDoc.theme;
1105
+ } else {
1106
+ const next = {};
1107
+ for (const group of import_schema3.THEME_TOKEN_GROUPS) {
1108
+ const base = merge ? { ...previousTheme?.[group] ?? {} } : {};
1109
+ const patch = theme[group];
1110
+ if (patch === null) {
1111
+ continue;
1112
+ }
1113
+ if (patch) {
1114
+ for (const [key, value] of Object.entries(patch)) {
1115
+ if (value === null) {
1116
+ delete base[key];
1117
+ } else {
1118
+ base[key] = value;
1119
+ }
1120
+ }
1121
+ }
1122
+ if (Object.keys(base).length > 0) {
1123
+ next[group] = base;
1124
+ }
1125
+ }
1126
+ const parsed = import_schema3.ThemeSchema.parse(next);
1127
+ if (Object.keys(parsed).length > 0) {
1128
+ newDoc.theme = parsed;
1129
+ } else {
1130
+ delete newDoc.theme;
1131
+ }
1132
+ }
1133
+ return {
1134
+ document: newDoc,
1135
+ event: {
1136
+ type: "THEME_UPDATED",
1137
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1138
+ nodeId: newDoc.document.id,
1139
+ payload: {
1140
+ theme: newDoc.theme,
1141
+ previousTheme
1142
+ }
1143
+ }
1144
+ };
1145
+ }
1032
1146
 
1033
1147
  // src/document/artboards.ts
1034
1148
  var import_schema4 = require("@kubuild/schema");
@@ -1551,6 +1665,15 @@ var DocumentHistoryManager = class {
1551
1665
  var FORBIDDEN_KEY_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1552
1666
  function resolveBinding(binding, context) {
1553
1667
  const segments = binding.key.split(".").filter(Boolean);
1668
+ if (segments.some((segment) => FORBIDDEN_KEY_SEGMENTS.has(segment))) {
1669
+ return applyMissingPolicy(binding);
1670
+ }
1671
+ if (context?.variables && typeof context.variables === "object" && Object.prototype.hasOwnProperty.call(context.variables, binding.key)) {
1672
+ const directValue = context.variables[binding.key];
1673
+ if (directValue !== void 0 && typeof directValue !== "function") {
1674
+ return { status: "resolved", value: directValue };
1675
+ }
1676
+ }
1554
1677
  let current = context?.variables;
1555
1678
  for (const segment of segments) {
1556
1679
  if (FORBIDDEN_KEY_SEGMENTS.has(segment)) {
@@ -2122,14 +2245,14 @@ var ActionPipelineExecutor = class {
2122
2245
  async executeSingleStep(step, context, parentSignal, options) {
2123
2246
  const stepStartTime = Date.now();
2124
2247
  if (step.condition && !evaluateActionCondition(step.condition, context)) {
2125
- const skippedResult = {
2248
+ const skippedResult2 = {
2126
2249
  stepId: step.id,
2127
2250
  stepType: step.type,
2128
2251
  status: "skipped",
2129
2252
  durationMs: Date.now() - stepStartTime
2130
2253
  };
2131
- options?.onStepComplete?.(step, skippedResult, context);
2132
- return skippedResult;
2254
+ options?.onStepComplete?.(step, skippedResult2, context);
2255
+ return skippedResult2;
2133
2256
  }
2134
2257
  options?.onStepStart?.(step, context);
2135
2258
  const stepTimeout = step.timeout;
@@ -2621,7 +2744,7 @@ function generateTrackingEventId(prefix = "evt") {
2621
2744
  const randomPart = Math.random().toString(36).substring(2, 10);
2622
2745
  return `${prefix}_${timestamp}_${randomPart}`;
2623
2746
  }
2624
- async function sendMetaCapiEvent(event, config, options) {
2747
+ async function sendMetaCapiEvent(event, config, secrets, options) {
2625
2748
  const fetcher = options?.fetchFn || globalThis.fetch;
2626
2749
  if (!config.pixelId) {
2627
2750
  return {
@@ -2695,9 +2818,10 @@ async function sendMetaCapiEvent(event, config, options) {
2695
2818
  const headers = {
2696
2819
  "Content-Type": "application/json"
2697
2820
  };
2698
- if (config.capiAccessToken) {
2699
- headers["Authorization"] = `Bearer ${config.capiAccessToken}`;
2821
+ if (!secrets?.capiAccessToken) {
2822
+ return skippedResult("meta", "No Meta CAPI access token resolved for this credential");
2700
2823
  }
2824
+ headers["Authorization"] = `Bearer ${secrets.capiAccessToken}`;
2701
2825
  const res = await fetcher(url, {
2702
2826
  method: "POST",
2703
2827
  headers,
@@ -2727,7 +2851,7 @@ async function sendMetaCapiEvent(event, config, options) {
2727
2851
  };
2728
2852
  }
2729
2853
  }
2730
- async function sendTikTokEventsApi(event, config, options) {
2854
+ async function sendTikTokEventsApi(event, config, secrets, options) {
2731
2855
  const fetcher = options?.fetchFn || globalThis.fetch;
2732
2856
  if (!config.pixelId) {
2733
2857
  return { provider: "tiktok", success: false, error: "TikTok Pixel ID is missing" };
@@ -2777,9 +2901,10 @@ async function sendTikTokEventsApi(event, config, options) {
2777
2901
  const headers = {
2778
2902
  "Content-Type": "application/json"
2779
2903
  };
2780
- if (config.accessToken) {
2781
- headers["Access-Token"] = config.accessToken;
2904
+ if (!secrets?.accessToken) {
2905
+ return skippedResult("tiktok", "No TikTok Events API access token resolved for this credential");
2782
2906
  }
2907
+ headers["Access-Token"] = secrets.accessToken;
2783
2908
  const res = await fetcher(url, {
2784
2909
  method: "POST",
2785
2910
  headers,
@@ -2804,7 +2929,7 @@ async function sendTikTokEventsApi(event, config, options) {
2804
2929
  };
2805
2930
  }
2806
2931
  }
2807
- async function sendGa4MeasurementEvent(event, config, options) {
2932
+ async function sendGa4MeasurementEvent(event, config, secrets, options) {
2808
2933
  const fetcher = options?.fetchFn || globalThis.fetch;
2809
2934
  if (!config.measurementId) {
2810
2935
  return { provider: "google", success: false, error: "GA4 Measurement ID is missing" };
@@ -2828,12 +2953,10 @@ async function sendGa4MeasurementEvent(event, config, options) {
2828
2953
  return { provider: "google", success: true, data: { simulated: true, payload: ga4Payload } };
2829
2954
  }
2830
2955
  try {
2831
- let url = `https://www.google-analytics.com/mp/collect?measurement_id=${encodeURIComponent(
2832
- config.measurementId
2833
- )}`;
2834
- if (config.measurementProtocolSecret) {
2835
- url += `&api_secret=${encodeURIComponent(config.measurementProtocolSecret)}`;
2956
+ if (!secrets?.measurementProtocolSecret) {
2957
+ return skippedResult("google", "No GA4 Measurement Protocol API secret resolved for this credential");
2836
2958
  }
2959
+ const url = `https://www.google-analytics.com/mp/collect?measurement_id=${encodeURIComponent(config.measurementId)}&api_secret=${encodeURIComponent(secrets.measurementProtocolSecret)}`;
2837
2960
  const res = await fetcher(url, {
2838
2961
  method: "POST",
2839
2962
  headers: { "Content-Type": "application/json" },
@@ -2853,11 +2976,8 @@ async function sendGa4MeasurementEvent(event, config, options) {
2853
2976
  };
2854
2977
  }
2855
2978
  }
2856
- async function sendCustomWebhookEvent(event, config, options) {
2979
+ async function sendCustomWebhookEvent(event, _config, secrets, options) {
2857
2980
  const fetcher = options?.fetchFn || globalThis.fetch;
2858
- if (!config.endpointUrl) {
2859
- return { provider: "custom", success: false, error: "Custom Webhook URL is missing" };
2860
- }
2861
2981
  const webhookPayload = {
2862
2982
  event: event.eventName,
2863
2983
  eventId: event.eventId,
@@ -2871,12 +2991,15 @@ async function sendCustomWebhookEvent(event, config, options) {
2871
2991
  options.onLog?.("[Custom Webhook SIMULATED]", webhookPayload);
2872
2992
  return { provider: "custom", success: true, data: { simulated: true, payload: webhookPayload } };
2873
2993
  }
2994
+ if (!secrets?.endpointUrl) {
2995
+ return skippedResult("custom", "No custom webhook destination resolved for this credential");
2996
+ }
2874
2997
  try {
2875
- const res = await fetcher(config.endpointUrl, {
2998
+ const res = await fetcher(secrets.endpointUrl, {
2876
2999
  method: "POST",
2877
3000
  headers: {
2878
3001
  "Content-Type": "application/json",
2879
- ...config.headers || {}
3002
+ ...secrets.headers || {}
2880
3003
  },
2881
3004
  body: JSON.stringify(webhookPayload)
2882
3005
  });
@@ -2895,6 +3018,9 @@ async function sendCustomWebhookEvent(event, config, options) {
2895
3018
  };
2896
3019
  }
2897
3020
  }
3021
+ function skippedResult(provider, reason) {
3022
+ return { provider, success: true, skipped: true, reason };
3023
+ }
2898
3024
  async function dispatchServerTracking(event, config, options) {
2899
3025
  const eventId = event.eventId || generateTrackingEventId();
2900
3026
  const eventWithId = { ...event, eventId };
@@ -2919,66 +3045,231 @@ async function dispatchServerTracking(event, config, options) {
2919
3045
  options?.onLog?.(msg, data);
2920
3046
  }
2921
3047
  };
3048
+ const target = options?.provider || "all";
3049
+ const wants = (key) => target === "all" || target === key;
2922
3050
  const providers = config.providers || {};
2923
- const dispatchPromises = [];
2924
- if (providers.meta && providers.meta.enabled !== false && providers.meta.capiEnabled) {
2925
- dispatchPromises.push(
2926
- sendMetaCapiEvent(eventWithId, providers.meta, mergedOptions).then((result) => ({
2927
- key: "meta",
2928
- result
2929
- }))
2930
- );
3051
+ const tasks = [];
3052
+ const meta = providers.meta;
3053
+ if (wants("meta") && meta && meta.enabled !== false && meta.capiEnabled) {
3054
+ tasks.push({
3055
+ key: "meta",
3056
+ credentialId: meta.credentialId,
3057
+ run: (s) => sendMetaCapiEvent(eventWithId, meta, s, mergedOptions)
3058
+ });
2931
3059
  }
2932
- if (providers.tiktok && providers.tiktok.enabled !== false && providers.tiktok.eventsApiEnabled) {
2933
- dispatchPromises.push(
2934
- sendTikTokEventsApi(eventWithId, providers.tiktok, mergedOptions).then((result) => ({
2935
- key: "tiktok",
2936
- result
2937
- }))
2938
- );
3060
+ const tiktok = providers.tiktok;
3061
+ if (wants("tiktok") && tiktok && tiktok.enabled !== false && tiktok.eventsApiEnabled) {
3062
+ tasks.push({
3063
+ key: "tiktok",
3064
+ credentialId: tiktok.credentialId,
3065
+ run: (s) => sendTikTokEventsApi(eventWithId, tiktok, s, mergedOptions)
3066
+ });
2939
3067
  }
2940
- if (providers.google && providers.google.enabled !== false && providers.google.measurementId && providers.google.measurementProtocolSecret) {
2941
- dispatchPromises.push(
2942
- sendGa4MeasurementEvent(eventWithId, providers.google, mergedOptions).then((result) => ({
2943
- key: "google",
2944
- result
2945
- }))
2946
- );
3068
+ const google = providers.google;
3069
+ if (wants("google") && google && google.enabled !== false && google.measurementId) {
3070
+ tasks.push({
3071
+ key: "google",
3072
+ credentialId: google.credentialId,
3073
+ run: (s) => sendGa4MeasurementEvent(eventWithId, google, s, mergedOptions)
3074
+ });
2947
3075
  }
2948
- if (providers.custom && providers.custom.enabled !== false && providers.custom.endpointUrl) {
2949
- dispatchPromises.push(
2950
- sendCustomWebhookEvent(eventWithId, providers.custom, mergedOptions).then((result) => ({
2951
- key: "custom",
2952
- result
2953
- }))
2954
- );
3076
+ const custom = providers.custom;
3077
+ if (wants("custom") && custom && custom.enabled !== false) {
3078
+ tasks.push({
3079
+ key: "custom",
3080
+ credentialId: custom.credentialId,
3081
+ run: (s) => sendCustomWebhookEvent(eventWithId, custom, s, mergedOptions)
3082
+ });
3083
+ }
3084
+ if (tasks.length === 0) {
3085
+ return {
3086
+ success: true,
3087
+ eventId,
3088
+ skipped: true,
3089
+ reason: target === "gtm" ? "GTM is client-side only (dataLayer); no server delivery" : "No server-side tracking provider is enabled",
3090
+ results: {}
3091
+ };
2955
3092
  }
2956
- const settled = await Promise.allSettled(dispatchPromises);
3093
+ const resolve = options?.resolveSecrets;
3094
+ const runTask = async (task) => {
3095
+ let secrets = null;
3096
+ if (resolve) {
3097
+ try {
3098
+ secrets = await resolve({ provider: task.key, credentialId: task.credentialId, documentId: options?.documentId }) ?? null;
3099
+ } catch (err) {
3100
+ return {
3101
+ provider: task.key,
3102
+ success: false,
3103
+ error: `Secret resolution failed: ${err instanceof Error ? err.message : String(err)}`
3104
+ };
3105
+ }
3106
+ }
3107
+ if (!secrets && !simulate) {
3108
+ const reason = resolve ? `No secret resolved for provider "${task.key}"${task.credentialId ? ` (credentialId "${task.credentialId}")` : " (no credentialId linked)"}` : "No secret resolver configured (options.resolveSecrets)";
3109
+ mergedOptions.onLog?.(`[${task.key}] skipped: ${reason}`);
3110
+ return skippedResult(task.key, reason);
3111
+ }
3112
+ return task.run(secrets);
3113
+ };
3114
+ const settled = await Promise.allSettled(tasks.map((task) => runTask(task)));
2957
3115
  const results = {};
2958
3116
  let overallSuccess = true;
2959
- for (const item of settled) {
3117
+ let delivered = 0;
3118
+ settled.forEach((item, index) => {
3119
+ const key = tasks[index].key;
2960
3120
  if (item.status === "fulfilled") {
2961
- results[item.value.key] = item.value.result;
2962
- if (!item.value.result.success) {
2963
- overallSuccess = false;
2964
- }
3121
+ results[key] = item.value;
3122
+ if (!item.value.success) overallSuccess = false;
3123
+ if (!item.value.skipped) delivered++;
2965
3124
  } else {
2966
3125
  overallSuccess = false;
3126
+ results[key] = {
3127
+ provider: key,
3128
+ success: false,
3129
+ error: item.reason instanceof Error ? item.reason.message : String(item.reason)
3130
+ };
2967
3131
  }
2968
- }
3132
+ });
3133
+ const allSkipped = delivered === 0 && overallSuccess;
2969
3134
  return {
2970
3135
  success: overallSuccess,
2971
3136
  eventId,
3137
+ ...allSkipped ? { skipped: true, reason: "All server providers were skipped" } : {},
2972
3138
  results
2973
3139
  };
2974
3140
  }
2975
3141
 
3142
+ // src/runtime/tracking-relay.ts
3143
+ var import_schema5 = require("@kubuild/schema");
3144
+ var DEFAULT_MAX_BODY_BYTES = 64 * 1024;
3145
+ function defaultClientIp(request) {
3146
+ const forwarded = request.headers.get("x-forwarded-for");
3147
+ if (forwarded) {
3148
+ const first = forwarded.split(",")[0]?.trim();
3149
+ if (first) return first;
3150
+ }
3151
+ return request.headers.get("cf-connecting-ip")?.trim() || request.headers.get("x-real-ip")?.trim() || void 0;
3152
+ }
3153
+ function isOriginAllowed(origin, allowed) {
3154
+ if (!allowed) return true;
3155
+ if (!origin) return false;
3156
+ if (typeof allowed === "function") return allowed(origin);
3157
+ return allowed.includes(origin);
3158
+ }
3159
+ function createTrackingRelayHandler(options) {
3160
+ const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
3161
+ return async (request) => {
3162
+ const origin = request.headers.get("origin");
3163
+ const corsHeaders = {};
3164
+ if (options.allowedOrigins && origin && isOriginAllowed(origin, options.allowedOrigins)) {
3165
+ corsHeaders["Access-Control-Allow-Origin"] = origin;
3166
+ corsHeaders["Vary"] = "Origin";
3167
+ }
3168
+ const json = (body, status, extra) => new Response(JSON.stringify(body), {
3169
+ status,
3170
+ headers: { "Content-Type": "application/json", ...corsHeaders, ...extra || {} }
3171
+ });
3172
+ const fail = (code, message, status, extra) => json({ version: import_schema5.TRACKING_RELAY_PROTOCOL_VERSION, success: false, error: { code, message } }, status, extra);
3173
+ if (!isOriginAllowed(origin, options.allowedOrigins)) {
3174
+ return fail("FORBIDDEN_ORIGIN", "Origin is not allowed", 403);
3175
+ }
3176
+ if (request.method === "OPTIONS") {
3177
+ return new Response(null, {
3178
+ status: 204,
3179
+ headers: {
3180
+ ...corsHeaders,
3181
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
3182
+ "Access-Control-Allow-Headers": "Content-Type",
3183
+ "Access-Control-Max-Age": "600"
3184
+ }
3185
+ });
3186
+ }
3187
+ if (request.method !== "POST") {
3188
+ return fail("METHOD_NOT_ALLOWED", "Only POST requests are accepted", 405, { Allow: "POST, OPTIONS" });
3189
+ }
3190
+ try {
3191
+ const text = await request.text();
3192
+ if (text.length > maxBodyBytes) {
3193
+ return fail("INVALID_REQUEST", `Request body exceeds ${maxBodyBytes} bytes`, 413);
3194
+ }
3195
+ let raw;
3196
+ try {
3197
+ raw = JSON.parse(text);
3198
+ } catch {
3199
+ return fail("INVALID_REQUEST", "Request body must be valid JSON", 400);
3200
+ }
3201
+ if (raw && typeof raw === "object" && "version" in raw && raw.version !== import_schema5.TRACKING_RELAY_PROTOCOL_VERSION) {
3202
+ return fail(
3203
+ "UNSUPPORTED_VERSION",
3204
+ `Unsupported relay protocol version; expected ${import_schema5.TRACKING_RELAY_PROTOCOL_VERSION}`,
3205
+ 400
3206
+ );
3207
+ }
3208
+ const parsed = import_schema5.TrackingRelayRequestSchema.safeParse(raw);
3209
+ if (!parsed.success) {
3210
+ const issues = parsed.error.issues.slice(0, 5).map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
3211
+ return fail("INVALID_REQUEST", `Invalid relay request: ${issues}`, 400);
3212
+ }
3213
+ const body = parsed.data;
3214
+ const config = await options.getConfig({
3215
+ request,
3216
+ documentId: body.documentId,
3217
+ credentialId: body.credentialId,
3218
+ provider: body.provider
3219
+ });
3220
+ if (!config) {
3221
+ return fail("CONFIG_NOT_FOUND", "No tracking configuration found for this page", 404);
3222
+ }
3223
+ const clientIp = (options.getClientIp ?? defaultClientIp)(request);
3224
+ const clientUserAgent = request.headers.get("user-agent") || void 0;
3225
+ const userData = { ...body.event.userData || {} };
3226
+ delete userData.clientIp;
3227
+ delete userData.clientUserAgent;
3228
+ const event = { ...body.event, userData };
3229
+ const outcome = await dispatchServerTracking(event, config, {
3230
+ resolveSecrets: options.resolveSecrets,
3231
+ documentId: body.documentId,
3232
+ provider: body.provider,
3233
+ fetchFn: options.fetchFn,
3234
+ clientIp,
3235
+ clientUserAgent,
3236
+ onLog: options.onLog
3237
+ });
3238
+ const results = {};
3239
+ for (const [key, r] of Object.entries(outcome.results)) {
3240
+ results[key] = {
3241
+ provider: r.provider,
3242
+ success: r.success,
3243
+ ...r.status !== void 0 ? { status: r.status } : {},
3244
+ ...r.skipped ? { skipped: true } : {},
3245
+ ...r.reason ? { reason: r.reason } : {},
3246
+ ...r.error ? { error: r.error } : {}
3247
+ };
3248
+ }
3249
+ return json(
3250
+ {
3251
+ version: import_schema5.TRACKING_RELAY_PROTOCOL_VERSION,
3252
+ success: outcome.success,
3253
+ eventId: outcome.eventId,
3254
+ ...outcome.skipped ? { skipped: true } : {},
3255
+ ...outcome.reason ? { reason: outcome.reason } : {},
3256
+ results
3257
+ },
3258
+ 200
3259
+ );
3260
+ } catch (err) {
3261
+ options.onLog?.("[Tracking relay] internal error", err);
3262
+ return fail("INTERNAL_ERROR", "Tracking relay failed", 500);
3263
+ }
3264
+ };
3265
+ }
3266
+
2976
3267
  // src/io/exporter.ts
2977
3268
  var import_fflate = require("fflate");
2978
- var import_schema6 = require("@kubuild/schema");
3269
+ var import_schema8 = require("@kubuild/schema");
2979
3270
 
2980
3271
  // src/validation/validator.ts
2981
- var import_schema5 = require("@kubuild/schema");
3272
+ var import_schema6 = require("@kubuild/schema");
2982
3273
 
2983
3274
  // src/validation/security.ts
2984
3275
  var DEFAULT_DOCUMENT_SECURITY_LIMITS = {
@@ -3267,10 +3558,10 @@ function validateDocument(input, options = {}) {
3267
3558
  };
3268
3559
  }
3269
3560
  }
3270
- if (doc.schema !== import_schema5.SCHEMA_NAME) {
3561
+ if (doc.schema !== import_schema6.SCHEMA_NAME) {
3271
3562
  errors.push({
3272
3563
  code: "GLOBAL_SCHEMA_INVALID",
3273
- message: `Invalid schema identifier. Expected "${import_schema5.SCHEMA_NAME}", received "${String(doc.schema)}"`,
3564
+ message: `Invalid schema identifier. Expected "${import_schema6.SCHEMA_NAME}", received "${String(doc.schema)}"`,
3274
3565
  path: "/schema"
3275
3566
  });
3276
3567
  }
@@ -3334,7 +3625,7 @@ function validateDocument(input, options = {}) {
3334
3625
  warnings
3335
3626
  );
3336
3627
  if (errors.length === 0) {
3337
- const zodResult = import_schema5.PageDocumentSchema.safeParse(input);
3628
+ const zodResult = import_schema6.PageDocumentSchema.safeParse(input);
3338
3629
  if (!zodResult.success) {
3339
3630
  for (const issue of zodResult.error.issues) {
3340
3631
  const jsonPath = "/" + issue.path.join("/");
@@ -3514,7 +3805,8 @@ function validateNodeRecursive(nodeObj, currentPath, parentNode, seenIds, visite
3514
3805
  `${currentPath}/props`,
3515
3806
  effectiveNodeId,
3516
3807
  options,
3517
- errors
3808
+ errors,
3809
+ warnings
3518
3810
  );
3519
3811
  }
3520
3812
  }
@@ -3529,7 +3821,10 @@ function validateNodeRecursive(nodeObj, currentPath, parentNode, seenIds, visite
3529
3821
  }
3530
3822
  }
3531
3823
  }
3532
- function validatePropsBindings(propsObj, propsPath, nodeId, options, errors) {
3824
+ function validatePropsBindings(propsObj, propsPath, nodeId, options, errors, warnings) {
3825
+ const checkAssets = options.checkAssetReferences !== false;
3826
+ const checkVariables = options.checkVariableBindings !== false;
3827
+ const checkActions = options.checkActionBindings !== false;
3533
3828
  for (const [key, value] of Object.entries(propsObj)) {
3534
3829
  const currentPath = `${propsPath}/${key}`;
3535
3830
  if (!value || typeof value !== "object") {
@@ -3543,7 +3838,8 @@ function validatePropsBindings(propsObj, propsPath, nodeId, options, errors) {
3543
3838
  `${currentPath}/${index}`,
3544
3839
  nodeId,
3545
3840
  options,
3546
- errors
3841
+ errors,
3842
+ warnings
3547
3843
  );
3548
3844
  }
3549
3845
  });
@@ -3551,7 +3847,9 @@ function validatePropsBindings(propsObj, propsPath, nodeId, options, errors) {
3551
3847
  }
3552
3848
  const record = value;
3553
3849
  if (record.type === "asset") {
3554
- if (typeof record.assetId !== "string" || record.assetId.trim().length === 0) {
3850
+ if (!checkAssets) continue;
3851
+ const assetId = typeof record.assetId === "string" && record.assetId.trim().length > 0 ? record.assetId : void 0;
3852
+ if (assetId === void 0) {
3555
3853
  errors.push({
3556
3854
  code: "INVALID_ASSET_REFERENCE",
3557
3855
  message: 'Asset reference must have a non-empty "assetId"',
@@ -3567,7 +3865,27 @@ function validatePropsBindings(propsObj, propsPath, nodeId, options, errors) {
3567
3865
  nodeId
3568
3866
  });
3569
3867
  }
3868
+ if (assetId !== void 0 && options.knownAssetIds && !isKnownId(options.knownAssetIds, assetId)) {
3869
+ if (typeof record.fallbackUrl === "string" && record.fallbackUrl.length > 0) {
3870
+ warnings.push({
3871
+ code: "UNRESOLVED_ASSET_REFERENCE",
3872
+ message: `Asset "${assetId}" is not among the known assets; its fallbackUrl will be used`,
3873
+ path: `${currentPath}/assetId`,
3874
+ nodeId,
3875
+ details: { assetId }
3876
+ });
3877
+ } else {
3878
+ errors.push({
3879
+ code: "INVALID_ASSET_REFERENCE",
3880
+ message: `Asset "${assetId}" is not among the known assets and has no fallbackUrl`,
3881
+ path: `${currentPath}/assetId`,
3882
+ nodeId,
3883
+ details: { assetId }
3884
+ });
3885
+ }
3886
+ }
3570
3887
  } else if (record.type === "variable") {
3888
+ if (!checkVariables) continue;
3571
3889
  if (typeof record.key !== "string" || record.key.trim().length === 0) {
3572
3890
  errors.push({
3573
3891
  code: "INVALID_VARIABLE_BINDING",
@@ -3577,6 +3895,7 @@ function validatePropsBindings(propsObj, propsPath, nodeId, options, errors) {
3577
3895
  });
3578
3896
  }
3579
3897
  } else if (key === "action" || typeof record.type === "string" && record.payload !== void 0) {
3898
+ if (!checkActions) continue;
3580
3899
  if (typeof record.type !== "string" || record.type.trim().length === 0) {
3581
3900
  errors.push({
3582
3901
  code: "INVALID_ACTION_BINDING",
@@ -3586,10 +3905,62 @@ function validatePropsBindings(propsObj, propsPath, nodeId, options, errors) {
3586
3905
  });
3587
3906
  }
3588
3907
  } else {
3589
- validatePropsBindings(record, currentPath, nodeId, options, errors);
3908
+ validatePropsBindings(record, currentPath, nodeId, options, errors, warnings);
3590
3909
  }
3591
3910
  }
3592
3911
  }
3912
+ function isKnownId(ids, id) {
3913
+ return Array.isArray(ids) ? ids.includes(id) : ids.has(id);
3914
+ }
3915
+
3916
+ // src/io/tracking-sanitizer.ts
3917
+ var import_schema7 = require("@kubuild/schema");
3918
+ function stripTrackingSecretsInPlace(tracking, basePath = "tracking") {
3919
+ const removed = [];
3920
+ if (!tracking || typeof tracking !== "object" || Array.isArray(tracking)) return removed;
3921
+ const providers = tracking.providers;
3922
+ if (!providers || typeof providers !== "object" || Array.isArray(providers)) return removed;
3923
+ for (const provider of Object.keys(import_schema7.LEGACY_TRACKING_SECRET_KEYS)) {
3924
+ const cfg = providers[provider];
3925
+ if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) continue;
3926
+ for (const key of import_schema7.LEGACY_TRACKING_SECRET_KEYS[provider]) {
3927
+ if (Object.prototype.hasOwnProperty.call(cfg, key)) {
3928
+ delete cfg[key];
3929
+ removed.push(`${basePath}.providers.${provider}.${key}`);
3930
+ }
3931
+ }
3932
+ }
3933
+ return removed;
3934
+ }
3935
+ function stripDocumentTrackingSecretsInPlace(doc) {
3936
+ const removed = [];
3937
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) return removed;
3938
+ const record = doc;
3939
+ removed.push(...stripTrackingSecretsInPlace(record.tracking, "tracking"));
3940
+ const metadata = record.metadata;
3941
+ if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
3942
+ removed.push(
3943
+ ...stripTrackingSecretsInPlace(metadata.tracking, "metadata.tracking")
3944
+ );
3945
+ }
3946
+ if (Array.isArray(record.artboards)) {
3947
+ record.artboards.forEach((artboard, index) => {
3948
+ if (artboard && typeof artboard === "object") {
3949
+ const inner = artboard.document;
3950
+ for (const path of stripDocumentTrackingSecretsInPlace(inner)) {
3951
+ removed.push(`artboards.${index}.document.${path}`);
3952
+ }
3953
+ }
3954
+ });
3955
+ }
3956
+ return removed;
3957
+ }
3958
+ function sanitizeDocumentTracking(doc) {
3959
+ if (!doc || typeof doc !== "object") return { document: doc, removed: [] };
3960
+ const copy = JSON.parse(JSON.stringify(doc));
3961
+ const removed = stripDocumentTrackingSecretsInPlace(copy);
3962
+ return { document: copy, removed };
3963
+ }
3593
3964
 
3594
3965
  // src/io/exporter.ts
3595
3966
  function sha256Sync(data) {
@@ -3810,6 +4181,7 @@ async function exportPackage(document, options = {}) {
3810
4181
  };
3811
4182
  }
3812
4183
  const pageDoc = JSON.parse(JSON.stringify(validation.data));
4184
+ stripDocumentTrackingSecretsInPlace(pageDoc);
3813
4185
  if (options.metadata) {
3814
4186
  pageDoc.metadata = {
3815
4187
  ...pageDoc.metadata || {
@@ -3933,8 +4305,8 @@ async function exportPackage(document, options = {}) {
3933
4305
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3934
4306
  };
3935
4307
  const manifest = {
3936
- schema: import_schema6.SCHEMA_NAME,
3937
- schemaVersion: pageDoc.version || import_schema6.CURRENT_SCHEMA_VERSION,
4308
+ schema: import_schema8.SCHEMA_NAME,
4309
+ schemaVersion: pageDoc.version || import_schema8.CURRENT_SCHEMA_VERSION,
3938
4310
  packageVersion: options.packageVersion || "1.0.0",
3939
4311
  builderCompatibility: options.builderCompatibility || ">=0.1.0",
3940
4312
  requiredComponents: requirements.requiredComponents,
@@ -3959,10 +4331,10 @@ var exportStoraPackage = exportPackage;
3959
4331
 
3960
4332
  // src/io/importer.ts
3961
4333
  var import_fflate2 = require("fflate");
3962
- var import_schema8 = require("@kubuild/schema");
4334
+ var import_schema10 = require("@kubuild/schema");
3963
4335
 
3964
4336
  // src/io/migration.ts
3965
- var import_schema7 = require("@kubuild/schema");
4337
+ var import_schema9 = require("@kubuild/schema");
3966
4338
  var MigrationRegistry = class {
3967
4339
  steps = /* @__PURE__ */ new Map();
3968
4340
  /**
@@ -4041,7 +4413,7 @@ defaultMigrationRegistry.register({
4041
4413
  description: "Migrate alpha 0.1.0 schema (root node key, flat styles) to v1.0.0",
4042
4414
  migrate: (rawDoc) => {
4043
4415
  const migrated = deepClone(rawDoc);
4044
- migrated.schema = import_schema7.SCHEMA_NAME;
4416
+ migrated.schema = import_schema9.SCHEMA_NAME;
4045
4417
  migrated.version = "1.0.0";
4046
4418
  if (migrated.root && !migrated.document) {
4047
4419
  migrated.document = migrated.root;
@@ -4076,7 +4448,7 @@ defaultMigrationRegistry.register({
4076
4448
  description: "Migrate 0.2.0 schema to v1.0.0",
4077
4449
  migrate: (rawDoc) => {
4078
4450
  const migrated = deepClone(rawDoc);
4079
- migrated.schema = import_schema7.SCHEMA_NAME;
4451
+ migrated.schema = import_schema9.SCHEMA_NAME;
4080
4452
  migrated.version = "1.0.0";
4081
4453
  if (migrated.document && typeof migrated.document === "object") {
4082
4454
  convertNodeStylesToResponsive(migrated.document);
@@ -4090,7 +4462,7 @@ defaultMigrationRegistry.register({
4090
4462
  description: "Migrate 0.9.0 beta schema to v1.0.0",
4091
4463
  migrate: (rawDoc) => {
4092
4464
  const migrated = deepClone(rawDoc);
4093
- migrated.schema = import_schema7.SCHEMA_NAME;
4465
+ migrated.schema = import_schema9.SCHEMA_NAME;
4094
4466
  migrated.version = "1.0.0";
4095
4467
  if (!migrated.metadata || typeof migrated.metadata !== "object") {
4096
4468
  migrated.metadata = {
@@ -4115,17 +4487,86 @@ defaultMigrationRegistry.register({
4115
4487
  return migrated;
4116
4488
  }
4117
4489
  });
4118
- function canMigrate(sourceVersion, targetVersion = import_schema7.CURRENT_SCHEMA_VERSION, registry = defaultMigrationRegistry) {
4490
+ defaultMigrationRegistry.register({
4491
+ fromVersion: "1.0.0",
4492
+ toVersion: "1.1.0",
4493
+ description: "Remove tracking secrets and server destinations (capiAccessToken, accessToken, measurementProtocolSecret, serverRelayUrl, custom endpointUrl/headers) from the document",
4494
+ migrate: (rawDoc, context) => {
4495
+ const migrated = deepClone(rawDoc);
4496
+ migrated.version = "1.1.0";
4497
+ const removed = stripDocumentTrackingSecretsInPlace(migrated);
4498
+ if (removed.length > 0) {
4499
+ context.warn({
4500
+ code: "TRACKING_SECRET_REMOVED",
4501
+ message: "Tracking secret removed; re-link credential via credentialId (secrets are now stored by the host and resolved server-side).",
4502
+ step: "1.0.0->1.1.0",
4503
+ paths: removed
4504
+ });
4505
+ }
4506
+ return migrated;
4507
+ }
4508
+ });
4509
+ function canonicalizeTextPropsInPlace(root, basePath = "document") {
4510
+ const changed = [];
4511
+ const walk2 = (value, path) => {
4512
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
4513
+ const node = value;
4514
+ const props = node.props;
4515
+ const rule = typeof node.type === "string" ? (0, import_schema9.getCanonicalTextPropRule)(node.type) : void 0;
4516
+ if (rule && props && typeof props === "object" && !Array.isArray(props)) {
4517
+ const record = props;
4518
+ const presentAliases = rule.aliases.filter((alias) => record[alias] !== void 0);
4519
+ if (presentAliases.length > 0) {
4520
+ const displayedKey = rule.legacyReadOrder.find((key) => record[key] !== void 0);
4521
+ if (node.type === "text" && displayedKey === "content" && record.as === void 0 && record.tag === void 0) {
4522
+ record.as = "p";
4523
+ }
4524
+ if (displayedKey !== void 0 && displayedKey !== rule.canonical) {
4525
+ record[rule.canonical] = record[displayedKey];
4526
+ }
4527
+ for (const alias of presentAliases) {
4528
+ delete record[alias];
4529
+ changed.push(`${path}.props.${alias}`);
4530
+ }
4531
+ }
4532
+ }
4533
+ if (Array.isArray(node.children)) {
4534
+ node.children.forEach((child, index) => walk2(child, `${path}.children.${index}`));
4535
+ }
4536
+ };
4537
+ walk2(root, basePath);
4538
+ return changed;
4539
+ }
4540
+ defaultMigrationRegistry.register({
4541
+ fromVersion: "1.1.0",
4542
+ toVersion: "1.2.0",
4543
+ description: "Rename deprecated text prop aliases to canonical names (heading/text/paragraph/link/badge/blockquote -> text, button -> label)",
4544
+ migrate: (rawDoc, context) => {
4545
+ const migrated = deepClone(rawDoc);
4546
+ migrated.version = "1.2.0";
4547
+ const changed = canonicalizeTextPropsInPlace(migrated.document);
4548
+ if (changed.length > 0) {
4549
+ context.warn({
4550
+ code: "PROP_ALIAS_MIGRATED",
4551
+ message: "Deprecated text prop aliases were renamed to their canonical names.",
4552
+ step: "1.1.0->1.2.0",
4553
+ paths: changed
4554
+ });
4555
+ }
4556
+ return migrated;
4557
+ }
4558
+ });
4559
+ function canMigrate(sourceVersion, targetVersion = import_schema9.CURRENT_SCHEMA_VERSION, registry = defaultMigrationRegistry) {
4119
4560
  if (!sourceVersion || !targetVersion) return false;
4120
4561
  if (sourceVersion === targetVersion) return true;
4121
4562
  return registry.hasPath(sourceVersion, targetVersion);
4122
4563
  }
4123
- function getMigrationPath(sourceVersion, targetVersion = import_schema7.CURRENT_SCHEMA_VERSION, registry = defaultMigrationRegistry) {
4564
+ function getMigrationPath(sourceVersion, targetVersion = import_schema9.CURRENT_SCHEMA_VERSION, registry = defaultMigrationRegistry) {
4124
4565
  if (!sourceVersion || !targetVersion) return null;
4125
4566
  return registry.findPath(sourceVersion, targetVersion);
4126
4567
  }
4127
4568
  function migrateDocument(rawDocument, options = {}) {
4128
- const targetVersion = options.targetVersion ?? import_schema7.CURRENT_SCHEMA_VERSION;
4569
+ const targetVersion = options.targetVersion ?? import_schema9.CURRENT_SCHEMA_VERSION;
4129
4570
  const dryRun = options.dryRun ?? false;
4130
4571
  const registry = options.registry ?? defaultMigrationRegistry;
4131
4572
  const validate = options.validate ?? true;
@@ -4153,6 +4594,14 @@ function migrateDocument(rawDocument, options = {}) {
4153
4594
  const sourceVersion = typeof doc.version === "string" ? doc.version.trim() : "unknown";
4154
4595
  if (sourceVersion === targetVersion) {
4155
4596
  const cloned = deepClone(doc);
4597
+ const removedSecrets = stripDocumentTrackingSecretsInPlace(cloned);
4598
+ const currentWarnings = removedSecrets.length > 0 ? [
4599
+ {
4600
+ code: "TRACKING_SECRET_REMOVED",
4601
+ message: "Tracking secret removed; re-link credential via credentialId (secrets are stored by the host and resolved server-side).",
4602
+ paths: removedSecrets
4603
+ }
4604
+ ] : [];
4156
4605
  if (validate) {
4157
4606
  const validation = validateDocument(cloned);
4158
4607
  if (!validation.valid) {
@@ -4184,7 +4633,8 @@ function migrateDocument(rawDocument, options = {}) {
4184
4633
  targetVersion,
4185
4634
  migrationPath: [sourceVersion],
4186
4635
  stepsApplied: 0,
4187
- dryRun
4636
+ dryRun,
4637
+ ...currentWarnings.length > 0 ? { warnings: currentWarnings } : {}
4188
4638
  }
4189
4639
  };
4190
4640
  }
@@ -4210,6 +4660,16 @@ function migrateDocument(rawDocument, options = {}) {
4210
4660
  };
4211
4661
  }
4212
4662
  if (dryRun) {
4663
+ const simulatedWarnings = [];
4664
+ try {
4665
+ let simulated = deepClone(doc);
4666
+ for (let i = 0; i < path.length - 1; i++) {
4667
+ const step = registry.getStep(path[i], path[i + 1]);
4668
+ if (!step) break;
4669
+ simulated = step.migrate(simulated, { warn: (w) => simulatedWarnings.push(w) });
4670
+ }
4671
+ } catch {
4672
+ }
4213
4673
  return {
4214
4674
  success: true,
4215
4675
  diagnostic: {
@@ -4218,12 +4678,15 @@ function migrateDocument(rawDocument, options = {}) {
4218
4678
  targetVersion,
4219
4679
  migrationPath: path,
4220
4680
  stepsApplied: path.length - 1,
4221
- dryRun: true
4681
+ dryRun: true,
4682
+ ...simulatedWarnings.length > 0 ? { warnings: simulatedWarnings } : {}
4222
4683
  }
4223
4684
  };
4224
4685
  }
4225
4686
  let currentDoc = deepClone(doc);
4226
4687
  let stepsApplied = 0;
4688
+ const warnings = [];
4689
+ const stepContext = { warn: (w) => warnings.push(w) };
4227
4690
  for (let i = 0; i < path.length - 1; i++) {
4228
4691
  const fromVer = path[i];
4229
4692
  const toVer = path[i + 1];
@@ -4249,7 +4712,7 @@ function migrateDocument(rawDocument, options = {}) {
4249
4712
  };
4250
4713
  }
4251
4714
  try {
4252
- currentDoc = step.migrate(currentDoc);
4715
+ currentDoc = step.migrate(currentDoc, stepContext);
4253
4716
  stepsApplied++;
4254
4717
  } catch (err) {
4255
4718
  const error = {
@@ -4305,7 +4768,8 @@ function migrateDocument(rawDocument, options = {}) {
4305
4768
  targetVersion,
4306
4769
  migrationPath: path,
4307
4770
  stepsApplied,
4308
- dryRun: false
4771
+ dryRun: false,
4772
+ ...warnings.length > 0 ? { warnings } : {}
4309
4773
  }
4310
4774
  };
4311
4775
  }
@@ -4340,7 +4804,7 @@ async function preflightPackage(archiveData, options = {}) {
4340
4804
  ...DEFAULT_SECURITY_LIMITS,
4341
4805
  ...options.securityLimits
4342
4806
  };
4343
- const targetVersion = options.targetSchemaVersion || import_schema8.CURRENT_SCHEMA_VERSION;
4807
+ const targetVersion = options.targetSchemaVersion || import_schema10.CURRENT_SCHEMA_VERSION;
4344
4808
  const migrationRegistry = options.migrationRegistry || defaultMigrationRegistry;
4345
4809
  const diagnostics = [];
4346
4810
  let dependencyPolicy = options.dependencyPolicy || "cancel";
@@ -4473,7 +4937,7 @@ async function preflightPackage(archiveData, options = {}) {
4473
4937
  path: manifestProtoCheck.path
4474
4938
  });
4475
4939
  }
4476
- const parseResult = import_schema8.ManifestSchema.safeParse(manifestJson);
4940
+ const parseResult = import_schema10.ManifestSchema.safeParse(manifestJson);
4477
4941
  if (!parseResult.success) {
4478
4942
  const issues = parseResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
4479
4943
  diagnostics.push({
@@ -4503,6 +4967,16 @@ async function preflightPackage(archiveData, options = {}) {
4503
4967
  let rawPageDoc;
4504
4968
  try {
4505
4969
  rawPageDoc = JSON.parse((0, import_fflate2.strFromU8)(pageEntry));
4970
+ const secretScan = sanitizeDocumentTracking(rawPageDoc);
4971
+ if (secretScan.removed.length > 0) {
4972
+ diagnostics.push({
4973
+ code: "TRACKING_SECRET_REMOVED",
4974
+ severity: "warning",
4975
+ message: "Tracking secrets found in page.json will be removed on import; re-link credentials via credentialId.",
4976
+ path: "page.json",
4977
+ details: { paths: secretScan.removed }
4978
+ });
4979
+ }
4506
4980
  const pageProtoCheck = containsProhibitedKeys(rawPageDoc);
4507
4981
  if (pageProtoCheck.found) {
4508
4982
  diagnostics.push({
@@ -4749,7 +5223,7 @@ function buildReport(valid, canImport, fields) {
4749
5223
  return {
4750
5224
  valid,
4751
5225
  canImport,
4752
- targetVersion: fields.targetVersion || import_schema8.CURRENT_SCHEMA_VERSION,
5226
+ targetVersion: fields.targetVersion || import_schema10.CURRENT_SCHEMA_VERSION,
4753
5227
  requiresMigration: fields.requiresMigration || false,
4754
5228
  missingComponents: fields.missingComponents || [],
4755
5229
  missingCapabilities: fields.missingCapabilities || [],
@@ -4822,7 +5296,7 @@ async function importPackage(archiveData, options = {}) {
4822
5296
  if (preflight.requiresMigration) {
4823
5297
  const migrationRegistry = options.migrationRegistry || defaultMigrationRegistry;
4824
5298
  const migrationRes = migrateDocument(pageRaw, {
4825
- targetVersion: options.targetSchemaVersion || import_schema8.CURRENT_SCHEMA_VERSION,
5299
+ targetVersion: options.targetSchemaVersion || import_schema10.CURRENT_SCHEMA_VERSION,
4826
5300
  registry: migrationRegistry,
4827
5301
  validate: true
4828
5302
  });
@@ -4844,13 +5318,24 @@ async function importPackage(archiveData, options = {}) {
4844
5318
  } else {
4845
5319
  finalDocument = pageRaw;
4846
5320
  }
5321
+ const importWarnings = [];
5322
+ const strippedSecrets = stripDocumentTrackingSecretsInPlace(finalDocument);
5323
+ if (strippedSecrets.length > 0) {
5324
+ importWarnings.push({
5325
+ code: "TRACKING_SECRET_REMOVED",
5326
+ severity: "warning",
5327
+ message: "Tracking secret removed from imported document; re-link credential via credentialId.",
5328
+ path: "page.json",
5329
+ details: { paths: strippedSecrets }
5330
+ });
5331
+ }
4847
5332
  let metadata = finalDocument.metadata || {
4848
5333
  title: "Imported Page",
4849
5334
  description: "",
4850
5335
  author: "",
4851
5336
  tags: [],
4852
5337
  category: "general",
4853
- version: finalDocument.version || import_schema8.CURRENT_SCHEMA_VERSION
5338
+ version: finalDocument.version || import_schema10.CURRENT_SCHEMA_VERSION
4854
5339
  };
4855
5340
  if (unzipped["metadata.json"]) {
4856
5341
  try {
@@ -5002,13 +5487,26 @@ async function importPackage(archiveData, options = {}) {
5002
5487
  metadata,
5003
5488
  extractedAssets,
5004
5489
  renamedAssets: Object.keys(renameMap).length > 0 ? renameMap : void 0,
5005
- preflight
5490
+ preflight,
5491
+ ...importWarnings.length > 0 ? { warnings: importWarnings } : {}
5006
5492
  };
5007
5493
  }
5008
5494
  var importStoraPackage = importPackage;
5009
5495
 
5010
5496
  // src/io/load-project.ts
5011
- var import_schema9 = require("@kubuild/schema");
5497
+ var import_schema11 = require("@kubuild/schema");
5498
+ function migrateProjectArtboards(project) {
5499
+ let changed = false;
5500
+ const artboards = project.artboards.map((artboard) => {
5501
+ const version = artboard.document.version;
5502
+ if (version === import_schema11.CURRENT_SCHEMA_VERSION || !canMigrate(version)) return artboard;
5503
+ const migration = migrateDocument(artboard.document);
5504
+ if (!migration.success || !migration.document) return artboard;
5505
+ changed = true;
5506
+ return { ...artboard, document: migration.document };
5507
+ });
5508
+ return changed ? { ...project, artboards } : project;
5509
+ }
5012
5510
  var DEFAULT_PAGE_ARTBOARD_ID = "artboard-page-1";
5013
5511
  function wrapPageDocumentAsProject(document, options = {}) {
5014
5512
  const artboardId = options.artboardId ?? DEFAULT_PAGE_ARTBOARD_ID;
@@ -5021,8 +5519,8 @@ function wrapPageDocumentAsProject(document, options = {}) {
5021
5519
  ...options.width !== void 0 ? { width: options.width } : {}
5022
5520
  };
5023
5521
  return {
5024
- schema: import_schema9.PROJECT_SCHEMA_NAME,
5025
- version: import_schema9.CURRENT_PROJECT_SCHEMA_VERSION,
5522
+ schema: import_schema11.PROJECT_SCHEMA_NAME,
5523
+ version: import_schema11.CURRENT_PROJECT_SCHEMA_VERSION,
5026
5524
  metadata: document.metadata,
5027
5525
  artboards: [artboard],
5028
5526
  activeArtboardId: artboardId
@@ -5041,8 +5539,8 @@ function loadProjectDocument(raw) {
5041
5539
  ]
5042
5540
  };
5043
5541
  }
5044
- if ((0, import_schema9.looksLikeProjectDocument)(raw)) {
5045
- const parsed = import_schema9.ProjectDocumentSchema.safeParse(raw);
5542
+ if ((0, import_schema11.looksLikeProjectDocument)(raw)) {
5543
+ const parsed = import_schema11.ProjectDocumentSchema.safeParse(raw);
5046
5544
  if (!parsed.success) {
5047
5545
  return {
5048
5546
  success: false,
@@ -5058,7 +5556,7 @@ function loadProjectDocument(raw) {
5058
5556
  }
5059
5557
  return {
5060
5558
  success: true,
5061
- project: parsed.data,
5559
+ project: migrateProjectArtboards(parsed.data),
5062
5560
  wrappedFromLegacyPage: false,
5063
5561
  errors: []
5064
5562
  };
@@ -5527,21 +6025,10 @@ function compareManifestsSemantically(expected, actual, options = {}) {
5527
6025
  }
5528
6026
 
5529
6027
  // src/io/template-utils.ts
5530
- var import_schema10 = require("@kubuild/schema");
5531
- var CORE_BUILTIN_COMPONENTS = /* @__PURE__ */ new Set([
5532
- "page",
5533
- "section",
5534
- "container",
5535
- "columns",
5536
- "column",
5537
- "heading",
5538
- "text",
5539
- "image",
5540
- "button",
5541
- "collection"
5542
- ]);
6028
+ var import_schema12 = require("@kubuild/schema");
6029
+ var CORE_BUILTIN_COMPONENTS = new Set(import_schema12.BUILTIN_COMPONENT_TYPES);
5543
6030
  function validateTemplate(value) {
5544
- const parseResult = import_schema10.TemplateRecordSchema.safeParse(value);
6031
+ const parseResult = import_schema12.TemplateRecordSchema.safeParse(value);
5545
6032
  if (parseResult.success) {
5546
6033
  return {
5547
6034
  valid: true,
@@ -5650,26 +6137,10 @@ function saveDraftAsTemplate(draft, metadata, options = {}) {
5650
6137
  custom: metadata.custom ? deepClone(metadata.custom) : void 0
5651
6138
  });
5652
6139
  }
5653
- function cloneTreeWithFreshIds(root, idGen) {
5654
- function cloneRec(node) {
5655
- const newId = idGen(node.id, node);
5656
- const clonedProps = node.props ? deepClone(node.props) : void 0;
5657
- const clonedStyles = node.styles ? deepClone(node.styles) : void 0;
5658
- const clonedChildren = node.children ? node.children.map((child) => cloneRec(child)) : [];
5659
- return {
5660
- id: newId,
5661
- type: node.type,
5662
- ...clonedProps ? { props: clonedProps } : {},
5663
- ...clonedStyles ? { styles: clonedStyles } : {},
5664
- children: clonedChildren
5665
- };
5666
- }
5667
- return cloneRec(root);
5668
- }
5669
6140
  function cloneTemplateAsPage(templateOrDoc, options = {}) {
5670
6141
  let sourceDoc;
5671
6142
  let templateOrigin = null;
5672
- if ((0, import_schema10.isTemplateRecord)(templateOrDoc)) {
6143
+ if ((0, import_schema12.isTemplateRecord)(templateOrDoc)) {
5673
6144
  if (!templateOrDoc.document) {
5674
6145
  throw new Error(
5675
6146
  `Template "${templateOrDoc.name}" (${templateOrDoc.id}) does not contain an inline document snapshot to clone from.`
@@ -5686,7 +6157,7 @@ function cloneTemplateAsPage(templateOrDoc, options = {}) {
5686
6157
  } else {
5687
6158
  throw new Error("Invalid input: expected a TemplateRecord or PageDocument.");
5688
6159
  }
5689
- const existingIdsInSource = new Set((0, import_schema10.collectNodeIds)(sourceDoc.document));
6160
+ const existingIdsInSource = new Set((0, import_schema12.collectNodeIds)(sourceDoc.document));
5690
6161
  const assignedNewIds = /* @__PURE__ */ new Set();
5691
6162
  const prefix = options.idPrefix || `page_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 6)}`;
5692
6163
  let counter = 1;
@@ -5701,7 +6172,7 @@ function cloneTemplateAsPage(templateOrDoc, options = {}) {
5701
6172
  return candidate;
5702
6173
  };
5703
6174
  const idGen = options.idGenerator || defaultIdGen;
5704
- const clonedRootNode = cloneTreeWithFreshIds(sourceDoc.document, idGen);
6175
+ const { clonedNode: clonedRootNode } = cloneNodeTreeWithFreshIds(sourceDoc.document, idGen);
5705
6176
  const rootPageNode = {
5706
6177
  ...clonedRootNode,
5707
6178
  type: "page"
@@ -5735,8 +6206,8 @@ function cloneTemplateAsPage(templateOrDoc, options = {}) {
5735
6206
  }
5736
6207
  function areNodeIdsCompletelyDistinct(docA, docB) {
5737
6208
  if (!docA?.document || !docB?.document) return true;
5738
- const idsA = new Set((0, import_schema10.collectNodeIds)(docA.document));
5739
- const idsB = (0, import_schema10.collectNodeIds)(docB.document);
6209
+ const idsA = new Set((0, import_schema12.collectNodeIds)(docA.document));
6210
+ const idsB = (0, import_schema12.collectNodeIds)(docB.document);
5740
6211
  for (const id of idsB) {
5741
6212
  if (idsA.has(id)) {
5742
6213
  return false;
@@ -5746,7 +6217,7 @@ function areNodeIdsCompletelyDistinct(docA, docB) {
5746
6217
  }
5747
6218
 
5748
6219
  // src/validation/project-validator.ts
5749
- var import_schema11 = require("@kubuild/schema");
6220
+ var import_schema13 = require("@kubuild/schema");
5750
6221
  function walk(node, visit) {
5751
6222
  visit(node);
5752
6223
  for (const child of node.children ?? []) {
@@ -5756,7 +6227,7 @@ function walk(node, visit) {
5756
6227
  function collectArtboardReferences(artboard) {
5757
6228
  const refs = [];
5758
6229
  walk(artboard.document.document, (node) => {
5759
- if (node.type === import_schema11.ARTBOARD_REFERENCE_NODE_TYPE) {
6230
+ if (node.type === import_schema13.ARTBOARD_REFERENCE_NODE_TYPE) {
5760
6231
  const target = node.props?.artboardId;
5761
6232
  refs.push({
5762
6233
  nodeId: node.id,
@@ -5783,7 +6254,7 @@ function validateProject(input, options = {}) {
5783
6254
  artboardErrors
5784
6255
  };
5785
6256
  }
5786
- const parsed = import_schema11.ProjectDocumentSchema.safeParse(input);
6257
+ const parsed = import_schema13.ProjectDocumentSchema.safeParse(input);
5787
6258
  if (!parsed.success) {
5788
6259
  return {
5789
6260
  valid: false,
@@ -5791,7 +6262,7 @@ function validateProject(input, options = {}) {
5791
6262
  errors: [
5792
6263
  {
5793
6264
  code: "PROJECT_SCHEMA_INVALID",
5794
- message: `Project failed schema validation. Expected schema "${import_schema11.PROJECT_SCHEMA_NAME}".`,
6265
+ message: `Project failed schema validation. Expected schema "${import_schema13.PROJECT_SCHEMA_NAME}".`,
5795
6266
  path: "",
5796
6267
  details: { issues: parsed.error.issues }
5797
6268
  }
@@ -5884,7 +6355,7 @@ function validateProject(input, options = {}) {
5884
6355
  function collectArtboardReferenceNodes(root) {
5885
6356
  const refs = [];
5886
6357
  walk(root, (node) => {
5887
- if (node.type === import_schema11.ARTBOARD_REFERENCE_NODE_TYPE) {
6358
+ if (node.type === import_schema13.ARTBOARD_REFERENCE_NODE_TYPE) {
5888
6359
  const target = node.props?.artboardId;
5889
6360
  refs.push({ nodeId: node.id, artboardId: typeof target === "string" ? target : void 0 });
5890
6361
  }
@@ -6109,6 +6580,7 @@ function isFormValid(errors) {
6109
6580
  DocumentHistoryManager,
6110
6581
  HistoryEngine,
6111
6582
  MigrationRegistry,
6583
+ NODE_ID_REFERENCE_KEYS,
6112
6584
  RuntimeStateStore,
6113
6585
  addArtboard,
6114
6586
  applyFieldTransform,
@@ -6116,7 +6588,9 @@ function isFormValid(errors) {
6116
6588
  buildSampleVariablesFromCatalog,
6117
6589
  calculateChecksum,
6118
6590
  canMigrate,
6591
+ canonicalizeTextPropsInPlace,
6119
6592
  checkZipBomb,
6593
+ cloneNodeTreeWithFreshIds,
6120
6594
  cloneTemplateAsPage,
6121
6595
  cloneTreeWithNewIds,
6122
6596
  collectArtboardReferenceNodes,
@@ -6133,6 +6607,7 @@ function isFormValid(errors) {
6133
6607
  createPageArtboard,
6134
6608
  createRuntimeStore,
6135
6609
  createTemplateRecord,
6610
+ createTrackingRelayHandler,
6136
6611
  deepClone,
6137
6612
  defaultIdGenerator,
6138
6613
  defaultMigrationRegistry,
@@ -6192,6 +6667,7 @@ function isFormValid(errors) {
6192
6667
  previewImportPackage,
6193
6668
  remapAssetReferences,
6194
6669
  remapDocumentAssetReferences,
6670
+ remapNodeReferences,
6195
6671
  removeArtboard,
6196
6672
  removeNode,
6197
6673
  renameArtboard,
@@ -6199,6 +6675,7 @@ function isFormValid(errors) {
6199
6675
  resolveBinding,
6200
6676
  resolveBindingValue,
6201
6677
  resolvePropertyPath,
6678
+ sanitizeDocumentTracking,
6202
6679
  sanitizeFilename,
6203
6680
  sanitizeHtml,
6204
6681
  sanitizeUrl,
@@ -6209,6 +6686,8 @@ function isFormValid(errors) {
6209
6686
  sendTikTokEventsApi,
6210
6687
  setActiveArtboard,
6211
6688
  sha256Sync,
6689
+ stripDocumentTrackingSecretsInPlace,
6690
+ stripTrackingSecretsInPlace,
6212
6691
  ungroupNodeFrame,
6213
6692
  updateActions,
6214
6693
  updateAnimation,
@@ -6216,6 +6695,7 @@ function isFormValid(errors) {
6216
6695
  updateFormConfig,
6217
6696
  updateProps,
6218
6697
  updateStyle,
6698
+ updateTheme,
6219
6699
  validateDocument,
6220
6700
  validateDocumentSecurity,
6221
6701
  validateFieldValue,