sf-plugin-cms 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +276 -20
  2. package/lib/commands/cms/export/workspace.d.ts +7 -1
  3. package/lib/commands/cms/export/workspace.js +113 -2
  4. package/lib/commands/cms/export/workspace.js.map +1 -1
  5. package/lib/commands/cms/import/workspace.d.ts +12 -4
  6. package/lib/commands/cms/import/workspace.js +167 -20
  7. package/lib/commands/cms/import/workspace.js.map +1 -1
  8. package/lib/commands/cms/info.js +21 -2
  9. package/lib/commands/cms/info.js.map +1 -1
  10. package/lib/contracts/info.d.ts +7 -4
  11. package/lib/contracts/info.js +17 -8
  12. package/lib/contracts/info.js.map +1 -1
  13. package/lib/contracts/shared.d.ts +2 -1
  14. package/lib/contracts/shared.js +11 -3
  15. package/lib/contracts/shared.js.map +1 -1
  16. package/lib/contracts/workspace-export.d.ts +41 -3
  17. package/lib/contracts/workspace-export.js +171 -5
  18. package/lib/contracts/workspace-export.js.map +1 -1
  19. package/lib/contracts/workspace-import.d.ts +65 -0
  20. package/lib/contracts/workspace-import.js +132 -1
  21. package/lib/contracts/workspace-import.js.map +1 -1
  22. package/lib/index.d.ts +3 -1
  23. package/lib/index.js +2 -0
  24. package/lib/index.js.map +1 -1
  25. package/lib/services/cms-prerequisite-lookup.d.ts +16 -0
  26. package/lib/services/cms-prerequisite-lookup.js +89 -0
  27. package/lib/services/cms-prerequisite-lookup.js.map +1 -0
  28. package/lib/services/content-key.d.ts +9 -0
  29. package/lib/services/content-key.js +15 -0
  30. package/lib/services/content-key.js.map +1 -0
  31. package/lib/services/email-fragment.d.ts +15 -0
  32. package/lib/services/email-fragment.js +203 -0
  33. package/lib/services/email-fragment.js.map +1 -0
  34. package/lib/services/export-workspace.d.ts +15 -1
  35. package/lib/services/export-workspace.js +284 -51
  36. package/lib/services/export-workspace.js.map +1 -1
  37. package/lib/services/image-import.d.ts +91 -0
  38. package/lib/services/image-import.js +575 -0
  39. package/lib/services/image-import.js.map +1 -0
  40. package/lib/services/import-identities.js +5 -1
  41. package/lib/services/import-identities.js.map +1 -1
  42. package/lib/services/import-workspace.d.ts +63 -3
  43. package/lib/services/import-workspace.js +277 -26
  44. package/lib/services/import-workspace.js.map +1 -1
  45. package/lib/services/landing-page-pair-export.d.ts +31 -0
  46. package/lib/services/landing-page-pair-export.js +122 -0
  47. package/lib/services/landing-page-pair-export.js.map +1 -0
  48. package/lib/services/landing-page-template.d.ts +62 -0
  49. package/lib/services/landing-page-template.js +372 -0
  50. package/lib/services/landing-page-template.js.map +1 -0
  51. package/lib/services/landing-page.d.ts +87 -0
  52. package/lib/services/landing-page.js +355 -0
  53. package/lib/services/landing-page.js.map +1 -0
  54. package/lib/services/variant-identity.d.ts +14 -0
  55. package/lib/services/variant-identity.js +24 -0
  56. package/lib/services/variant-identity.js.map +1 -1
  57. package/lib/services/web-fragment.d.ts +47 -0
  58. package/lib/services/web-fragment.js +286 -0
  59. package/lib/services/web-fragment.js.map +1 -0
  60. package/lib/transport/experimental-media.d.ts +30 -0
  61. package/lib/transport/experimental-media.js +224 -0
  62. package/lib/transport/experimental-media.js.map +1 -0
  63. package/lib/transport/json-request.d.ts +9 -1
  64. package/lib/transport/json-request.js +30 -3
  65. package/lib/transport/json-request.js.map +1 -1
  66. package/lib/transport/multipart-image-create.d.ts +28 -0
  67. package/lib/transport/multipart-image-create.js +189 -0
  68. package/lib/transport/multipart-image-create.js.map +1 -0
  69. package/messages/cms.export.workspace.md +20 -0
  70. package/messages/cms.import.workspace.md +12 -2
  71. package/package.json +1 -1
