1'use strict';23const crypto = require('node:crypto');45// The LEGACY Contacts V4 custom-field path, and only that path. A migration reaches it6// only by routing a mapping explicitly to `contactWriteSurface: "contacts-v4"`; the default V57// contact goes through the Data Extension Schema helper (FQDN wix.contacts.*.contact) instead.8// The two are never both attempted for one field, and a failure on one never falls back to the9// other: their values live in different places (`info.extendedFields` vs10// `extendedFields.namespaces`) and pair with different contact writers.11//12// API facts this relies on (docs re-read 2026-09-29):
39 if (!requirement || requirement.kind !== 'extendedFieldSchema') {
40 return { valid: false, errors: fail([], 'not-an-extended-field-requirement', 'requirement.kind must be "extendedFieldSchema"') };
41 }
42 if (requirement.schemaKind !== SCHEMA_KIND) {
43 fail(errors, 'unsupported-schema-kind', `this helper implements ${SCHEMA_KIND} only, not "${requirement.schemaKind}"`);
44 }
45 if (requirement.contactWriteSurface !== CONTACT_WRITE_SURFACE) {
46 // V4 is opt-in. Without the explicit surface this is a V5 requirement in the wrong shape.
47 fail(errors, 'contact-surface-mismatch', `${SCHEMA_KIND} requires contactWriteSurface "${CONTACT_WRITE_SURFACE}", not "${requirement.contactWriteSurface}"`);
48 }
49 if (requirement.targetRef !== undefined && requirement.targetRef !== 'crm/contact') {
50 fail(errors, 'target-ref-mismatch', `a ${SCHEMA_KIND} requirement targets crm/contact, not "${requirement.targetRef}"`);
51 }
52 if (requirement.fqdn || requirement.namespace) {
53 // The DES fields on a V4 requirement mean someone mixed the two surfaces.
54 fail(errors, 'contact-surface-mismatch', `${SCHEMA_KIND} does not take fqdn/namespace; those belong to the contacts-v5 Data Extension Schema path`);
55 }
56 if (!Array.isArray(requirement.fields) || requirement.fields.length === 0) {
57 fail(errors, 'no-fields-declared', 'a requirement with no fields provisions nothing');
58 return { valid: false, errors };
59 }
60 const seen = new Set();
61 for (const field of requirement.fields) {
62 if (!field || typeof field !== 'object') {
63 fail(errors, 'field-not-an-object', 'a field intent must be an object');
72 if (seen.has(normalized)) fail(errors, 'duplicate-display-name', `displayName "${field.displayName}" is declared twice`);
73 seen.add(normalized);
74 }
75 if (!field.dataType) {
76 fail(errors, 'missing-data-type', `field "${field.displayName}" has no dataType`);
77 } else if (!DOCUMENTED_CONTACT_DATA_TYPES.has(field.dataType)) {
78 fail(errors, 'unsupported-contact-data-type', `dataType "${field.dataType}" is not a Wix contact field type`);
79 } else if (!VERIFIED_CONTACT_DATA_TYPES.has(field.dataType)) {
80 fail(errors, 'unverified-contact-data-type', `dataType "${field.dataType}" is not verified for this phase; only ${[...VERIFIED_CONTACT_DATA_TYPES].join(', ')} is`);
81 }
82 if (field.expectedNamespace !== undefined && field.expectedNamespace !== EXPECTED_NAMESPACE) {
83 fail(errors, 'unexpected-contact-namespace', `created contact fields land in "${EXPECTED_NAMESPACE}", not "${field.expectedNamespace}"`);
84 }
85 if (field.writePath && !/^info\.extendedFields\./.test(field.writePath)) {
86 fail(errors, 'write-path-mismatch', `a contacts-v4 value lands under info.extendedFields, not "${field.writePath}"`);
87 }
88 if (field.writePathTemplate !== undefined && field.writePathTemplate !== 'info.extendedFields.<verifiedKey>') {
89 fail(errors, 'write-path-mismatch', `writePathTemplate must be "info.extendedFields.<verifiedKey>", not "${field.writePathTemplate}"`);