@@ -3,11 +3,15 @@ import { lstat, mkdir, open, readFile, readdir, realpath } from 'node:fs/promise
3
3
  import path from 'node:path';
4
4
  import { cleanupOwnedPath, replaceFileByAtomicRename, } from './atomic-publish.js';
5
5
  import { CmsRequestError } from '../transport/json-request.js';
6
- import { assertWorkspaceExportManifest, } from '../contracts/workspace-export.js';
6
+ import { assertWorkspaceExportManifest, UnsupportedWorkspacePackageVersionError, } from '../contracts/workspace-export.js';
7
7
  import { getContent } from './read.js';
8
8
  import { variantIdentity } from './variant-identity.js';
9
9
  import { inventoryExportReferences } from './export-references.js';
10
- import { planImportIdentities, planNativeCopies } from './import-identities.js';
10
+ import { planImportIdentities, planNativeCopies, } from './import-identities.js';
11
+ import { planEmailFragmentCopies } from './email-fragment.js';
12
+ import { planWebFragmentCopies, validateDataGraphPrerequisites, } from './web-fragment.js';
13
+ import { planLandingPageTemplateCopies, validateLandingPageTemplatePrerequisites, } from './landing-page-template.js';
14
+ import { planLandingPageCopies, resolveLandingPageTemplateDependencies, validateLandingPagePrerequisites, } from './landing-page.js';
11
15
  import { loadEditableRawHtml } from './editable-raw-html-import.js';
12
16
  import { isDeepStrictEqual } from 'node:util';
13
17
  import { createContent, createVariant, } from './write.js';
@@ -233,6 +237,9 @@ export async function loadWorkspaceExport(sourceDirectory, options = {}) {
233
237
  const manifestValue = parseJson(manifestBytes, 'manifest.json');
234
238
  assertWorkspaceExportManifest(manifestValue);
235
239
  const manifest = manifestValue;
240
+ if (options.profile === 'image' && manifest.schemaVersion !== 2) {
241
+ throw new UnsupportedWorkspacePackageVersionError('Image import profile requires workspace package manifest version 2');
242
+ }
236
243
  const packageFiles = await enumeratePackageFiles(canonicalSource, canonicalSource);
237
244
  const expectedFiles = [
238
245
  'manifest.json',
@@ -242,6 +249,19 @@ export async function loadWorkspaceExport(sourceDirectory, options = {}) {
242
249
  throw new TypeError('Workspace package contains missing, substituted, or unlisted regular files');
243
250
  }
244
251
  const manifestItems = new Map(manifest.items.map((item_) => [item_.path, item_]));
252
+ for (const [index, declared] of manifest.items.entries()) {
253
+ const segments = declared.path.split('/');
254
+ const file = path.join(canonicalSource, ...segments);
255
+ const normalized = path.normalize(file);
256
+ if (!normalized.startsWith(`${canonicalSource}${path.sep}`)) {
257
+ throw new TypeError(`manifest.items[${index}].path escapes the source directory`);
258
+ }
259
+ const bytes = await readStableRegularFile(file, `Manifest item ${declared.path}`);
260
+ const actualSha256 = createHash('sha256').update(bytes).digest('hex');
261
+ if (actualSha256 !== declared.sha256) {
262
+ throw new TypeError(`Manifest item ${declared.path} failed SHA-256 verification`);
263
+ }
264
+ }
245
265
  const seenFiles = new Set();
246
266
  const seenIds = new Set();
247
267
  const items = [];
@@ -414,7 +434,13 @@ function variantPayload(item, contentKey) {
414
434
  return payload;
415
435
  }
416
436
  function isProvenNotFound(error) {
417
- return (error instanceof CmsRequestError && error.operationKey === 'content.get' && error.status === 404);
437
+ return (error instanceof CmsRequestError &&
438
+ error.operationKey === 'content.get' &&
439
+ (error.status === 404 ||
440
+ ((error.status === 400 || error.status === undefined) &&
441
+ error.errorEntryCount === 1 &&
442
+ error.errorCode === 'INVALID_ID_FIELD' &&
443
+ error.responseMessage === 'Provide a valid content key, ID, or FQN.')));
418
444
  }
419
445
  function assertTransportableBody(value) {
420
446
  if (Array.isArray(value)) {
@@ -432,6 +458,16 @@ function assertTransportableBody(value) {
432
458
  for (const child of Object.values(value))
433
459
  assertTransportableBody(child);
434
460
  }
461
+ function selectedLandingPageTemplateReferences(source, selectedItems, resolvableRelationshipIds) {
462
+ const selectedIds = new Set(selectedItems.map(({ id }) => id));
463
+ return source.manifest.externalReferences.filter((reference) => {
464
+ if (reference.kind !== 'cms.relationship' || !selectedIds.has(reference.source.sourceId)) {
465
+ return true;
466
+ }
467
+ return (resolvableRelationshipIds !== undefined &&
468
+ !resolvableRelationshipIds.has(reference.referenceId));
469
+ });
470
+ }
435
471
  function selectedNativeReferences(source, selectedItems) {
436
472
  const selectedIds = new Set(selectedItems.map(({ id }) => id));
437
473
  const inventory = inventoryExportReferences(source.manifest.workspaceId,
@@ -452,7 +488,7 @@ function selectedNativeReferences(source, selectedItems) {
452
488
  source.manifest.items.some((item) => item.referenceId === reference.referenceId && item.path !== ownerEntry.file));
453
489
  });
454
490
  }
455
- function preflightReferences(plan, references = plan.source.manifest.externalReferences) {
491
+ function preflightReferences(plan, references = plan.source.manifest.externalReferences, allowValidatedStructuredReferences = false) {
456
492
  if (references.some((reference) => reference.required && reference.resolution !== 'included') ||
457
493
  plan.source.manifest.dependencies.length > 0) {
458
494
  throw new TypeError('Unresolved required package reference; import is blocked');
@@ -469,7 +505,8 @@ function preflightReferences(plan, references = plan.source.manifest.externalRef
469
505
  item.contentType === 'sfdc_cms__doc') {
470
506
  throw new TypeError('Unresolved content reference or media transport; import is blocked');
471
507
  }
472
- assertTransportableBody(item.contentBody);
508
+ if (!allowValidatedStructuredReferences)
509
+ assertTransportableBody(item.contentBody);
473
510
  if (item.externalSource !== undefined)
474
511
  assertTransportableBody(item.externalSource);
475
512
  const bodyUrl = item.contentBody['sfdc_cms:urlName'];
@@ -499,10 +536,24 @@ async function preflightConflicts(connection, plan, requestOptions) {
499
536
  }
500
537
  }
501
538
  }
539
+ function soqlString(value) {
540
+ return `'${value.replaceAll('\\', '\\\\').replaceAll("'", String.raw `\'`)}'`;
541
+ }
542
+ async function preflightApiNames(connection, plan) {
543
+ if (connection.query === undefined) {
544
+ throw new TypeError('Destination API-name absence cannot be proven; email-fragment apply is blocked');
545
+ }
546
+ for (const apiName of new Set(plan.groups.flatMap(({ primary, variants }) => [primary, ...variants].flatMap((item) => (item.apiName === undefined ? [] : [item.apiName]))))) {
547
+ const result = await connection.query(`SELECT ApiName FROM ManagedContent WHERE ApiName = ${soqlString(apiName)} LIMIT 1`);
548
+ if (result.totalSize !== 0 || result.records.length > 0) {
549
+ throw new Error(`API name ${apiName} already exists`);
550
+ }
551
+ }
552
+ }
502
553
  function compareMappingIdentity(left, right) {
503
554
  return `${left.kind}\u0000${left.referenceId}`.localeCompare(`${right.kind}\u0000${right.referenceId}`);
504
555
  }
505
- function importContractResult(source, destinationOrgId, plan, mappings, references) {
556
+ function importContractResult(source, destinationOrgId, plan, mappings, references, templatePrerequisites) {
506
557
  return {
507
558
  sourcePackage: {
508
559
  manifestSha256: source.manifestSha256,
@@ -515,11 +566,66 @@ function importContractResult(source, destinationOrgId, plan, mappings, referenc
515
566
  integrity: source.integrity,
516
567
  mappings: mappings.toSorted(compareMappingIdentity),
517
568
  references: references.toSorted(compareMappingIdentity),
569
+ ...(templatePrerequisites === undefined ? {} : { templatePrerequisites }),
518
570
  };
519
571
  }
520
572
  function jsonBytes(value) {
521
573
  return `${JSON.stringify(value, null, 2)}\n`;
522
574
  }
575
+ function sha256Json(value) {
576
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
577
+ }
578
+ function landingPagePhaseEvidence(proposal, item) {
579
+ const dependencyIndex = proposal.compatibility.findIndex(({ pageVariantId }) => pageVariantId === item.id);
580
+ const dependency = proposal.templateDependencies[dependencyIndex];
581
+ if (dependency?.validationStatus !== 'passed' || dependency.target === undefined) {
582
+ throw new TypeError('Landing-page target template validation evidence is incomplete');
583
+ }
584
+ return {
585
+ checkedAt: new Date().toISOString(),
586
+ status: 'passed',
587
+ template: structuredClone(dependency.target),
588
+ imageResolutionSha256: sha256Json(proposal.cmsPrerequisites),
589
+ dataGraphResolutionSha256: sha256Json(proposal.dataGraphs),
590
+ targetPageIdentitySha256: sha256Json({
591
+ apiName: item.apiName,
592
+ contentKey: item.contentKey,
593
+ contentType: item.contentType,
594
+ title: item.title,
595
+ urlName: item.urlName,
596
+ }),
597
+ };
598
+ }
599
+ function landingPageTemplateEvidence(source, proposal) {
600
+ return proposal.items.map((item, index) => {
601
+ const compatibility = proposal.compatibility[index];
602
+ const dependency = proposal.templateDependencies[index];
603
+ const pair = source.manifest.landingPageTemplatePairs?.find(({ page }) => page.variantId === compatibility.pageVariantId);
604
+ if (pair === undefined)
605
+ throw new TypeError('Landing-page template pair evidence is missing');
606
+ return {
607
+ sourcePage: pair.page,
608
+ sourceTemplate: pair.template,
609
+ requestedTargetSelector: {
610
+ ...(dependency.targetContentKey === undefined
611
+ ? {}
612
+ : { contentKey: dependency.targetContentKey }),
613
+ ...(dependency.targetApiName === undefined ? {} : { apiName: dependency.targetApiName }),
614
+ ...(dependency.targetTitle === undefined ? {} : { title: dependency.targetTitle }),
615
+ },
616
+ relationship: {
617
+ referenceId: compatibility.relationshipReferenceId,
618
+ descriptorSha256: compatibility.relationshipDescriptorSha256,
619
+ },
620
+ normalization: {
621
+ version: compatibility.normalizationVersion,
622
+ pageBodySha256: compatibility.normalizedPageBodySha256,
623
+ templateBodySha256: compatibility.normalizedTemplateBodySha256,
624
+ },
625
+ preflight: landingPagePhaseEvidence(proposal, item),
626
+ };
627
+ });
628
+ }
523
629
  async function writeExclusive(file, value) {
524
630
  const handle = await open(file, 'wx');
525
631
  try {
@@ -675,15 +781,43 @@ export async function executeWorkspaceImport(options) {
675
781
  }
676
782
  validateRelationships(source.items);
677
783
  const native = options.nativeCopyMappings !== undefined;
678
- if (native && options.identityMappings !== undefined)
784
+ const emailFragment = options.emailFragmentMappings !== undefined;
785
+ const webFragment = options.webFragmentMappings !== undefined;
786
+ const landingPageTemplate = options.landingPageTemplateMappings !== undefined;
787
+ const landingPage = options.landingPageMappings !== undefined;
788
+ if (Number(native) +
789
+ Number(emailFragment) +
790
+ Number(webFragment) +
791
+ Number(landingPageTemplate) +
792
+ Number(landingPage) +
793
+ Number(options.identityMappings !== undefined) >
794
+ 1)
679
795
  throw new TypeError('Choose one identity profile');
680
796
  if (options.editableDirectory !== undefined && !native)
681
797
  throw new TypeError('editableDirectory requires nativeCopyMappings');
682
798
  let proposal;
683
799
  let editableSource;
800
+ let landingPageTemplateProposal;
801
+ let landingPageProposal;
802
+ let webFragmentProposal;
684
803
  if (native) {
685
804
  ({ proposal, editableSource } = await planNativeWorkspaceImport(source, options.nativeCopyMappings, options.editableDirectory));
686
805
  }
806
+ else if (emailFragment) {
807
+ proposal = planEmailFragmentCopies(source, options.emailFragmentMappings);
808
+ }
809
+ else if (webFragment) {
810
+ webFragmentProposal = planWebFragmentCopies(source, options.webFragmentMappings);
811
+ proposal = webFragmentProposal;
812
+ }
813
+ else if (landingPageTemplate) {
814
+ landingPageTemplateProposal = planLandingPageTemplateCopies(source, options.landingPageTemplateMappings);
815
+ proposal = landingPageTemplateProposal;
816
+ }
817
+ else if (landingPage) {
818
+ landingPageProposal = planLandingPageCopies(source, options.landingPageMappings);
819
+ proposal = landingPageProposal;
820
+ }
687
821
  else if (options.identityMappings !== undefined)
688
822
  proposal = planImportIdentities(source, options.identityMappings);
689
823
  const selectedIds = new Set(proposal?.items.map((item) => item.id));
@@ -696,7 +830,9 @@ export async function executeWorkspaceImport(options) {
696
830
  retryable: false,
697
831
  },
698
832
  ];
699
- const selectedSource = native ? { ...source, items: proposal.items } : source;
833
+ const selectedSource = native || emailFragment || webFragment || landingPageTemplate || landingPage
834
+ ? { ...source, items: proposal.items }
835
+ : source;
700
836
  const sourcePlan = planWorkspaceImport(selectedSource, options.destinationWorkspace, options.workspaceId);
701
837
  // Keep original variant IDs and source manifest associations; never substitute the raw package.
702
838
  const proposedById = new Map(proposal?.items.map((item) => [item.id, item]));
@@ -711,23 +847,56 @@ export async function executeWorkspaceImport(options) {
711
847
  variants: group.variants.map((variant) => proposedById.get(variant.id)),
712
848
  })),
713
849
  };
714
- preflightReferences(plan, native ? selectedNativeReferences(source, proposal.items) : undefined);
850
+ let selectedReferences;
851
+ if (native)
852
+ selectedReferences = selectedNativeReferences(source, proposal.items);
853
+ else if (landingPageTemplate) {
854
+ selectedReferences = selectedLandingPageTemplateReferences(source, proposal.items);
855
+ }
856
+ else if (landingPage) {
857
+ selectedReferences = selectedLandingPageTemplateReferences(source, proposal.items, new Set(landingPageProposal.compatibility.map(({ relationshipReferenceId }) => relationshipReferenceId)));
858
+ }
859
+ preflightReferences(plan, selectedReferences, landingPageTemplate || landingPage);
715
860
  const requestOptions = options.requestOptions ?? {};
716
861
  if (!native)
717
862
  await preflightConflicts(options.connection, plan, requestOptions);
863
+ if (emailFragment || webFragment || landingPageTemplate || landingPage)
864
+ await preflightApiNames(options.connection, plan);
865
+ if (webFragment) {
866
+ await validateDataGraphPrerequisites(options.connection, webFragmentProposal.dataGraphs);
867
+ }
868
+ if (landingPageTemplate) {
869
+ await validateLandingPageTemplatePrerequisites(options.connection, plan.destinationWorkspaceId, landingPageTemplateProposal, requestOptions);
870
+ }
871
+ let templatePrerequisites;
872
+ if (landingPage) {
873
+ await resolveLandingPageTemplateDependencies(options.connection, plan.destinationWorkspaceId, landingPageProposal, requestOptions);
874
+ await validateLandingPagePrerequisites(options.connection, plan.destinationWorkspaceId, landingPageProposal, requestOptions);
875
+ templatePrerequisites = landingPageTemplateEvidence(source, landingPageProposal);
876
+ }
718
877
  const hasNamedIdentities = plan.groups.some((group) => [group.primary, ...group.variants].some((item) => item.apiName !== undefined ||
719
878
  item.urlName !== undefined ||
720
879
  item.contentBody['sfdc_cms:urlName'] !== undefined));
721
- if (!native && hasNamedIdentities && options.dryRun !== true) {
880
+ if (!native &&
881
+ !emailFragment &&
882
+ !webFragment &&
883
+ !landingPageTemplate &&
884
+ !landingPage &&
885
+ hasNamedIdentities &&
886
+ options.dryRun !== true) {
722
887
  throw new TypeError('Destination API-name/URL-name uniqueness cannot be verified by the supported CMS APIs; import is blocked');
723
888
  }
889
+ const pairedRelationshipIds = new Set(landingPageProposal?.compatibility.map(({ relationshipReferenceId }) => relationshipReferenceId));
724
890
  const references = source.manifest.externalReferences
725
891
  .filter(({ resolution }) => resolution !== 'included')
726
- .map(({ kind, referenceId, resolution }) => ({
727
- referenceId,
728
- kind,
729
- status: resolution === 'unsupported' ? 'unsupported' : 'unresolved',
730
- }))
892
+ .map(({ kind, referenceId, resolution }) => {
893
+ let status = 'unresolved';
894
+ if (pairedRelationshipIds.has(referenceId))
895
+ status = 'resolved';
896
+ else if (resolution === 'unsupported')
897
+ status = 'unsupported';
898
+ return { referenceId, kind, status };
899
+ })
731
900
  .toSorted(compareMappingIdentity);
732
901
  if (options.dryRun === true) {
733
902
  const diagnostics = [
@@ -748,15 +917,18 @@ export async function executeWorkspaceImport(options) {
748
917
  if (hasNamedIdentities && !native) {
749
918
  diagnostics.unshift({
750
919
  code: 'NAME_AVAILABILITY_UNVERIFIED',
751
- message: 'Destination API-name/URL-name availability is unverified; named apply remains blocked until collision evidence is available.',
920
+ message: emailFragment || webFragment || landingPageTemplate || landingPage
921
+ ? `Destination API-name absence was proven by exact ManagedContent.ApiName equality lookup;${webFragment || landingPageTemplate || landingPage ? ' every named prerequisite was proven in exact type/workspace scope;' : ''} other server conflict behavior remains unverified.`
922
+ : 'Destination API-name/URL-name availability is unverified; named apply remains blocked until collision evidence is available.',
752
923
  retryable: false,
753
924
  });
754
925
  }
755
926
  return {
756
927
  diagnostics,
757
- contractResult: importContractResult(source, options.destinationOrgId, plan, [], references),
928
+ contractResult: importContractResult(source, options.destinationOrgId, plan, [], references, templatePrerequisites),
758
929
  dryRun: true,
759
930
  plan,
931
+ ...(templatePrerequisites === undefined ? {} : { templatePrerequisites }),
760
932
  };
761
933
  }
762
934
  if (!nonemptyString(options.reportDirectory)) {
@@ -767,10 +939,27 @@ export async function executeWorkspaceImport(options) {
767
939
  const reportFile = path.join(reportDirectory, 'workspace-import-run.json');
768
940
  const report = {
769
941
  ...(editableSource === undefined ? {} : { editableSource }),
942
+ ...(landingPageTemplateProposal === undefined && landingPageProposal === undefined
943
+ ? {}
944
+ : {
945
+ cmsPrerequisites: landingPageTemplateProposal?.cmsPrerequisites ?? landingPageProposal.cmsPrerequisites,
946
+ }),
947
+ ...(webFragmentProposal === undefined &&
948
+ landingPageTemplateProposal === undefined &&
949
+ landingPageProposal === undefined
950
+ ? {}
951
+ : {
952
+ dataGraphPrerequisites: webFragmentProposal?.dataGraphs ??
953
+ landingPageTemplateProposal?.dataGraphs ??
954
+ landingPageProposal.dataGraphs,
955
+ }),
770
956
  createdParents: [],
771
957
  destinationOrgId: options.destinationOrgId,
772
958
  destinationWorkspaceId: plan.destinationWorkspaceId,
773
959
  operations: [],
960
+ ...(templatePrerequisites === undefined
961
+ ? {}
962
+ : { templatePrerequisites: structuredClone(templatePrerequisites) }),
774
963
  runId: randomUUID(),
775
964
  sourceDirectory: source.sourceDirectory,
776
965
  sourceManifestSha256: source.manifestSha256,
@@ -808,8 +997,14 @@ export async function executeWorkspaceImport(options) {
808
997
  await writeExclusive(reportFile, report);
809
998
  try {
810
999
  for (const group of plan.groups) {
1000
+ if (landingPageTemplate) {
1001
+ await validateLandingPageTemplatePrerequisites(options.connection, plan.destinationWorkspaceId, landingPageTemplateProposal, requestOptions);
1002
+ }
1003
+ const finalPrimary = proposal?.items.find((item) => item.id === group.primary.id) ?? group.primary;
1004
+ const finalVariants = group.variants.map((variant) => proposal?.items.find((item) => item.id === variant.id) ?? variant);
1005
+ const finalGroup = { ...group, primary: finalPrimary, variants: finalVariants };
811
1006
  const groupReferenceKeys = new Set();
812
- for (const item_ of [group.primary, ...group.variants]) {
1007
+ for (const item_ of [finalGroup.primary, ...finalGroup.variants]) {
813
1008
  for (const key of cmsContentBodyReferenceKeys(item_.contentBody, sourceReferenceValues)) {
814
1009
  groupReferenceKeys.add(key);
815
1010
  }
@@ -826,11 +1021,11 @@ export async function executeWorkspaceImport(options) {
826
1021
  }
827
1022
  }
828
1023
  const rewrittenGroup = hasUnknownRequiredMapping
829
- ? group
1024
+ ? finalGroup
830
1025
  : {
831
- ...group,
832
- primary: rewriteItem(group.primary, replacements),
833
- variants: group.variants.map((variant) => rewriteItem(variant, replacements)),
1026
+ ...finalGroup,
1027
+ primary: rewriteItem(finalGroup.primary, replacements),
1028
+ variants: finalGroup.variants.map((variant) => rewriteItem(variant, replacements)),
834
1029
  };
835
1030
  const parentPayload = createPayload(rewrittenGroup, native ? plan.destinationWorkspaceId : plan.rootFolderId);
836
1031
  if (native) {
@@ -838,18 +1033,60 @@ export async function executeWorkspaceImport(options) {
838
1033
  delete parentPayload.externalId;
839
1034
  delete parentPayload.externalSource;
840
1035
  }
1036
+ if (emailFragment || webFragment || landingPageTemplate || landingPage) {
1037
+ const currentPlan = {
1038
+ ...plan,
1039
+ groups: [rewrittenGroup],
1040
+ };
1041
+ await preflightConflicts(options.connection, currentPlan, requestOptions);
1042
+ await preflightApiNames(options.connection, currentPlan);
1043
+ if (webFragment) {
1044
+ await validateDataGraphPrerequisites(options.connection, webFragmentProposal.dataGraphs);
1045
+ }
1046
+ }
1047
+ let finalPayload = parentPayload;
1048
+ if (landingPage) {
1049
+ const freshProposal = planLandingPageCopies(source, options.landingPageMappings);
1050
+ await resolveLandingPageTemplateDependencies(options.connection, plan.destinationWorkspaceId, freshProposal, requestOptions);
1051
+ await validateLandingPagePrerequisites(options.connection, plan.destinationWorkspaceId, freshProposal, requestOptions);
1052
+ const freshItem = freshProposal.items.find(({ id }) => id === group.primary.id);
1053
+ if (freshItem === undefined)
1054
+ throw new TypeError('Landing-page pre-create item is missing');
1055
+ const evidenceIndex = report.templatePrerequisites?.findIndex(({ sourcePage }) => sourcePage.variantId === group.primary.id);
1056
+ if (evidenceIndex === undefined || evidenceIndex < 0) {
1057
+ throw new TypeError('Landing-page durable template evidence is missing');
1058
+ }
1059
+ const preflight = report.templatePrerequisites[evidenceIndex].preflight;
1060
+ const preCreate = landingPagePhaseEvidence(freshProposal, freshItem);
1061
+ if (!isDeepStrictEqual(preflight.template, preCreate.template) ||
1062
+ preflight.imageResolutionSha256 !== preCreate.imageResolutionSha256 ||
1063
+ preflight.dataGraphResolutionSha256 !== preCreate.dataGraphResolutionSha256 ||
1064
+ preflight.targetPageIdentitySha256 !== preCreate.targetPageIdentitySha256) {
1065
+ throw new TypeError('Landing-page template, image, Data Graph, or target page identity drifted before CREATE');
1066
+ }
1067
+ report.templatePrerequisites[evidenceIndex].preCreate = preCreate;
1068
+ finalPayload = createPayload({ ...group, primary: freshItem, variants: [] }, plan.rootFolderId);
1069
+ }
841
1070
  const parentOperation = editableSource === undefined
842
- ? pendingOperation(report, 'create-parent', group.contentKey, group.primary.language, parentPayload)
1071
+ ? pendingOperation(report, 'create-parent', group.contentKey, group.primary.language, finalPayload)
843
1072
  : report.operations.find((operation) => operation.contentKey === group.contentKey);
844
1073
  if (editableSource === undefined)
845
1074
  report.operations.push(parentOperation);
1075
+ if (landingPage && report.templatePrerequisites !== undefined) {
1076
+ const evidence = report.templatePrerequisites.find(({ sourcePage }) => sourcePage.variantId === group.primary.id);
1077
+ if (evidence === undefined)
1078
+ throw new TypeError('Landing-page journal evidence is missing');
1079
+ evidence.finalRequestSha256 = createHash('sha256')
1080
+ .update(JSON.stringify(finalPayload))
1081
+ .digest('hex');
1082
+ }
846
1083
  else if (parentOperation.requestSha256 !==
847
- operationIdentity(report.runId, 'create-parent', group.contentKey, group.primary.language, parentPayload).requestSha256)
1084
+ operationIdentity(report.runId, 'create-parent', group.contentKey, group.primary.language, finalPayload).requestSha256)
848
1085
  throw new TypeError('Editable CREATE payload changed after initial journal');
849
1086
  await rewrite(reportFile, report);
850
1087
  let parentResponse;
851
1088
  try {
852
- parentResponse = await createContent(options.connection, parentPayload, requestOptions);
1089
+ parentResponse = await createContent(options.connection, finalPayload, requestOptions);
853
1090
  }
854
1091
  catch (error) {
855
1092
  parentOperation.state = native ? 'pending' : 'failed';
@@ -887,6 +1124,17 @@ export async function executeWorkspaceImport(options) {
887
1124
  contentKey: created.contentKey,
888
1125
  primaryVariantId: created.primaryVariantId,
889
1126
  };
1127
+ if (landingPage && report.templatePrerequisites !== undefined) {
1128
+ const evidence = report.templatePrerequisites.find(({ sourcePage }) => sourcePage.variantId === group.primary.id);
1129
+ if (evidence === undefined)
1130
+ throw new TypeError('Landing-page result evidence is missing');
1131
+ evidence.returned = {
1132
+ contentId: created.contentId,
1133
+ contentKey: created.contentKey,
1134
+ primaryVariantId: created.primaryVariantId,
1135
+ state: 'unverified',
1136
+ };
1137
+ }
890
1138
  await persistOperationResult(reportFile, report, parentOperation, rewrite);
891
1139
  if (native) {
892
1140
  if (source.items.some((item) => item.contentKey === created.contentKey ||
@@ -984,9 +1232,12 @@ export async function executeWorkspaceImport(options) {
984
1232
  }
985
1233
  return {
986
1234
  diagnostics: editableDiagnostics,
987
- contractResult: importContractResult(source, options.destinationOrgId, plan, mappings, references),
1235
+ contractResult: importContractResult(source, options.destinationOrgId, plan, mappings, references, report.templatePrerequisites),
988
1236
  dryRun: false,
989
1237
  plan,
1238
+ ...(report.templatePrerequisites === undefined
1239
+ ? {}
1240
+ : { templatePrerequisites: report.templatePrerequisites }),
990
1241
  report,
991
1242
  reportFile,
992
1243
  };