@flamingo-stack/openframe-frontend-core 0.0.676 → 0.0.677
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/components/meeting-scheduler/booking-form.d.ts +21 -2
- package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +92 -38
- package/dist/components/meeting-scheduler/index.cjs.map +1 -1
- package/dist/components/meeting-scheduler/index.d.ts +7 -6
- package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.js +66 -12
- package/dist/components/meeting-scheduler/index.js.map +1 -1
- package/dist/schemas/meeting-booking-schema.cjs +16 -0
- package/dist/schemas/meeting-booking-schema.cjs.map +1 -1
- package/dist/schemas/meeting-booking-schema.d.ts +34 -0
- package/dist/schemas/meeting-booking-schema.d.ts.map +1 -1
- package/dist/schemas/meeting-booking-schema.js +15 -1
- package/dist/schemas/meeting-booking-schema.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/meeting-scheduler-details-first.test.tsx +42 -1
- package/src/components/meeting-scheduler/booking-form.tsx +91 -17
- package/src/components/meeting-scheduler/index.tsx +7 -5
- package/src/schemas/meeting-booking-schema.ts +34 -0
|
@@ -260,6 +260,20 @@ function makeDeferredBookingSchema(formFields, legalConsent) {
|
|
|
260
260
|
timezone: TIMEZONE.nullish()
|
|
261
261
|
});
|
|
262
262
|
}
|
|
263
|
+
function emailDomainDenied(email, rule) {
|
|
264
|
+
if (typeof email !== "string") return false;
|
|
265
|
+
const domain = email.split("@")[1]?.trim().toLowerCase();
|
|
266
|
+
if (domain === void 0 || domain === "") return false;
|
|
267
|
+
return rule.domains.includes(domain);
|
|
268
|
+
}
|
|
269
|
+
function withDeniedEmailDomains(schema, rule) {
|
|
270
|
+
if (!rule) return schema;
|
|
271
|
+
return schema.superRefine((values, ctx) => {
|
|
272
|
+
if (emailDomainDenied(values.email, rule)) {
|
|
273
|
+
ctx.addIssue({ code: "custom", path: ["email"], message: rule.message });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
263
277
|
|
|
264
278
|
exports.BUILT_IN_BOOKING_FIELDS = BUILT_IN_BOOKING_FIELDS;
|
|
265
279
|
exports.DECIMAL_LITERAL_RE = DECIMAL_LITERAL_RE;
|
|
@@ -271,6 +285,7 @@ exports.MULTI_VALUE_SEPARATOR = MULTI_VALUE_SEPARATOR;
|
|
|
271
285
|
exports.PHONE_RE = PHONE_RE;
|
|
272
286
|
exports.SUPPORTED_FORM_FIELD_TYPES = SUPPORTED_FORM_FIELD_TYPES;
|
|
273
287
|
exports.blocksNativeBooking = blocksNativeBooking;
|
|
288
|
+
exports.emailDomainDenied = emailDomainDenied;
|
|
274
289
|
exports.fieldTypeSpec = fieldTypeSpec;
|
|
275
290
|
exports.formFieldTypeHasOptions = formFieldTypeHasOptions;
|
|
276
291
|
exports.isRecognisedFormFieldType = isRecognisedFormFieldType;
|
|
@@ -283,5 +298,6 @@ exports.normalizeFormField = normalizeFormField;
|
|
|
283
298
|
exports.normalizeFormFields = normalizeFormFields;
|
|
284
299
|
exports.resolveFormFieldControl = resolveFormFieldControl;
|
|
285
300
|
exports.splitMultiValue = splitMultiValue;
|
|
301
|
+
exports.withDeniedEmailDomains = withDeniedEmailDomains;
|
|
286
302
|
//# sourceMappingURL=meeting-booking-schema.cjs.map
|
|
287
303
|
//# sourceMappingURL=meeting-booking-schema.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/schemas/meeting-booking-schema.ts"],"names":["z"],"mappings":"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","file":"meeting-booking-schema.cjs","sourcesContent":["import { z } from 'zod';\n\n/**\n * Meeting-booking wire contracts + validation factory.\n *\n * SERVER-SAFE tsup entry (no \"use client\" banner) with its OWN per-file\n * subpath (`./schemas/meeting-booking-schema`) — same pattern and reasons as\n * `schemas/contact-schema`: used by BOTH the lib's `<HubSpotMeetingScheduler>`\n * (client-side validation) AND the host's server-side booking route, which\n * REBUILDS the schema from the link's own fetched metadata and never trusts a\n * client-shaped instance. zod is an optional peer quarantined to per-subpath\n * verticals — do NOT re-export this module through any broad barrel.\n *\n * Every cross-boundary type for the scheduling feature lives HERE (the lib\n * owns the contract; hosts import this subpath directly, type-only where\n * possible).\n */\n\n// ---------------------------------------------------------------------------\n// Wire types\n// ---------------------------------------------------------------------------\n\n/**\n * Sanitized availability payload served by the host proxy\n * (`GET {apiBaseUrl}/api/meetings/availability?meeting=<id>&monthOffset=<n>`).\n *\n * NO timezone field on purpose: slot starts are absolute epoch-ms instants\n * (verified timezone-independent against the live API), upstream fetches are\n * UTC-pinned, and ALL zone rendering happens client-side. Slots must be\n * whitelist-copied from HubSpot's `linkAvailability` ONLY — never derived\n * from busy-time data.\n */\nexport interface MeetingAvailability {\n meetingId: string;\n monthOffset: number;\n hasMore: boolean;\n /** Offered durations in ms — HubSpot's native unit for the booking POST. */\n durationsMs: number[];\n /** Bookable slot start times (epoch ms), keyed by duration in ms. */\n slotsByDurationMs: Record<string, number[]>;\n formFields: MeetingFormField[];\n /** Verbatim whitelist-copy of HubSpot's `legalConsentOptions` when consent is enabled; null when disabled. */\n legalConsent: MeetingLegalConsent | null;\n /**\n * Who the visitor is meeting — whitelisted DISPLAY projection the host DAL\n * builds from its own people data (e.g. a profiles table matched\n * server-side). NEVER carries emails or busy-time data; optional so\n * existing hosts stay wire-compatible.\n */\n hosts?: MeetingHost[];\n}\n\n/** Display-only host identity for the scheduler's context panel. */\nexport interface MeetingHost {\n name: string;\n avatarUrl: string | null;\n /** Job title / role line under the name (null → omitted). */\n title: string | null;\n}\n\n/**\n * One scheduling link on the DIRECTORY wire (`GET /api/meetings`) — the\n * host-DAL whitelist projection consumed by `MeetingSchedulerDirectory` and\n * host pages. Never carries organizer emails/busy-time data.\n */\nexport interface SchedulingLink {\n id: string;\n /** The link's public HubSpot booking URL — escape-hatch target only. */\n link: string;\n /** HubSpot slug path — the row's in-app destination is `<basePath>/<slug>`. */\n slug: string;\n /** Audience group key (slugified audience label; `\"other\"` in scope=all). */\n purpose: string;\n title: string;\n description: string | null;\n kind: 'personal' | 'team';\n /** Display-only minutes projection (booking stays ms end-to-end). */\n durationsMinutes: number[];\n hosts: MeetingHost[];\n /** Earliest bookable slot (epoch ms) from the current-month payload. */\n nextAvailableMs: number | null;\n}\n\nexport interface SchedulingLinksPayload {\n purposes: Array<{ purpose: string; label: string; links: SchedulingLink[] }>;\n fetchedAt: string;\n}\n\n/**\n * One declared question as the host forwards it. `type` is HubSpot's\n * `fieldType` (the control HubSpot draws: `phonenumber`, `booleancheckbox`…)\n * VERBATIM, and `dataType` its `type` (the stored value's kind: `string`,\n * `enumeration`, `phone_number`…). Neither is a closed set: the widget resolves\n * ANY pair to a control through `resolveFormFieldControl`, so a type HubSpot\n * adds tomorrow renders as the nearest control instead of taking the form down.\n */\nexport interface MeetingFormField {\n name: string;\n label: string;\n type: string;\n /** HubSpot's property data type — the fallback when `type` is unknown. Optional: older hosts omit it. */\n dataType?: string;\n required: boolean;\n /** Option VALUES (what is submitted). */\n options?: string[];\n /** Option value → display label, when HubSpot's label differs from the value. */\n optionLabels?: Record<string, string>;\n}\n\n/**\n * HubSpot's consent copy, rendered VERBATIM by the widget (GDPR surface —\n * never edited, never summarized). Responses are keyed by\n * `communicationTypeId`.\n */\nexport interface MeetingLegalConsent {\n processingConsentText: string;\n processingConsentCheckboxLabel: string | null;\n communicationConsentText: string | null;\n communicationConsentCheckboxes: Array<{\n communicationTypeId: string;\n label: string;\n required: boolean;\n }>;\n privacyPolicyText: string | null;\n isLegitimateInterest: boolean;\n}\n\n/**\n * Whitelisted booking result returned by the host proxy — the THIRD HubSpot\n * payload that reaches a browser, so it gets the same whitelist-copy\n * treatment as the two GETs. Nothing organizer-derived.\n */\nexport interface BookingConfirmation {\n meetingId: string;\n title: string;\n startTimeMs: number;\n durationMs: number;\n}\n\n/**\n * Typed domain errors the booking route emits; the widget keys its recovery\n * UI off these. `SLOT_TAKEN` → refetch-and-recover; `TEMPORARILY_UNAVAILABLE`\n * → retry affordance; `MEETING_UNAVAILABLE` → daily ceiling exhausted\n * (escape hatch, not a retry timer); `LINK_GONE` → link deleted upstream;\n * `INVALID_EMAIL` → HubSpot rejected the attendee address (its\n * MeetingsBookingCreatedError.INVALID_EMAIL class — fake/unreachable\n * mailboxes), so the widget tells the visitor to fix the email instead of\n * blaming the slot or their other details.\n */\nexport const MEETING_BOOKING_ERROR_CODES = [\n 'SLOT_TAKEN',\n 'VALIDATION',\n 'INVALID_EMAIL',\n 'LINK_GONE',\n 'TEMPORARILY_UNAVAILABLE',\n 'MEETING_UNAVAILABLE',\n] as const;\n\n/**\n * Derived from the runtime array ABOVE — the array is the single source of\n * truth. The booking hook validates server codes against it at runtime, so a\n * code added only to a hand-written type would be silently coerced to\n * TEMPORARILY_UNAVAILABLE by every deployed widget (exactly how the\n * INVALID_EMAIL rollout mis-rendered on 2026-08-27: type extended, runtime\n * allowlist stale — an array annotation is not exhaustiveness-checked).\n */\nexport type MeetingBookingErrorCode = (typeof MEETING_BOOKING_ERROR_CODES)[number];\n\n// ---------------------------------------------------------------------------\n// Field-type vocabulary — ONE set drives the renderer AND the validator\n// ---------------------------------------------------------------------------\n\n/**\n * THE registry of CONTROLS the native form can draw — one entry per control,\n * and the ONLY place a control is declared. Everything else derives from it:\n * `SupportedFormFieldType` is its key union, `SUPPORTED_FORM_FIELD_TYPES` its\n * keys, and `makeBookingSchema` maps each answer through the entry's\n * validator. The widget's control table (`booking-form.tsx`) is a `Record`\n * over the same key union, so a control added here without a renderer is a\n * COMPILE error, not a silent gap.\n *\n * These are controls, NOT HubSpot's type vocabulary. A HubSpot question\n * reaches a control through `resolveFormFieldControl` (exact `fieldType`, then\n * its data type, then a generic fallback), so the open set of HubSpot types\n * maps onto this closed set of controls and no type is ever \"unsupported\".\n * The one thing that still falls back to HubSpot's own page is a REQUIRED\n * question no control can answer (a file upload) — see `blocksNativeBooking`.\n *\n * Every string control rides the wire as a STRING (HubSpot's book endpoint\n * takes `{ name, value: string }`); `checkbox` is the one boolean. `number` is\n * a decimal literal, `multiselect` HubSpot's `;`-joined enumeration value,\n * `date` an ISO `YYYY-MM-DD`.\n */\nexport interface FormFieldTypeSpec {\n /** The answer's wire shape, which also decides how required/optional wraps it. */\n kind: 'string' | 'boolean';\n /** Whether HubSpot publishes `options` for the type (the pickers). */\n hasOptions: boolean;\n /**\n * The validator for ONE answer. String types EXTEND `base`, which already\n * carries the required-ness (`min(1)` when required) — so \"X is required\" is\n * the first issue reported for an empty answer, ahead of the type's own rule.\n * Boolean types ignore it.\n */\n validator: (field: MeetingFormField, base: z.ZodString) => z.ZodTypeAny;\n /** The control's placeholder, derived from the field (the mock's \"Enter Company Name\"). */\n placeholder?: (field: MeetingFormField) => string;\n}\n\nconst optionValidator: FormFieldTypeSpec['validator'] = (field, base) =>\n base.refine(v => !v || (field.options ?? []).includes(v), {\n message: `Please choose a valid option for ${field.label}`,\n });\n\n/** The wire shape of a number answer — what the form canonicalises TO and the validator checks. */\nexport const DECIMAL_LITERAL_RE = /^-?\\d+(\\.\\d+)?$/;\n\n/** HubSpot's separator for a multi-value enumeration (`a;b;c`). */\nexport const MULTI_VALUE_SEPARATOR = ';';\n\n/** Split a multiselect answer into its values (empty string → none). */\nexport const splitMultiValue = (value: unknown): string[] =>\n typeof value === 'string' && value ? value.split(MULTI_VALUE_SEPARATOR) : [];\n\n/**\n * A phone answer: digits with the punctuation people actually type (`+1 (415)\n * 555-2671`, `020 7946 0958 ext. 12`). Deliberately a SHAPE check, not a\n * numbering-plan check — HubSpot stores the string as typed, and rejecting a\n * valid number we failed to parse is worse than accepting an odd one.\n */\nexport const PHONE_RE = /^\\+?[\\d\\s().\\-/]+(?:\\s*(?:x|ext\\.?)\\s*\\d+)?$/i;\nconst PHONE_MIN_DIGITS = 5;\nconst PHONE_MAX_DIGITS = 20;\nconst phoneDigits = (v: string) => v.replace(/\\D/g, '').length;\n\n/** A date answer as `<input type=\"date\">` emits it. */\nexport const ISO_DATE_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst isRealIsoDate = (v: string) => {\n if (!ISO_DATE_RE.test(v)) return false;\n const d = new Date(`${v}T00:00:00Z`);\n return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === v;\n};\n\nexport const FORM_FIELD_TYPES = {\n text: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) => base.max(1000, { message: `${field.label} is too long` }),\n placeholder: field => `Enter ${field.label}`,\n },\n textarea: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) => base.max(5000, { message: `${field.label} is too long` }),\n placeholder: field => `Enter text${field.required ? '' : ' (optional)'}`,\n },\n number: {\n kind: 'string',\n hasOptions: false,\n // Canonical decimal literal — the control normalises what the browser\n // accepts (`1e3`, `007`) to this before it is validated or sent.\n validator: (field, base) =>\n base\n .max(32, { message: `${field.label} is too long` })\n .regex(DECIMAL_LITERAL_RE, { message: `${field.label} must be a number` }),\n },\n phone: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) =>\n base\n .max(40, { message: `${field.label} is too long` })\n .refine(\n v => !v || (PHONE_RE.test(v) && phoneDigits(v) >= PHONE_MIN_DIGITS && phoneDigits(v) <= PHONE_MAX_DIGITS),\n { message: `Please enter a valid phone number for ${field.label}` },\n ),\n placeholder: field => `Enter ${field.label}`,\n },\n date: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) => base.refine(v => !v || isRealIsoDate(v), { message: `${field.label} must be a date` }),\n },\n select: { kind: 'string', hasOptions: true, validator: optionValidator },\n radio: { kind: 'string', hasOptions: true, validator: optionValidator },\n multiselect: {\n kind: 'string',\n hasOptions: true,\n validator: (field, base) =>\n base.refine(v => splitMultiValue(v).every(part => (field.options ?? []).includes(part)), {\n message: `Please choose valid options for ${field.label}`,\n }),\n },\n checkbox: { kind: 'boolean', hasOptions: false, validator: () => z.boolean() },\n} as const satisfies Record<string, FormFieldTypeSpec>;\n\nexport type SupportedFormFieldType = keyof typeof FORM_FIELD_TYPES;\n\nexport const SUPPORTED_FORM_FIELD_TYPES = Object.keys(FORM_FIELD_TYPES) as readonly SupportedFormFieldType[];\n\n// ---------------------------------------------------------------------------\n// HubSpot type → control resolution (open set in, closed set out)\n// ---------------------------------------------------------------------------\n\n/**\n * What a HubSpot question resolves to: a drawable control, `display` (a\n * read-only block — `html`, `calculation_*` — never rendered, never\n * submitted) or `unanswerable` (nothing the booking POST can carry, e.g. a\n * file upload).\n */\nexport type FormFieldResolution = SupportedFormFieldType | 'display' | 'unanswerable';\n\n/**\n * HubSpot `fieldType` spellings → resolution. Keys are lower-cased. A spelling\n * missing here is NOT an error: resolution falls through to the data type and\n * then to a generic control. Add an entry only to draw a BETTER control for a\n * type, never to make one \"work\".\n *\n * `checkbox` is absent on purpose: in HubSpot it is \"multiple checkboxes\" (an\n * enumeration with options), while older links sent it for a single yes/no —\n * `resolveFormFieldControl` decides by whether options came with it.\n */\nconst FIELD_TYPE_CONTROLS: Readonly<Record<string, FormFieldResolution>> = {\n text: 'text',\n textarea: 'textarea',\n number: 'number',\n select: 'select',\n radio: 'radio',\n booleancheckbox: 'checkbox',\n phonenumber: 'phone',\n date: 'date',\n html: 'display',\n file: 'unanswerable',\n};\n\n/** HubSpot property data types (`type`) → resolution, used when `fieldType` is unknown. */\nconst DATA_TYPE_CONTROLS: Readonly<Record<string, FormFieldResolution>> = {\n string: 'text',\n number: 'number',\n enumeration: 'select',\n bool: 'checkbox',\n date: 'date',\n datetime: 'date',\n phone_number: 'phone',\n};\n\nconst hasDeclaredOptions = (field: MeetingFormField) => (field.options?.length ?? 0) > 0;\n\n/**\n * THE one mapping from a HubSpot question to what the form does with it.\n * Total: every input yields a resolution, so an unrecognised type can never\n * make a link unbookable — at worst it is a text box. An option control with\n * no options degrades to text (nothing to pick from, but still answerable).\n */\nexport function resolveFormFieldControl(field: MeetingFormField): FormFieldResolution {\n const fieldType = (field.type ?? '').toLowerCase();\n const withOptions = hasDeclaredOptions(field);\n const usable = (r: FormFieldResolution | undefined): FormFieldResolution | undefined => {\n if (!r || r === 'display' || r === 'unanswerable') return r;\n return FORM_FIELD_TYPES[r].hasOptions && !withOptions ? 'text' : r;\n };\n\n if (fieldType === 'checkbox') return withOptions ? 'multiselect' : 'checkbox';\n if (fieldType.startsWith('calculation')) return 'display';\n // A control name resolves to itself, so resolution is IDEMPOTENT: the form\n // resolves once to draw, then hands the resolved fields to the schema\n // factory, which resolves again (the server rebuild resolves raw ones).\n const asControl = Object.prototype.hasOwnProperty.call(FORM_FIELD_TYPES, fieldType)\n ? (fieldType as SupportedFormFieldType)\n : undefined;\n return (\n usable(FIELD_TYPE_CONTROLS[fieldType] ?? asControl) ??\n usable(DATA_TYPE_CONTROLS[(field.dataType ?? '').toLowerCase()]) ??\n (withOptions ? 'select' : 'text')\n );\n}\n\n/**\n * @deprecated Kept for hosts built against the closed six-type registry (they\n * asked this to decide whether to forward `options`). Answers for any HubSpot\n * type through the resolver: true when the type resolves to an option control\n * once options are present. New hosts forward options whenever HubSpot sends\n * them and do not need this.\n */\nexport function formFieldTypeHasOptions(type: string): boolean {\n const control = resolveFormFieldControl({ name: '', label: '', type, required: false, options: ['_'] });\n return control !== 'display' && control !== 'unanswerable' && FORM_FIELD_TYPES[control].hasOptions;\n}\n\n/** @deprecated The option controls (see `formFieldTypeHasOptions`). */\nexport const FORM_FIELD_TYPES_WITH_OPTIONS: readonly SupportedFormFieldType[] = SUPPORTED_FORM_FIELD_TYPES.filter(\n type => FORM_FIELD_TYPES[type].hasOptions,\n);\n\n/**\n * Whether the question's `fieldType` is one the resolver maps EXPLICITLY — as\n * opposed to guessing from its data type or falling back to text. A host logs\n * the false case: the form still works, but a better control may be one table\n * entry away.\n */\nexport function isRecognisedFormFieldType(type: string): boolean {\n const t = (type ?? '').toLowerCase();\n return (\n t === 'checkbox' ||\n t.startsWith('calculation') ||\n Object.prototype.hasOwnProperty.call(FIELD_TYPE_CONTROLS, t) ||\n Object.prototype.hasOwnProperty.call(FORM_FIELD_TYPES, t)\n );\n}\n\n/** A declared question after resolution: `type` is the CONTROL, `hubspotType` what HubSpot sent. */\nexport type SupportedMeetingFormField = MeetingFormField & { type: SupportedFormFieldType; hubspotType?: string };\n\n/** One question → its drawable form, or null when the form does not draw it (display / unanswerable). */\nexport function normalizeFormField(\n field: MeetingFormField & { hubspotType?: string },\n): SupportedMeetingFormField | null {\n const control = resolveFormFieldControl(field);\n if (control === 'display' || control === 'unanswerable') return null;\n // A control that takes no options drops them (a `booleancheckbox` arrives\n // with HubSpot's `true`/`false` pair), which also keeps a re-resolution of\n // the result on the same control.\n const normalized: SupportedMeetingFormField = {\n ...field,\n type: control,\n hubspotType: field.hubspotType ?? field.type,\n };\n if (!FORM_FIELD_TYPES[control].hasOptions) {\n delete normalized.options;\n delete normalized.optionLabels;\n }\n return normalized;\n}\n\n/** The questions the native form draws and validates, in declared order. */\nexport function normalizeFormFields(fields: readonly MeetingFormField[]): SupportedMeetingFormField[] {\n return fields.flatMap(f => normalizeFormField(f) ?? []);\n}\n\n/** Whether the native form draws this question (false for display blocks and unanswerable questions). */\nexport function isSupportedFormField(field: MeetingFormField): boolean {\n return normalizeFormField(field) !== null;\n}\n\n/**\n * The ONLY reason left to hand a link to HubSpot's own page: a REQUIRED\n * question no control can answer. An optional one is simply left out — the\n * visitor can still book, and HubSpot does not require it.\n */\nexport function blocksNativeBooking(field: MeetingFormField): boolean {\n return field.required && resolveFormFieldControl(field) === 'unanswerable';\n}\n\n/**\n * The scheduler's fixed identity fields. HubSpot's book endpoint takes them\n * TOP-LEVEL (`firstName`, `lastName`, `email`), its own booking page hardcodes\n * them, and the link's `formFields` never lists them — so they are data HERE,\n * rendered by the widget through the SAME control path as every declared\n * question, rather than three hand-written blocks. `inputType`/`autoComplete`\n * are the browser hints a text control takes; the wire and the validator do\n * not see them.\n */\nexport interface BuiltInBookingField extends MeetingFormField {\n type: 'text';\n required: true;\n inputType?: 'email';\n autoComplete: string;\n /** Own placeholder. Omitted, the control derives one from the label (the registry's rule). */\n placeholder?: string;\n /** The wire's own required/format message — kept verbatim from the schema it replaced. */\n requiredMessage: string;\n}\n\nexport const BUILT_IN_BOOKING_FIELDS = [\n {\n name: 'firstName',\n label: 'First Name',\n type: 'text',\n required: true,\n autoComplete: 'given-name',\n requiredMessage: 'First name is required',\n },\n {\n name: 'lastName',\n label: 'Last Name',\n type: 'text',\n required: true,\n autoComplete: 'family-name',\n requiredMessage: 'Last name is required',\n },\n {\n name: 'email',\n label: 'Email',\n type: 'text',\n required: true,\n inputType: 'email',\n autoComplete: 'email',\n placeholder: 'username@mail.com',\n requiredMessage: 'Please enter a valid email address',\n },\n] as const satisfies readonly BuiltInBookingField[];\n\n/** `'firstName' | 'lastName' | 'email'` — derived from the array, never restated. */\nexport type BuiltInBookingFieldName = (typeof BUILT_IN_BOOKING_FIELDS)[number]['name'];\n\n/** The registry entry for a supported type, widened to the spec so optional\n * members (`placeholder`) are readable without narrowing on the union. */\nexport function fieldTypeSpec(type: SupportedFormFieldType): FormFieldTypeSpec {\n return FORM_FIELD_TYPES[type];\n}\n\n/** The wire validator for one identity field, from ITS declaration above. */\nfunction identityValidator(name: BuiltInBookingFieldName) {\n const field: BuiltInBookingField | undefined = BUILT_IN_BOOKING_FIELDS.find(f => f.name === name);\n if (!field) throw new Error(`Unknown built-in booking field: ${name}`);\n // The field's own message FIRST: an empty value must fail `min`/`email` with\n // the wire's copy, not a length cap it cannot have hit.\n const tooLong = { message: `${field.label} is too long` };\n return field.inputType === 'email'\n ? z.string().email({ message: field.requiredMessage }).max(255, tooLong)\n : z.string().min(1, { message: field.requiredMessage }).max(255, tooLong);\n}\n\n// ---------------------------------------------------------------------------\n// Validators (single home — client widget and server rebuild both use these)\n// ---------------------------------------------------------------------------\n\nconst IANA_TZ_RE = /^(?:UTC|[A-Za-z_]+(?:\\/[A-Za-z0-9_+-]+)+)$/;\n\n/**\n * IANA timezone check. Shape prefilter, then the authoritative resolution\n * test: `Intl.DateTimeFormat` throws on unknown zones. NOT\n * `Intl.supportedValuesOf('timeZone')` — that list excludes `'UTC'` itself\n * (verified in Node), which is a legitimate booking zone. Reject, never coerce.\n */\nexport function isValidIanaTimezone(tz: string): boolean {\n if (!IANA_TZ_RE.test(tz)) return false;\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: tz });\n return true;\n } catch {\n return false;\n }\n}\n\n/** BCP-47 locale shape check via `Intl.getCanonicalLocales`. Reject, never coerce. */\nexport function isValidBcp47Locale(locale: string): boolean {\n try {\n return Intl.getCanonicalLocales(locale).length > 0;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Schema factory\n// ---------------------------------------------------------------------------\n\n/**\n * Build the booking-form schema for ONE link's declared questions + consent.\n *\n * A factory (not a static schema) because per-link required questions cannot\n * be expressed statically. The widget builds it from the availability payload\n * it rendered; the server REBUILDS it from the link's own fetched metadata —\n * required-consent enforcement flows from this rebuild, not a parallel check.\n *\n * Deliberately NOT `.strict()`: the humanity-signal fields\n * (`HUMANITY_SIGNAL_KEYS` from `utils/humanity-signals`) ride alongside in\n * the same POST body (read raw by the host's bot gate BEFORE parsing) and are\n * stripped server-side before anything reaches HubSpot. zod's default\n * unknown-key stripping means the parsed output never contains them.\n *\n * `timezone`/`locale` are POST-only presentation fields (the invite renders\n * in the visitor's local time) — this schema is the ONLY place a\n * client-supplied zone is accepted; the availability path is UTC-pinned.\n */\nfunction buildBookingSchema<TStart extends z.ZodTypeAny, TDuration extends z.ZodTypeAny, TZone extends z.ZodTypeAny>(\n formFields: MeetingFormField[],\n legalConsent: MeetingLegalConsent | null,\n // Passed IN rather than switched on a boolean: a `deferSlot: boolean`\n // parameter cannot be narrowed at type level, so every ternary inside would\n // infer a union and widen `startTimeMs`/`durationMs` to `number | null |\n // undefined` for BOTH schemas — including the server's wire type.\n slot: { startTimeMs: TStart; durationMs: TDuration; timezone: TZone },\n) {\n const answers: Record<string, z.ZodTypeAny> = {};\n // Resolved HERE, not by the caller: the server rebuild passes the link's raw\n // questions, so client and server resolve every type through the same rule.\n // Display blocks and unanswerable optional questions carry no answer.\n const drawn = normalizeFormFields(formFields);\n for (const field of drawn) {\n const spec: FormFieldTypeSpec = FORM_FIELD_TYPES[field.type];\n // required/optional wrapping follows the answer's wire KIND, not its type:\n // a boolean is true-or-absent, a string is non-empty-or-empty. For strings\n // the required check is the FIRST rule on the chain, so an empty answer\n // reports \"is required\" rather than the type's own message.\n let validator: z.ZodTypeAny;\n if (spec.kind === 'boolean') {\n validator = field.required ? z.literal(true, { message: `${field.label} is required` }) : z.boolean().optional();\n } else {\n const base = field.required ? z.string().min(1, { message: `${field.label} is required` }) : z.string();\n validator = spec.validator(field, base);\n if (!field.required) validator = validator.optional().or(z.literal(''));\n }\n answers[field.name] = validator;\n }\n\n const requiredConsentIds = (legalConsent?.communicationConsentCheckboxes ?? [])\n .filter(c => c.required)\n .map(c => c.communicationTypeId);\n\n // A required answer cannot be enforced by an `.optional()` parent object —\n // omitting the `formFields` key entirely would skip every per-question\n // rule. When the link declares at least one required supported question,\n // the object itself is required.\n const hasRequiredAnswers = drawn.some(f => f.required);\n const answersObject = z.object(answers);\n\n return (\n z\n .object({\n meetingId: z.string().min(1),\n startTimeMs: slot.startTimeMs,\n durationMs: slot.durationMs,\n // The identity trio's rules come from BUILT_IN_BOOKING_FIELDS — the\n // keys stay literal so the payload type keeps its named properties.\n firstName: identityValidator('firstName'),\n lastName: identityValidator('lastName'),\n email: identityValidator('email'),\n timezone: slot.timezone,\n locale: z.string().refine(isValidBcp47Locale, { message: 'Invalid locale' }).optional(),\n // Plain `.optional()` (no `.default()`) so zod's input and output types\n // match — react-hook-form's zodResolver needs them identical, and the\n // server handles `undefined` explicitly anyway. REQUIRED when the link\n // declares required questions (see `hasRequiredAnswers` above).\n formFields: hasRequiredAnswers ? answersObject : answersObject.optional(),\n legalConsentResponses: z\n .array(z.object({ communicationTypeId: z.string(), consented: z.boolean() }))\n .optional(),\n })\n // Object-level rule: `.refine` on an `.optional()` field is skipped when\n // the field is absent — required consents must reject even on a payload\n // that omits the array entirely.\n .superRefine((data, ctx) => {\n const responses = data.legalConsentResponses ?? [];\n const ok = requiredConsentIds.every(id =>\n responses.some(r => r.communicationTypeId === id && r.consented === true),\n );\n if (!ok) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['legalConsentResponses'],\n message: 'Required consent checkboxes must be accepted',\n });\n }\n })\n );\n}\n\n/** The slot's wire validators — ONE declaration for the strict and the deferred schema. */\nconst SLOT_INSTANT = z.number().int().positive();\nconst TIMEZONE = z.string().refine(isValidIanaTimezone, { message: 'Invalid timezone' });\n\n/**\n * The STRICT schema — the wire contract. The server rebuilds it from the link's\n * own fetched metadata and validates every booking against it; a slot and a\n * duration are not optional on anything that reaches HubSpot.\n */\nexport function makeBookingSchema(formFields: MeetingFormField[], legalConsent: MeetingLegalConsent | null) {\n return buildBookingSchema(formFields, legalConsent, {\n startTimeMs: SLOT_INSTANT,\n durationMs: SLOT_INSTANT,\n timezone: TIMEZONE,\n });\n}\n\n/**\n * The DEFERRED schema — same fields, with the slot/duration/timezone triple\n * relaxed. `flow=\"details-first\"` collects answers BEFORE a slot exists, so the\n * form validates against this and the parent re-attaches the authoritative\n * values at POST time.\n *\n * Two named exports over one private builder rather than an overload or an\n * options flag: `MeetingBookingPayload` is `z.infer<ReturnType<...>>`, and\n * either of those alternatives would widen `startTimeMs`/`durationMs` to\n * `number | undefined` for EVERY consumer — including the server's\n * `durationsMs.includes(data.durationMs)`.\n */\nexport function makeDeferredBookingSchema(formFields: MeetingFormField[], legalConsent: MeetingLegalConsent | null) {\n // `.nullish()`, not `.optional()`: the widget passes these straight into\n // `defaultValues`, and details-first has `timezone === null` on the server\n // render and the first client render (zone resolution is post-hydration).\n // Accepting only `undefined` would reject a field with no rendered control\n // and no FieldWrapper — the submit button silently dead, type-check clean.\n return buildBookingSchema(formFields, legalConsent, {\n startTimeMs: SLOT_INSTANT.nullish(),\n durationMs: SLOT_INSTANT.nullish(),\n timezone: TIMEZONE.nullish(),\n });\n}\n\n/** The wire payload. Pinned to the STRICT builder — see above. */\nexport type MeetingBookingPayload = z.infer<ReturnType<typeof makeBookingSchema>>;\n\n/**\n * The form's value type, used as the `useForm` generic in BOTH flows: a relaxed\n * resolver is not assignable to `Resolver<MeetingBookingPayload>`, and mixing\n * the two survives only on TS's bivariant method-parameter check.\n */\nexport type BookingFormValues = z.infer<ReturnType<typeof makeDeferredBookingSchema>>;\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/schemas/meeting-booking-schema.ts"],"names":["z"],"mappings":";;;;;AAqJO,IAAM,2BAAA,GAA8B;AAAA,EACzC,YAAA;AAAA,EACA,YAAA;AAAA,EACA,eAAA;AAAA,EACA,WAAA;AAAA,EACA,yBAAA;AAAA,EACA;AACF;AAqDA,IAAM,eAAA,GAAkD,CAAC,KAAA,EAAO,IAAA,KAC9D,KAAK,MAAA,CAAO,CAAA,CAAA,KAAK,CAAC,CAAA,IAAA,CAAM,MAAM,OAAA,IAAW,EAAC,EAAG,QAAA,CAAS,CAAC,CAAA,EAAG;AAAA,EACxD,OAAA,EAAS,CAAA,iCAAA,EAAoC,KAAA,CAAM,KAAK,CAAA;AAC1D,CAAC,CAAA;AAGI,IAAM,kBAAA,GAAqB;AAG3B,IAAM,qBAAA,GAAwB;AAG9B,IAAM,eAAA,GAAkB,CAAC,KAAA,KAC9B,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,qBAAqB,CAAA,GAAI;AAQrE,IAAM,QAAA,GAAW;AACxB,IAAM,gBAAA,GAAmB,CAAA;AACzB,IAAM,gBAAA,GAAmB,EAAA;AACzB,IAAM,cAAc,CAAC,CAAA,KAAc,EAAE,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA,CAAE,MAAA;AAGjD,IAAM,WAAA,GAAc;AAC3B,IAAM,aAAA,GAAgB,CAAC,CAAA,KAAc;AACnC,EAAA,IAAI,CAAC,WAAA,CAAY,IAAA,CAAK,CAAC,GAAG,OAAO,KAAA;AACjC,EAAA,MAAM,CAAA,mBAAI,IAAI,IAAA,CAAK,CAAA,EAAG,CAAC,CAAA,UAAA,CAAY,CAAA;AACnC,EAAA,OAAO,CAAC,MAAA,CAAO,KAAA,CAAM,CAAA,CAAE,OAAA,EAAS,CAAA,IAAK,CAAA,CAAE,WAAA,EAAY,CAAE,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,KAAM,CAAA;AACxE,CAAA;AAEO,IAAM,gBAAA,GAAmB;AAAA,EAC9B,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY,KAAA;AAAA,IACZ,SAAA,EAAW,CAAC,KAAA,EAAO,IAAA,KAAS,IAAA,CAAK,GAAA,CAAI,GAAA,EAAM,EAAE,OAAA,EAAS,CAAA,EAAG,KAAA,CAAM,KAAK,gBAAgB,CAAA;AAAA,IACpF,WAAA,EAAa,CAAA,KAAA,KAAS,CAAA,MAAA,EAAS,KAAA,CAAM,KAAK,CAAA;AAAA,GAC5C;AAAA,EACA,QAAA,EAAU;AAAA,IACR,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY,KAAA;AAAA,IACZ,SAAA,EAAW,CAAC,KAAA,EAAO,IAAA,KAAS,IAAA,CAAK,GAAA,CAAI,GAAA,EAAM,EAAE,OAAA,EAAS,CAAA,EAAG,KAAA,CAAM,KAAK,gBAAgB,CAAA;AAAA,IACpF,aAAa,CAAA,KAAA,KAAS,CAAA,UAAA,EAAa,KAAA,CAAM,QAAA,GAAW,KAAK,aAAa,CAAA;AAAA,GACxE;AAAA,EACA,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY,KAAA;AAAA;AAAA;AAAA,IAGZ,SAAA,EAAW,CAAC,KAAA,EAAO,IAAA,KACjB,KACG,GAAA,CAAI,EAAA,EAAI,EAAE,OAAA,EAAS,CAAA,EAAG,KAAA,CAAM,KAAK,CAAA,YAAA,CAAA,EAAgB,CAAA,CACjD,KAAA,CAAM,kBAAA,EAAoB,EAAE,SAAS,CAAA,EAAG,KAAA,CAAM,KAAK,CAAA,iBAAA,CAAA,EAAqB;AAAA,GAC/E;AAAA,EACA,KAAA,EAAO;AAAA,IACL,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY,KAAA;AAAA,IACZ,SAAA,EAAW,CAAC,KAAA,EAAO,IAAA,KACjB,KACG,GAAA,CAAI,EAAA,EAAI,EAAE,OAAA,EAAS,CAAA,EAAG,KAAA,CAAM,KAAK,CAAA,YAAA,CAAA,EAAgB,CAAA,CACjD,MAAA;AAAA,MACC,CAAA,CAAA,KAAK,CAAC,CAAA,IAAM,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,IAAK,WAAA,CAAY,CAAC,CAAA,IAAK,gBAAA,IAAoB,WAAA,CAAY,CAAC,CAAA,IAAK,gBAAA;AAAA,MACxF,EAAE,OAAA,EAAS,CAAA,sCAAA,EAAyC,KAAA,CAAM,KAAK,CAAA,CAAA;AAAG,KACpE;AAAA,IACJ,WAAA,EAAa,CAAA,KAAA,KAAS,CAAA,MAAA,EAAS,KAAA,CAAM,KAAK,CAAA;AAAA,GAC5C;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY,KAAA;AAAA,IACZ,WAAW,CAAC,KAAA,EAAO,SAAS,IAAA,CAAK,MAAA,CAAO,OAAK,CAAC,CAAA,IAAK,aAAA,CAAc,CAAC,GAAG,EAAE,OAAA,EAAS,GAAG,KAAA,CAAM,KAAK,mBAAmB;AAAA,GACnH;AAAA,EACA,QAAQ,EAAE,IAAA,EAAM,UAAU,UAAA,EAAY,IAAA,EAAM,WAAW,eAAA,EAAgB;AAAA,EACvE,OAAO,EAAE,IAAA,EAAM,UAAU,UAAA,EAAY,IAAA,EAAM,WAAW,eAAA,EAAgB;AAAA,EACtE,WAAA,EAAa;AAAA,IACX,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY,IAAA;AAAA,IACZ,WAAW,CAAC,KAAA,EAAO,SACjB,IAAA,CAAK,MAAA,CAAO,OAAK,eAAA,CAAgB,CAAC,EAAE,KAAA,CAAM,CAAA,IAAA,KAAA,CAAS,MAAM,OAAA,IAAW,IAAI,QAAA,CAAS,IAAI,CAAC,CAAA,EAAG;AAAA,MACvF,OAAA,EAAS,CAAA,gCAAA,EAAmC,KAAA,CAAM,KAAK,CAAA;AAAA,KACxD;AAAA,GACL;AAAA,EACA,QAAA,EAAU,EAAE,IAAA,EAAM,SAAA,EAAW,UAAA,EAAY,OAAO,SAAA,EAAW,MAAMA,KAAA,CAAE,OAAA,EAAQ;AAC7E;AAIO,IAAM,0BAAA,GAA6B,MAAA,CAAO,IAAA,CAAK,gBAAgB;AAwBtE,IAAM,mBAAA,GAAqE;AAAA,EACzE,IAAA,EAAM,MAAA;AAAA,EACN,QAAA,EAAU,UAAA;AAAA,EACV,MAAA,EAAQ,QAAA;AAAA,EACR,MAAA,EAAQ,QAAA;AAAA,EACR,KAAA,EAAO,OAAA;AAAA,EACP,eAAA,EAAiB,UAAA;AAAA,EACjB,WAAA,EAAa,OAAA;AAAA,EACb,IAAA,EAAM,MAAA;AAAA,EACN,IAAA,EAAM,SAAA;AAAA,EACN,IAAA,EAAM;AACR,CAAA;AAGA,IAAM,kBAAA,GAAoE;AAAA,EACxE,MAAA,EAAQ,MAAA;AAAA,EACR,MAAA,EAAQ,QAAA;AAAA,EACR,WAAA,EAAa,QAAA;AAAA,EACb,IAAA,EAAM,UAAA;AAAA,EACN,IAAA,EAAM,MAAA;AAAA,EACN,QAAA,EAAU,MAAA;AAAA,EACV,YAAA,EAAc;AAChB,CAAA;AAEA,IAAM,qBAAqB,CAAC,KAAA,KAAA,CAA6B,KAAA,CAAM,OAAA,EAAS,UAAU,CAAA,IAAK,CAAA;AAQhF,SAAS,wBAAwB,KAAA,EAA8C;AACpF,EAAA,MAAM,SAAA,GAAA,CAAa,KAAA,CAAM,IAAA,IAAQ,EAAA,EAAI,WAAA,EAAY;AACjD,EAAA,MAAM,WAAA,GAAc,mBAAmB,KAAK,CAAA;AAC5C,EAAA,MAAM,MAAA,GAAS,CAAC,CAAA,KAAwE;AACtF,IAAA,IAAI,CAAC,CAAA,IAAK,CAAA,KAAM,SAAA,IAAa,CAAA,KAAM,gBAAgB,OAAO,CAAA;AAC1D,IAAA,OAAO,iBAAiB,CAAC,CAAA,CAAE,UAAA,IAAc,CAAC,cAAc,MAAA,GAAS,CAAA;AAAA,EACnE,CAAA;AAEA,EAAA,IAAI,SAAA,KAAc,UAAA,EAAY,OAAO,WAAA,GAAc,aAAA,GAAgB,UAAA;AACnE,EAAA,IAAI,SAAA,CAAU,UAAA,CAAW,aAAa,CAAA,EAAG,OAAO,SAAA;AAIhD,EAAA,MAAM,SAAA,GAAY,OAAO,SAAA,CAAU,cAAA,CAAe,KAAK,gBAAA,EAAkB,SAAS,IAC7E,SAAA,GACD,MAAA;AACJ,EAAA,OACE,OAAO,mBAAA,CAAoB,SAAS,CAAA,IAAK,SAAS,KAClD,MAAA,CAAO,kBAAA,CAAA,CAAoB,KAAA,CAAM,QAAA,IAAY,IAAI,WAAA,EAAa,CAAC,CAAA,KAC9D,cAAc,QAAA,GAAW,MAAA,CAAA;AAE9B;AASO,SAAS,wBAAwB,IAAA,EAAuB;AAC7D,EAAA,MAAM,OAAA,GAAU,uBAAA,CAAwB,EAAuB,IAAA,EAAuB,OAAA,EAAS,CAAC,GAAG,GAAG,CAAA;AACtG,EAAA,OAAO,YAAY,SAAA,IAAa,OAAA,KAAY,cAAA,IAAkB,gBAAA,CAAiB,OAAO,CAAA,CAAE,UAAA;AAC1F;AAGO,IAAM,gCAAmE,0BAAA,CAA2B,MAAA;AAAA,EACzG,CAAA,IAAA,KAAQ,gBAAA,CAAiB,IAAI,CAAA,CAAE;AACjC;AAQO,SAAS,0BAA0B,IAAA,EAAuB;AAC/D,EAAA,MAAM,CAAA,GAAA,CAAK,IAAA,IAAQ,EAAA,EAAI,WAAA,EAAY;AACnC,EAAA,OACE,MAAM,UAAA,IACN,CAAA,CAAE,WAAW,aAAa,CAAA,IAC1B,OAAO,SAAA,CAAU,cAAA,CAAe,IAAA,CAAK,mBAAA,EAAqB,CAAC,CAAA,IAC3D,MAAA,CAAO,UAAU,cAAA,CAAe,IAAA,CAAK,kBAAkB,CAAC,CAAA;AAE5D;AAMO,SAAS,mBACd,KAAA,EACkC;AAClC,EAAA,MAAM,OAAA,GAAU,wBAAwB,KAAK,CAAA;AAC7C,EAAA,IAAI,OAAA,KAAY,SAAA,IAAa,OAAA,KAAY,cAAA,EAAgB,OAAO,IAAA;AAIhE,EAAA,MAAM,UAAA,GAAwC;AAAA,IAC5C,GAAG,KAAA;AAAA,IACH,IAAA,EAAM,OAAA;AAAA,IACN,WAAA,EAAa,KAAA,CAAM,WAAA,IAAe,KAAA,CAAM;AAAA,GAC1C;AACA,EAAA,IAAI,CAAC,gBAAA,CAAiB,OAAO,CAAA,CAAE,UAAA,EAAY;AACzC,IAAA,OAAO,UAAA,CAAW,OAAA;AAClB,IAAA,OAAO,UAAA,CAAW,YAAA;AAAA,EACpB;AACA,EAAA,OAAO,UAAA;AACT;AAGO,SAAS,oBAAoB,MAAA,EAAkE;AACpG,EAAA,OAAO,OAAO,OAAA,CAAQ,CAAA,CAAA,KAAK,mBAAmB,CAAC,CAAA,IAAK,EAAE,CAAA;AACxD;AAGO,SAAS,qBAAqB,KAAA,EAAkC;AACrE,EAAA,OAAO,kBAAA,CAAmB,KAAK,CAAA,KAAM,IAAA;AACvC;AAOO,SAAS,oBAAoB,KAAA,EAAkC;AACpE,EAAA,OAAO,KAAA,CAAM,QAAA,IAAY,uBAAA,CAAwB,KAAK,CAAA,KAAM,cAAA;AAC9D;AAsBO,IAAM,uBAAA,GAA0B;AAAA,EACrC;AAAA,IACE,IAAA,EAAM,WAAA;AAAA,IACN,KAAA,EAAO,YAAA;AAAA,IACP,IAAA,EAAM,MAAA;AAAA,IACN,QAAA,EAAU,IAAA;AAAA,IACV,YAAA,EAAc,YAAA;AAAA,IACd,eAAA,EAAiB;AAAA,GACnB;AAAA,EACA;AAAA,IACE,IAAA,EAAM,UAAA;AAAA,IACN,KAAA,EAAO,WAAA;AAAA,IACP,IAAA,EAAM,MAAA;AAAA,IACN,QAAA,EAAU,IAAA;AAAA,IACV,YAAA,EAAc,aAAA;AAAA,IACd,eAAA,EAAiB;AAAA,GACnB;AAAA,EACA;AAAA,IACE,IAAA,EAAM,OAAA;AAAA,IACN,KAAA,EAAO,OAAA;AAAA,IACP,IAAA,EAAM,MAAA;AAAA,IACN,QAAA,EAAU,IAAA;AAAA,IACV,SAAA,EAAW,OAAA;AAAA,IACX,YAAA,EAAc,OAAA;AAAA,IACd,WAAA,EAAa,mBAAA;AAAA,IACb,eAAA,EAAiB;AAAA;AAErB;AAOO,SAAS,cAAc,IAAA,EAAiD;AAC7E,EAAA,OAAO,iBAAiB,IAAI,CAAA;AAC9B;AAGA,SAAS,kBAAkB,IAAA,EAA+B;AACxD,EAAA,MAAM,QAAyC,uBAAA,CAAwB,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,SAAS,IAAI,CAAA;AAChG,EAAA,IAAI,CAAC,KAAA,EAAO,MAAM,IAAI,KAAA,CAAM,CAAA,gCAAA,EAAmC,IAAI,CAAA,CAAE,CAAA;AAGrE,EAAA,MAAM,UAAU,EAAE,OAAA,EAAS,CAAA,EAAG,KAAA,CAAM,KAAK,CAAA,YAAA,CAAA,EAAe;AACxD,EAAA,OAAO,KAAA,CAAM,SAAA,KAAc,OAAA,GACvBA,KAAA,CAAE,MAAA,EAAO,CAAE,KAAA,CAAM,EAAE,OAAA,EAAS,KAAA,CAAM,eAAA,EAAiB,EAAE,GAAA,CAAI,GAAA,EAAK,OAAO,CAAA,GACrEA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,CAAI,CAAA,EAAG,EAAE,OAAA,EAAS,KAAA,CAAM,eAAA,EAAiB,CAAA,CAAE,GAAA,CAAI,KAAK,OAAO,CAAA;AAC5E;AAMA,IAAM,UAAA,GAAa,4CAAA;AAQZ,SAAS,oBAAoB,EAAA,EAAqB;AACvD,EAAA,IAAI,CAAC,UAAA,CAAW,IAAA,CAAK,EAAE,GAAG,OAAO,KAAA;AACjC,EAAA,IAAI;AACF,IAAA,IAAI,KAAK,cAAA,CAAe,OAAA,EAAS,EAAE,QAAA,EAAU,IAAI,CAAA;AACjD,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAGO,SAAS,mBAAmB,MAAA,EAAyB;AAC1D,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,mBAAA,CAAoB,MAAM,CAAA,CAAE,MAAA,GAAS,CAAA;AAAA,EACnD,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAwBA,SAAS,kBAAA,CACP,UAAA,EACA,YAAA,EAKA,IAAA,EACA;AACA,EAAA,MAAM,UAAwC,EAAC;AAI/C,EAAA,MAAM,KAAA,GAAQ,oBAAoB,UAAU,CAAA;AAC5C,EAAA,KAAA,MAAW,SAAS,KAAA,EAAO;AACzB,IAAA,MAAM,IAAA,GAA0B,gBAAA,CAAiB,KAAA,CAAM,IAAI,CAAA;AAK3D,IAAA,IAAI,SAAA;AACJ,IAAA,IAAI,IAAA,CAAK,SAAS,SAAA,EAAW;AAC3B,MAAA,SAAA,GAAY,MAAM,QAAA,GAAWA,KAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,EAAE,OAAA,EAAS,CAAA,EAAG,KAAA,CAAM,KAAK,gBAAgB,CAAA,GAAIA,KAAA,CAAE,OAAA,GAAU,QAAA,EAAS;AAAA,IACjH,CAAA,MAAO;AACL,MAAA,MAAM,OAAO,KAAA,CAAM,QAAA,GAAWA,KAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAA,EAAG,EAAE,OAAA,EAAS,GAAG,KAAA,CAAM,KAAK,gBAAgB,CAAA,GAAIA,MAAE,MAAA,EAAO;AACtG,MAAA,SAAA,GAAY,IAAA,CAAK,SAAA,CAAU,KAAA,EAAO,IAAI,CAAA;AACtC,MAAA,IAAI,CAAC,KAAA,CAAM,QAAA,EAAU,SAAA,GAAY,SAAA,CAAU,QAAA,EAAS,CAAE,EAAA,CAAGA,KAAA,CAAE,OAAA,CAAQ,EAAE,CAAC,CAAA;AAAA,IACxE;AACA,IAAA,OAAA,CAAQ,KAAA,CAAM,IAAI,CAAA,GAAI,SAAA;AAAA,EACxB;AAEA,EAAA,MAAM,kBAAA,GAAA,CAAsB,YAAA,EAAc,8BAAA,IAAkC,EAAC,EAC1E,MAAA,CAAO,CAAA,CAAA,KAAK,CAAA,CAAE,QAAQ,CAAA,CACtB,GAAA,CAAI,CAAA,CAAA,KAAK,EAAE,mBAAmB,CAAA;AAMjC,EAAA,MAAM,kBAAA,GAAqB,KAAA,CAAM,IAAA,CAAK,CAAA,CAAA,KAAK,EAAE,QAAQ,CAAA;AACrD,EAAA,MAAM,aAAA,GAAgBA,KAAA,CAAE,MAAA,CAAO,OAAO,CAAA;AAEtC,EAAA,OACEA,MACG,MAAA,CAAO;AAAA,IACN,SAAA,EAAWA,KAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,IAC3B,aAAa,IAAA,CAAK,WAAA;AAAA,IAClB,YAAY,IAAA,CAAK,UAAA;AAAA;AAAA;AAAA,IAGjB,SAAA,EAAW,kBAAkB,WAAW,CAAA;AAAA,IACxC,QAAA,EAAU,kBAAkB,UAAU,CAAA;AAAA,IACtC,KAAA,EAAO,kBAAkB,OAAO,CAAA;AAAA,IAChC,UAAU,IAAA,CAAK,QAAA;AAAA,IACf,MAAA,EAAQA,KAAA,CAAE,MAAA,EAAO,CAAE,MAAA,CAAO,kBAAA,EAAoB,EAAE,OAAA,EAAS,gBAAA,EAAkB,CAAA,CAAE,QAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA,IAKtF,UAAA,EAAY,kBAAA,GAAqB,aAAA,GAAgB,aAAA,CAAc,QAAA,EAAS;AAAA,IACxE,uBAAuBA,KAAA,CACpB,KAAA,CAAMA,KAAA,CAAE,MAAA,CAAO,EAAE,mBAAA,EAAqBA,KAAA,CAAE,MAAA,EAAO,EAAG,WAAWA,KAAA,CAAE,OAAA,IAAW,CAAC,EAC3E,QAAA;AAAS,GACb,CAAA,CAIA,WAAA,CAAY,CAAC,MAAM,GAAA,KAAQ;AAC1B,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,qBAAA,IAAyB,EAAC;AACjD,IAAA,MAAM,KAAK,kBAAA,CAAmB,KAAA;AAAA,MAAM,CAAA,EAAA,KAClC,UAAU,IAAA,CAAK,CAAA,CAAA,KAAK,EAAE,mBAAA,KAAwB,EAAA,IAAM,CAAA,CAAE,SAAA,KAAc,IAAI;AAAA,KAC1E;AACA,IAAA,IAAI,CAAC,EAAA,EAAI;AACP,MAAA,GAAA,CAAI,QAAA,CAAS;AAAA,QACX,IAAA,EAAMA,MAAE,YAAA,CAAa,MAAA;AAAA,QACrB,IAAA,EAAM,CAAC,uBAAuB,CAAA;AAAA,QAC9B,OAAA,EAAS;AAAA,OACV,CAAA;AAAA,IACH;AAAA,EACF,CAAC,CAAA;AAEP;AAGA,IAAM,eAAeA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,GAAM,QAAA,EAAS;AAC/C,IAAM,QAAA,GAAWA,MAAE,MAAA,EAAO,CAAE,OAAO,mBAAA,EAAqB,EAAE,OAAA,EAAS,kBAAA,EAAoB,CAAA;AAOhF,SAAS,iBAAA,CAAkB,YAAgC,YAAA,EAA0C;AAC1G,EAAA,OAAO,kBAAA,CAAmB,YAAY,YAAA,EAAc;AAAA,IAClD,WAAA,EAAa,YAAA;AAAA,IACb,UAAA,EAAY,YAAA;AAAA,IACZ,QAAA,EAAU;AAAA,GACX,CAAA;AACH;AAcO,SAAS,yBAAA,CAA0B,YAAgC,YAAA,EAA0C;AAMlH,EAAA,OAAO,kBAAA,CAAmB,YAAY,YAAA,EAAc;AAAA,IAClD,WAAA,EAAa,aAAa,OAAA,EAAQ;AAAA,IAClC,UAAA,EAAY,aAAa,OAAA,EAAQ;AAAA,IACjC,QAAA,EAAU,SAAS,OAAA;AAAQ,GAC5B,CAAA;AACH;AAeO,SAAS,iBAAA,CAAkB,OAAgB,IAAA,EAAmC;AACnF,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,EAAU,OAAO,KAAA;AACtC,EAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,GAAG,EAAE,CAAC,CAAA,EAAG,IAAA,EAAK,CAAE,WAAA,EAAY;AACvD,EAAA,IAAI,MAAA,KAAW,MAAA,IAAa,MAAA,KAAW,EAAA,EAAI,OAAO,KAAA;AAClD,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,QAAA,CAAS,MAAM,CAAA;AACrC;AAIO,SAAS,sBAAA,CACd,QACA,IAAA,EACA;AACA,EAAA,IAAI,CAAC,MAAM,OAAO,MAAA;AAClB,EAAA,OAAO,MAAA,CAAO,WAAA,CAAY,CAAC,MAAA,EAAQ,GAAA,KAAQ;AACzC,IAAA,IAAI,iBAAA,CAAkB,MAAA,CAAO,KAAA,EAAO,IAAI,CAAA,EAAG;AACzC,MAAA,GAAA,CAAI,QAAA,CAAS,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,CAAC,OAAO,CAAA,EAAG,OAAA,EAAS,IAAA,CAAK,OAAA,EAAS,CAAA;AAAA,IACzE;AAAA,EACF,CAAC,CAAA;AACH","file":"meeting-booking-schema.cjs","sourcesContent":["import { z } from 'zod';\n\n/**\n * Meeting-booking wire contracts + validation factory.\n *\n * SERVER-SAFE tsup entry (no \"use client\" banner) with its OWN per-file\n * subpath (`./schemas/meeting-booking-schema`) — same pattern and reasons as\n * `schemas/contact-schema`: used by BOTH the lib's `<HubSpotMeetingScheduler>`\n * (client-side validation) AND the host's server-side booking route, which\n * REBUILDS the schema from the link's own fetched metadata and never trusts a\n * client-shaped instance. zod is an optional peer quarantined to per-subpath\n * verticals — do NOT re-export this module through any broad barrel.\n *\n * Every cross-boundary type for the scheduling feature lives HERE (the lib\n * owns the contract; hosts import this subpath directly, type-only where\n * possible).\n */\n\n// ---------------------------------------------------------------------------\n// Wire types\n// ---------------------------------------------------------------------------\n\n/**\n * Sanitized availability payload served by the host proxy\n * (`GET {apiBaseUrl}/api/meetings/availability?meeting=<id>&monthOffset=<n>`).\n *\n * NO timezone field on purpose: slot starts are absolute epoch-ms instants\n * (verified timezone-independent against the live API), upstream fetches are\n * UTC-pinned, and ALL zone rendering happens client-side. Slots must be\n * whitelist-copied from HubSpot's `linkAvailability` ONLY — never derived\n * from busy-time data.\n */\nexport interface MeetingAvailability {\n meetingId: string;\n monthOffset: number;\n hasMore: boolean;\n /** Offered durations in ms — HubSpot's native unit for the booking POST. */\n durationsMs: number[];\n /** Bookable slot start times (epoch ms), keyed by duration in ms. */\n slotsByDurationMs: Record<string, number[]>;\n formFields: MeetingFormField[];\n /** Verbatim whitelist-copy of HubSpot's `legalConsentOptions` when consent is enabled; null when disabled. */\n legalConsent: MeetingLegalConsent | null;\n /**\n * Who the visitor is meeting — whitelisted DISPLAY projection the host DAL\n * builds from its own people data (e.g. a profiles table matched\n * server-side). NEVER carries emails or busy-time data; optional so\n * existing hosts stay wire-compatible.\n */\n hosts?: MeetingHost[];\n}\n\n/** Display-only host identity for the scheduler's context panel. */\nexport interface MeetingHost {\n name: string;\n avatarUrl: string | null;\n /** Job title / role line under the name (null → omitted). */\n title: string | null;\n}\n\n/**\n * One scheduling link on the DIRECTORY wire (`GET /api/meetings`) — the\n * host-DAL whitelist projection consumed by `MeetingSchedulerDirectory` and\n * host pages. Never carries organizer emails/busy-time data.\n */\nexport interface SchedulingLink {\n id: string;\n /** The link's public HubSpot booking URL — escape-hatch target only. */\n link: string;\n /** HubSpot slug path — the row's in-app destination is `<basePath>/<slug>`. */\n slug: string;\n /** Audience group key (slugified audience label; `\"other\"` in scope=all). */\n purpose: string;\n title: string;\n description: string | null;\n kind: 'personal' | 'team';\n /** Display-only minutes projection (booking stays ms end-to-end). */\n durationsMinutes: number[];\n hosts: MeetingHost[];\n /** Earliest bookable slot (epoch ms) from the current-month payload. */\n nextAvailableMs: number | null;\n}\n\nexport interface SchedulingLinksPayload {\n purposes: Array<{ purpose: string; label: string; links: SchedulingLink[] }>;\n fetchedAt: string;\n}\n\n/**\n * One declared question as the host forwards it. `type` is HubSpot's\n * `fieldType` (the control HubSpot draws: `phonenumber`, `booleancheckbox`…)\n * VERBATIM, and `dataType` its `type` (the stored value's kind: `string`,\n * `enumeration`, `phone_number`…). Neither is a closed set: the widget resolves\n * ANY pair to a control through `resolveFormFieldControl`, so a type HubSpot\n * adds tomorrow renders as the nearest control instead of taking the form down.\n */\nexport interface MeetingFormField {\n name: string;\n label: string;\n type: string;\n /** HubSpot's property data type — the fallback when `type` is unknown. Optional: older hosts omit it. */\n dataType?: string;\n required: boolean;\n /** Option VALUES (what is submitted). */\n options?: string[];\n /** Option value → display label, when HubSpot's label differs from the value. */\n optionLabels?: Record<string, string>;\n}\n\n/**\n * HubSpot's consent copy, rendered VERBATIM by the widget (GDPR surface —\n * never edited, never summarized). Responses are keyed by\n * `communicationTypeId`.\n */\nexport interface MeetingLegalConsent {\n processingConsentText: string;\n processingConsentCheckboxLabel: string | null;\n communicationConsentText: string | null;\n communicationConsentCheckboxes: Array<{\n communicationTypeId: string;\n label: string;\n required: boolean;\n }>;\n privacyPolicyText: string | null;\n isLegitimateInterest: boolean;\n}\n\n/**\n * Whitelisted booking result returned by the host proxy — the THIRD HubSpot\n * payload that reaches a browser, so it gets the same whitelist-copy\n * treatment as the two GETs. Nothing organizer-derived.\n */\nexport interface BookingConfirmation {\n meetingId: string;\n title: string;\n startTimeMs: number;\n durationMs: number;\n}\n\n/**\n * Typed domain errors the booking route emits; the widget keys its recovery\n * UI off these. `SLOT_TAKEN` → refetch-and-recover; `TEMPORARILY_UNAVAILABLE`\n * → retry affordance; `MEETING_UNAVAILABLE` → daily ceiling exhausted\n * (escape hatch, not a retry timer); `LINK_GONE` → link deleted upstream;\n * `INVALID_EMAIL` → HubSpot rejected the attendee address (its\n * MeetingsBookingCreatedError.INVALID_EMAIL class — fake/unreachable\n * mailboxes), so the widget tells the visitor to fix the email instead of\n * blaming the slot or their other details.\n */\nexport const MEETING_BOOKING_ERROR_CODES = [\n 'SLOT_TAKEN',\n 'VALIDATION',\n 'INVALID_EMAIL',\n 'LINK_GONE',\n 'TEMPORARILY_UNAVAILABLE',\n 'MEETING_UNAVAILABLE',\n] as const;\n\n/**\n * Derived from the runtime array ABOVE — the array is the single source of\n * truth. The booking hook validates server codes against it at runtime, so a\n * code added only to a hand-written type would be silently coerced to\n * TEMPORARILY_UNAVAILABLE by every deployed widget (exactly how the\n * INVALID_EMAIL rollout mis-rendered on 2026-08-27: type extended, runtime\n * allowlist stale — an array annotation is not exhaustiveness-checked).\n */\nexport type MeetingBookingErrorCode = (typeof MEETING_BOOKING_ERROR_CODES)[number];\n\n// ---------------------------------------------------------------------------\n// Field-type vocabulary — ONE set drives the renderer AND the validator\n// ---------------------------------------------------------------------------\n\n/**\n * THE registry of CONTROLS the native form can draw — one entry per control,\n * and the ONLY place a control is declared. Everything else derives from it:\n * `SupportedFormFieldType` is its key union, `SUPPORTED_FORM_FIELD_TYPES` its\n * keys, and `makeBookingSchema` maps each answer through the entry's\n * validator. The widget's control table (`booking-form.tsx`) is a `Record`\n * over the same key union, so a control added here without a renderer is a\n * COMPILE error, not a silent gap.\n *\n * These are controls, NOT HubSpot's type vocabulary. A HubSpot question\n * reaches a control through `resolveFormFieldControl` (exact `fieldType`, then\n * its data type, then a generic fallback), so the open set of HubSpot types\n * maps onto this closed set of controls and no type is ever \"unsupported\".\n * The one thing that still falls back to HubSpot's own page is a REQUIRED\n * question no control can answer (a file upload) — see `blocksNativeBooking`.\n *\n * Every string control rides the wire as a STRING (HubSpot's book endpoint\n * takes `{ name, value: string }`); `checkbox` is the one boolean. `number` is\n * a decimal literal, `multiselect` HubSpot's `;`-joined enumeration value,\n * `date` an ISO `YYYY-MM-DD`.\n */\nexport interface FormFieldTypeSpec {\n /** The answer's wire shape, which also decides how required/optional wraps it. */\n kind: 'string' | 'boolean';\n /** Whether HubSpot publishes `options` for the type (the pickers). */\n hasOptions: boolean;\n /**\n * The validator for ONE answer. String types EXTEND `base`, which already\n * carries the required-ness (`min(1)` when required) — so \"X is required\" is\n * the first issue reported for an empty answer, ahead of the type's own rule.\n * Boolean types ignore it.\n */\n validator: (field: MeetingFormField, base: z.ZodString) => z.ZodTypeAny;\n /** The control's placeholder, derived from the field (the mock's \"Enter Company Name\"). */\n placeholder?: (field: MeetingFormField) => string;\n}\n\nconst optionValidator: FormFieldTypeSpec['validator'] = (field, base) =>\n base.refine(v => !v || (field.options ?? []).includes(v), {\n message: `Please choose a valid option for ${field.label}`,\n });\n\n/** The wire shape of a number answer — what the form canonicalises TO and the validator checks. */\nexport const DECIMAL_LITERAL_RE = /^-?\\d+(\\.\\d+)?$/;\n\n/** HubSpot's separator for a multi-value enumeration (`a;b;c`). */\nexport const MULTI_VALUE_SEPARATOR = ';';\n\n/** Split a multiselect answer into its values (empty string → none). */\nexport const splitMultiValue = (value: unknown): string[] =>\n typeof value === 'string' && value ? value.split(MULTI_VALUE_SEPARATOR) : [];\n\n/**\n * A phone answer: digits with the punctuation people actually type (`+1 (415)\n * 555-2671`, `020 7946 0958 ext. 12`). Deliberately a SHAPE check, not a\n * numbering-plan check — HubSpot stores the string as typed, and rejecting a\n * valid number we failed to parse is worse than accepting an odd one.\n */\nexport const PHONE_RE = /^\\+?[\\d\\s().\\-/]+(?:\\s*(?:x|ext\\.?)\\s*\\d+)?$/i;\nconst PHONE_MIN_DIGITS = 5;\nconst PHONE_MAX_DIGITS = 20;\nconst phoneDigits = (v: string) => v.replace(/\\D/g, '').length;\n\n/** A date answer as `<input type=\"date\">` emits it. */\nexport const ISO_DATE_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst isRealIsoDate = (v: string) => {\n if (!ISO_DATE_RE.test(v)) return false;\n const d = new Date(`${v}T00:00:00Z`);\n return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === v;\n};\n\nexport const FORM_FIELD_TYPES = {\n text: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) => base.max(1000, { message: `${field.label} is too long` }),\n placeholder: field => `Enter ${field.label}`,\n },\n textarea: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) => base.max(5000, { message: `${field.label} is too long` }),\n placeholder: field => `Enter text${field.required ? '' : ' (optional)'}`,\n },\n number: {\n kind: 'string',\n hasOptions: false,\n // Canonical decimal literal — the control normalises what the browser\n // accepts (`1e3`, `007`) to this before it is validated or sent.\n validator: (field, base) =>\n base\n .max(32, { message: `${field.label} is too long` })\n .regex(DECIMAL_LITERAL_RE, { message: `${field.label} must be a number` }),\n },\n phone: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) =>\n base\n .max(40, { message: `${field.label} is too long` })\n .refine(\n v => !v || (PHONE_RE.test(v) && phoneDigits(v) >= PHONE_MIN_DIGITS && phoneDigits(v) <= PHONE_MAX_DIGITS),\n { message: `Please enter a valid phone number for ${field.label}` },\n ),\n placeholder: field => `Enter ${field.label}`,\n },\n date: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) => base.refine(v => !v || isRealIsoDate(v), { message: `${field.label} must be a date` }),\n },\n select: { kind: 'string', hasOptions: true, validator: optionValidator },\n radio: { kind: 'string', hasOptions: true, validator: optionValidator },\n multiselect: {\n kind: 'string',\n hasOptions: true,\n validator: (field, base) =>\n base.refine(v => splitMultiValue(v).every(part => (field.options ?? []).includes(part)), {\n message: `Please choose valid options for ${field.label}`,\n }),\n },\n checkbox: { kind: 'boolean', hasOptions: false, validator: () => z.boolean() },\n} as const satisfies Record<string, FormFieldTypeSpec>;\n\nexport type SupportedFormFieldType = keyof typeof FORM_FIELD_TYPES;\n\nexport const SUPPORTED_FORM_FIELD_TYPES = Object.keys(FORM_FIELD_TYPES) as readonly SupportedFormFieldType[];\n\n// ---------------------------------------------------------------------------\n// HubSpot type → control resolution (open set in, closed set out)\n// ---------------------------------------------------------------------------\n\n/**\n * What a HubSpot question resolves to: a drawable control, `display` (a\n * read-only block — `html`, `calculation_*` — never rendered, never\n * submitted) or `unanswerable` (nothing the booking POST can carry, e.g. a\n * file upload).\n */\nexport type FormFieldResolution = SupportedFormFieldType | 'display' | 'unanswerable';\n\n/**\n * HubSpot `fieldType` spellings → resolution. Keys are lower-cased. A spelling\n * missing here is NOT an error: resolution falls through to the data type and\n * then to a generic control. Add an entry only to draw a BETTER control for a\n * type, never to make one \"work\".\n *\n * `checkbox` is absent on purpose: in HubSpot it is \"multiple checkboxes\" (an\n * enumeration with options), while older links sent it for a single yes/no —\n * `resolveFormFieldControl` decides by whether options came with it.\n */\nconst FIELD_TYPE_CONTROLS: Readonly<Record<string, FormFieldResolution>> = {\n text: 'text',\n textarea: 'textarea',\n number: 'number',\n select: 'select',\n radio: 'radio',\n booleancheckbox: 'checkbox',\n phonenumber: 'phone',\n date: 'date',\n html: 'display',\n file: 'unanswerable',\n};\n\n/** HubSpot property data types (`type`) → resolution, used when `fieldType` is unknown. */\nconst DATA_TYPE_CONTROLS: Readonly<Record<string, FormFieldResolution>> = {\n string: 'text',\n number: 'number',\n enumeration: 'select',\n bool: 'checkbox',\n date: 'date',\n datetime: 'date',\n phone_number: 'phone',\n};\n\nconst hasDeclaredOptions = (field: MeetingFormField) => (field.options?.length ?? 0) > 0;\n\n/**\n * THE one mapping from a HubSpot question to what the form does with it.\n * Total: every input yields a resolution, so an unrecognised type can never\n * make a link unbookable — at worst it is a text box. An option control with\n * no options degrades to text (nothing to pick from, but still answerable).\n */\nexport function resolveFormFieldControl(field: MeetingFormField): FormFieldResolution {\n const fieldType = (field.type ?? '').toLowerCase();\n const withOptions = hasDeclaredOptions(field);\n const usable = (r: FormFieldResolution | undefined): FormFieldResolution | undefined => {\n if (!r || r === 'display' || r === 'unanswerable') return r;\n return FORM_FIELD_TYPES[r].hasOptions && !withOptions ? 'text' : r;\n };\n\n if (fieldType === 'checkbox') return withOptions ? 'multiselect' : 'checkbox';\n if (fieldType.startsWith('calculation')) return 'display';\n // A control name resolves to itself, so resolution is IDEMPOTENT: the form\n // resolves once to draw, then hands the resolved fields to the schema\n // factory, which resolves again (the server rebuild resolves raw ones).\n const asControl = Object.prototype.hasOwnProperty.call(FORM_FIELD_TYPES, fieldType)\n ? (fieldType as SupportedFormFieldType)\n : undefined;\n return (\n usable(FIELD_TYPE_CONTROLS[fieldType] ?? asControl) ??\n usable(DATA_TYPE_CONTROLS[(field.dataType ?? '').toLowerCase()]) ??\n (withOptions ? 'select' : 'text')\n );\n}\n\n/**\n * @deprecated Kept for hosts built against the closed six-type registry (they\n * asked this to decide whether to forward `options`). Answers for any HubSpot\n * type through the resolver: true when the type resolves to an option control\n * once options are present. New hosts forward options whenever HubSpot sends\n * them and do not need this.\n */\nexport function formFieldTypeHasOptions(type: string): boolean {\n const control = resolveFormFieldControl({ name: '', label: '', type, required: false, options: ['_'] });\n return control !== 'display' && control !== 'unanswerable' && FORM_FIELD_TYPES[control].hasOptions;\n}\n\n/** @deprecated The option controls (see `formFieldTypeHasOptions`). */\nexport const FORM_FIELD_TYPES_WITH_OPTIONS: readonly SupportedFormFieldType[] = SUPPORTED_FORM_FIELD_TYPES.filter(\n type => FORM_FIELD_TYPES[type].hasOptions,\n);\n\n/**\n * Whether the question's `fieldType` is one the resolver maps EXPLICITLY — as\n * opposed to guessing from its data type or falling back to text. A host logs\n * the false case: the form still works, but a better control may be one table\n * entry away.\n */\nexport function isRecognisedFormFieldType(type: string): boolean {\n const t = (type ?? '').toLowerCase();\n return (\n t === 'checkbox' ||\n t.startsWith('calculation') ||\n Object.prototype.hasOwnProperty.call(FIELD_TYPE_CONTROLS, t) ||\n Object.prototype.hasOwnProperty.call(FORM_FIELD_TYPES, t)\n );\n}\n\n/** A declared question after resolution: `type` is the CONTROL, `hubspotType` what HubSpot sent. */\nexport type SupportedMeetingFormField = MeetingFormField & { type: SupportedFormFieldType; hubspotType?: string };\n\n/** One question → its drawable form, or null when the form does not draw it (display / unanswerable). */\nexport function normalizeFormField(\n field: MeetingFormField & { hubspotType?: string },\n): SupportedMeetingFormField | null {\n const control = resolveFormFieldControl(field);\n if (control === 'display' || control === 'unanswerable') return null;\n // A control that takes no options drops them (a `booleancheckbox` arrives\n // with HubSpot's `true`/`false` pair), which also keeps a re-resolution of\n // the result on the same control.\n const normalized: SupportedMeetingFormField = {\n ...field,\n type: control,\n hubspotType: field.hubspotType ?? field.type,\n };\n if (!FORM_FIELD_TYPES[control].hasOptions) {\n delete normalized.options;\n delete normalized.optionLabels;\n }\n return normalized;\n}\n\n/** The questions the native form draws and validates, in declared order. */\nexport function normalizeFormFields(fields: readonly MeetingFormField[]): SupportedMeetingFormField[] {\n return fields.flatMap(f => normalizeFormField(f) ?? []);\n}\n\n/** Whether the native form draws this question (false for display blocks and unanswerable questions). */\nexport function isSupportedFormField(field: MeetingFormField): boolean {\n return normalizeFormField(field) !== null;\n}\n\n/**\n * The ONLY reason left to hand a link to HubSpot's own page: a REQUIRED\n * question no control can answer. An optional one is simply left out — the\n * visitor can still book, and HubSpot does not require it.\n */\nexport function blocksNativeBooking(field: MeetingFormField): boolean {\n return field.required && resolveFormFieldControl(field) === 'unanswerable';\n}\n\n/**\n * The scheduler's fixed identity fields. HubSpot's book endpoint takes them\n * TOP-LEVEL (`firstName`, `lastName`, `email`), its own booking page hardcodes\n * them, and the link's `formFields` never lists them — so they are data HERE,\n * rendered by the widget through the SAME control path as every declared\n * question, rather than three hand-written blocks. `inputType`/`autoComplete`\n * are the browser hints a text control takes; the wire and the validator do\n * not see them.\n */\nexport interface BuiltInBookingField extends MeetingFormField {\n type: 'text';\n required: true;\n inputType?: 'email';\n autoComplete: string;\n /** Own placeholder. Omitted, the control derives one from the label (the registry's rule). */\n placeholder?: string;\n /** The wire's own required/format message — kept verbatim from the schema it replaced. */\n requiredMessage: string;\n}\n\nexport const BUILT_IN_BOOKING_FIELDS = [\n {\n name: 'firstName',\n label: 'First Name',\n type: 'text',\n required: true,\n autoComplete: 'given-name',\n requiredMessage: 'First name is required',\n },\n {\n name: 'lastName',\n label: 'Last Name',\n type: 'text',\n required: true,\n autoComplete: 'family-name',\n requiredMessage: 'Last name is required',\n },\n {\n name: 'email',\n label: 'Email',\n type: 'text',\n required: true,\n inputType: 'email',\n autoComplete: 'email',\n placeholder: 'username@mail.com',\n requiredMessage: 'Please enter a valid email address',\n },\n] as const satisfies readonly BuiltInBookingField[];\n\n/** `'firstName' | 'lastName' | 'email'` — derived from the array, never restated. */\nexport type BuiltInBookingFieldName = (typeof BUILT_IN_BOOKING_FIELDS)[number]['name'];\n\n/** The registry entry for a supported type, widened to the spec so optional\n * members (`placeholder`) are readable without narrowing on the union. */\nexport function fieldTypeSpec(type: SupportedFormFieldType): FormFieldTypeSpec {\n return FORM_FIELD_TYPES[type];\n}\n\n/** The wire validator for one identity field, from ITS declaration above. */\nfunction identityValidator(name: BuiltInBookingFieldName) {\n const field: BuiltInBookingField | undefined = BUILT_IN_BOOKING_FIELDS.find(f => f.name === name);\n if (!field) throw new Error(`Unknown built-in booking field: ${name}`);\n // The field's own message FIRST: an empty value must fail `min`/`email` with\n // the wire's copy, not a length cap it cannot have hit.\n const tooLong = { message: `${field.label} is too long` };\n return field.inputType === 'email'\n ? z.string().email({ message: field.requiredMessage }).max(255, tooLong)\n : z.string().min(1, { message: field.requiredMessage }).max(255, tooLong);\n}\n\n// ---------------------------------------------------------------------------\n// Validators (single home — client widget and server rebuild both use these)\n// ---------------------------------------------------------------------------\n\nconst IANA_TZ_RE = /^(?:UTC|[A-Za-z_]+(?:\\/[A-Za-z0-9_+-]+)+)$/;\n\n/**\n * IANA timezone check. Shape prefilter, then the authoritative resolution\n * test: `Intl.DateTimeFormat` throws on unknown zones. NOT\n * `Intl.supportedValuesOf('timeZone')` — that list excludes `'UTC'` itself\n * (verified in Node), which is a legitimate booking zone. Reject, never coerce.\n */\nexport function isValidIanaTimezone(tz: string): boolean {\n if (!IANA_TZ_RE.test(tz)) return false;\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: tz });\n return true;\n } catch {\n return false;\n }\n}\n\n/** BCP-47 locale shape check via `Intl.getCanonicalLocales`. Reject, never coerce. */\nexport function isValidBcp47Locale(locale: string): boolean {\n try {\n return Intl.getCanonicalLocales(locale).length > 0;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Schema factory\n// ---------------------------------------------------------------------------\n\n/**\n * Build the booking-form schema for ONE link's declared questions + consent.\n *\n * A factory (not a static schema) because per-link required questions cannot\n * be expressed statically. The widget builds it from the availability payload\n * it rendered; the server REBUILDS it from the link's own fetched metadata —\n * required-consent enforcement flows from this rebuild, not a parallel check.\n *\n * Deliberately NOT `.strict()`: the humanity-signal fields\n * (`HUMANITY_SIGNAL_KEYS` from `utils/humanity-signals`) ride alongside in\n * the same POST body (read raw by the host's bot gate BEFORE parsing) and are\n * stripped server-side before anything reaches HubSpot. zod's default\n * unknown-key stripping means the parsed output never contains them.\n *\n * `timezone`/`locale` are POST-only presentation fields (the invite renders\n * in the visitor's local time) — this schema is the ONLY place a\n * client-supplied zone is accepted; the availability path is UTC-pinned.\n */\nfunction buildBookingSchema<TStart extends z.ZodTypeAny, TDuration extends z.ZodTypeAny, TZone extends z.ZodTypeAny>(\n formFields: MeetingFormField[],\n legalConsent: MeetingLegalConsent | null,\n // Passed IN rather than switched on a boolean: a `deferSlot: boolean`\n // parameter cannot be narrowed at type level, so every ternary inside would\n // infer a union and widen `startTimeMs`/`durationMs` to `number | null |\n // undefined` for BOTH schemas — including the server's wire type.\n slot: { startTimeMs: TStart; durationMs: TDuration; timezone: TZone },\n) {\n const answers: Record<string, z.ZodTypeAny> = {};\n // Resolved HERE, not by the caller: the server rebuild passes the link's raw\n // questions, so client and server resolve every type through the same rule.\n // Display blocks and unanswerable optional questions carry no answer.\n const drawn = normalizeFormFields(formFields);\n for (const field of drawn) {\n const spec: FormFieldTypeSpec = FORM_FIELD_TYPES[field.type];\n // required/optional wrapping follows the answer's wire KIND, not its type:\n // a boolean is true-or-absent, a string is non-empty-or-empty. For strings\n // the required check is the FIRST rule on the chain, so an empty answer\n // reports \"is required\" rather than the type's own message.\n let validator: z.ZodTypeAny;\n if (spec.kind === 'boolean') {\n validator = field.required ? z.literal(true, { message: `${field.label} is required` }) : z.boolean().optional();\n } else {\n const base = field.required ? z.string().min(1, { message: `${field.label} is required` }) : z.string();\n validator = spec.validator(field, base);\n if (!field.required) validator = validator.optional().or(z.literal(''));\n }\n answers[field.name] = validator;\n }\n\n const requiredConsentIds = (legalConsent?.communicationConsentCheckboxes ?? [])\n .filter(c => c.required)\n .map(c => c.communicationTypeId);\n\n // A required answer cannot be enforced by an `.optional()` parent object —\n // omitting the `formFields` key entirely would skip every per-question\n // rule. When the link declares at least one required supported question,\n // the object itself is required.\n const hasRequiredAnswers = drawn.some(f => f.required);\n const answersObject = z.object(answers);\n\n return (\n z\n .object({\n meetingId: z.string().min(1),\n startTimeMs: slot.startTimeMs,\n durationMs: slot.durationMs,\n // The identity trio's rules come from BUILT_IN_BOOKING_FIELDS — the\n // keys stay literal so the payload type keeps its named properties.\n firstName: identityValidator('firstName'),\n lastName: identityValidator('lastName'),\n email: identityValidator('email'),\n timezone: slot.timezone,\n locale: z.string().refine(isValidBcp47Locale, { message: 'Invalid locale' }).optional(),\n // Plain `.optional()` (no `.default()`) so zod's input and output types\n // match — react-hook-form's zodResolver needs them identical, and the\n // server handles `undefined` explicitly anyway. REQUIRED when the link\n // declares required questions (see `hasRequiredAnswers` above).\n formFields: hasRequiredAnswers ? answersObject : answersObject.optional(),\n legalConsentResponses: z\n .array(z.object({ communicationTypeId: z.string(), consented: z.boolean() }))\n .optional(),\n })\n // Object-level rule: `.refine` on an `.optional()` field is skipped when\n // the field is absent — required consents must reject even on a payload\n // that omits the array entirely.\n .superRefine((data, ctx) => {\n const responses = data.legalConsentResponses ?? [];\n const ok = requiredConsentIds.every(id =>\n responses.some(r => r.communicationTypeId === id && r.consented === true),\n );\n if (!ok) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['legalConsentResponses'],\n message: 'Required consent checkboxes must be accepted',\n });\n }\n })\n );\n}\n\n/** The slot's wire validators — ONE declaration for the strict and the deferred schema. */\nconst SLOT_INSTANT = z.number().int().positive();\nconst TIMEZONE = z.string().refine(isValidIanaTimezone, { message: 'Invalid timezone' });\n\n/**\n * The STRICT schema — the wire contract. The server rebuilds it from the link's\n * own fetched metadata and validates every booking against it; a slot and a\n * duration are not optional on anything that reaches HubSpot.\n */\nexport function makeBookingSchema(formFields: MeetingFormField[], legalConsent: MeetingLegalConsent | null) {\n return buildBookingSchema(formFields, legalConsent, {\n startTimeMs: SLOT_INSTANT,\n durationMs: SLOT_INSTANT,\n timezone: TIMEZONE,\n });\n}\n\n/**\n * The DEFERRED schema — same fields, with the slot/duration/timezone triple\n * relaxed. `flow=\"details-first\"` collects answers BEFORE a slot exists, so the\n * form validates against this and the parent re-attaches the authoritative\n * values at POST time.\n *\n * Two named exports over one private builder rather than an overload or an\n * options flag: `MeetingBookingPayload` is `z.infer<ReturnType<...>>`, and\n * either of those alternatives would widen `startTimeMs`/`durationMs` to\n * `number | undefined` for EVERY consumer — including the server's\n * `durationsMs.includes(data.durationMs)`.\n */\nexport function makeDeferredBookingSchema(formFields: MeetingFormField[], legalConsent: MeetingLegalConsent | null) {\n // `.nullish()`, not `.optional()`: the widget passes these straight into\n // `defaultValues`, and details-first has `timezone === null` on the server\n // render and the first client render (zone resolution is post-hydration).\n // Accepting only `undefined` would reject a field with no rendered control\n // and no FieldWrapper — the submit button silently dead, type-check clean.\n return buildBookingSchema(formFields, legalConsent, {\n startTimeMs: SLOT_INSTANT.nullish(),\n durationMs: SLOT_INSTANT.nullish(),\n timezone: TIMEZONE.nullish(),\n });\n}\n\n/**\n * Email domains a host's form does not accept, with the message it shows when\n * one is typed. The HOST owns the list (its own personal/free-provider rule)\n * and re-checks on its own server — this is the answer at the point of typing,\n * never the gate.\n */\nexport interface DeniedEmailDomains {\n domains: readonly string[];\n message: string;\n}\n\n/** The address's domain, lower-cased, matched WHOLE against the list — a\n * subdomain is a different domain, which is how a host's own list reads it. */\nexport function emailDomainDenied(email: unknown, rule: DeniedEmailDomains): boolean {\n if (typeof email !== 'string') return false;\n const domain = email.split('@')[1]?.trim().toLowerCase();\n if (domain === undefined || domain === '') return false;\n return rule.domains.includes(domain);\n}\n\n/** `schema` refined to reject those domains ON the email field, so the message\n * lands under the control the visitor typed in rather than as a form error. */\nexport function withDeniedEmailDomains(\n schema: ReturnType<typeof makeDeferredBookingSchema>,\n rule: DeniedEmailDomains | undefined,\n) {\n if (!rule) return schema;\n return schema.superRefine((values, ctx) => {\n if (emailDomainDenied(values.email, rule)) {\n ctx.addIssue({ code: 'custom', path: ['email'], message: rule.message });\n }\n });\n}\n\n/** The wire payload. Pinned to the STRICT builder — see above. */\nexport type MeetingBookingPayload = z.infer<ReturnType<typeof makeBookingSchema>>;\n\n/**\n * The form's value type, used as the `useForm` generic in BOTH flows: a relaxed\n * resolver is not assignable to `Resolver<MeetingBookingPayload>`, and mixing\n * the two survives only on TS's bivariant method-parameter check.\n */\nexport type BookingFormValues = z.infer<ReturnType<typeof makeDeferredBookingSchema>>;\n"]}
|
|
@@ -413,6 +413,40 @@ export declare function makeDeferredBookingSchema(formFields: MeetingFormField[]
|
|
|
413
413
|
consented: z.ZodBoolean;
|
|
414
414
|
}, z.core.$strip>>>;
|
|
415
415
|
}, z.core.$strip>;
|
|
416
|
+
/**
|
|
417
|
+
* Email domains a host's form does not accept, with the message it shows when
|
|
418
|
+
* one is typed. The HOST owns the list (its own personal/free-provider rule)
|
|
419
|
+
* and re-checks on its own server — this is the answer at the point of typing,
|
|
420
|
+
* never the gate.
|
|
421
|
+
*/
|
|
422
|
+
export interface DeniedEmailDomains {
|
|
423
|
+
domains: readonly string[];
|
|
424
|
+
message: string;
|
|
425
|
+
}
|
|
426
|
+
/** The address's domain, lower-cased, matched WHOLE against the list — a
|
|
427
|
+
* subdomain is a different domain, which is how a host's own list reads it. */
|
|
428
|
+
export declare function emailDomainDenied(email: unknown, rule: DeniedEmailDomains): boolean;
|
|
429
|
+
/** `schema` refined to reject those domains ON the email field, so the message
|
|
430
|
+
* lands under the control the visitor typed in rather than as a form error. */
|
|
431
|
+
export declare function withDeniedEmailDomains(schema: ReturnType<typeof makeDeferredBookingSchema>, rule: DeniedEmailDomains | undefined): z.ZodObject<{
|
|
432
|
+
meetingId: z.ZodString;
|
|
433
|
+
startTimeMs: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
434
|
+
durationMs: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
435
|
+
firstName: z.ZodString;
|
|
436
|
+
lastName: z.ZodString;
|
|
437
|
+
email: z.ZodString;
|
|
438
|
+
timezone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
439
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
440
|
+
formFields: z.ZodObject<{
|
|
441
|
+
[x: string]: z.ZodTypeAny<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
442
|
+
}, z.core.$strip> | z.ZodOptional<z.ZodObject<{
|
|
443
|
+
[x: string]: z.ZodTypeAny<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
444
|
+
}, z.core.$strip>>;
|
|
445
|
+
legalConsentResponses: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
446
|
+
communicationTypeId: z.ZodString;
|
|
447
|
+
consented: z.ZodBoolean;
|
|
448
|
+
}, z.core.$strip>>>;
|
|
449
|
+
}, z.core.$strip>;
|
|
416
450
|
/** The wire payload. Pinned to the STRICT builder — see above. */
|
|
417
451
|
export type MeetingBookingPayload = z.infer<ReturnType<typeof makeBookingSchema>>;
|
|
418
452
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meeting-booking-schema.d.ts","sourceRoot":"","sources":["../../src/schemas/meeting-booking-schema.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"meeting-booking-schema.d.ts","sourceRoot":"","sources":["../../src/schemas/meeting-booking-schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -258,7 +258,21 @@ function makeDeferredBookingSchema(formFields, legalConsent) {
|
|
|
258
258
|
timezone: TIMEZONE.nullish()
|
|
259
259
|
});
|
|
260
260
|
}
|
|
261
|
+
function emailDomainDenied(email, rule) {
|
|
262
|
+
if (typeof email !== "string") return false;
|
|
263
|
+
const domain = email.split("@")[1]?.trim().toLowerCase();
|
|
264
|
+
if (domain === void 0 || domain === "") return false;
|
|
265
|
+
return rule.domains.includes(domain);
|
|
266
|
+
}
|
|
267
|
+
function withDeniedEmailDomains(schema, rule) {
|
|
268
|
+
if (!rule) return schema;
|
|
269
|
+
return schema.superRefine((values, ctx) => {
|
|
270
|
+
if (emailDomainDenied(values.email, rule)) {
|
|
271
|
+
ctx.addIssue({ code: "custom", path: ["email"], message: rule.message });
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
261
275
|
|
|
262
|
-
export { BUILT_IN_BOOKING_FIELDS, DECIMAL_LITERAL_RE, FORM_FIELD_TYPES, FORM_FIELD_TYPES_WITH_OPTIONS, ISO_DATE_RE, MEETING_BOOKING_ERROR_CODES, MULTI_VALUE_SEPARATOR, PHONE_RE, SUPPORTED_FORM_FIELD_TYPES, blocksNativeBooking, fieldTypeSpec, formFieldTypeHasOptions, isRecognisedFormFieldType, isSupportedFormField, isValidBcp47Locale, isValidIanaTimezone, makeBookingSchema, makeDeferredBookingSchema, normalizeFormField, normalizeFormFields, resolveFormFieldControl, splitMultiValue };
|
|
276
|
+
export { BUILT_IN_BOOKING_FIELDS, DECIMAL_LITERAL_RE, FORM_FIELD_TYPES, FORM_FIELD_TYPES_WITH_OPTIONS, ISO_DATE_RE, MEETING_BOOKING_ERROR_CODES, MULTI_VALUE_SEPARATOR, PHONE_RE, SUPPORTED_FORM_FIELD_TYPES, blocksNativeBooking, emailDomainDenied, fieldTypeSpec, formFieldTypeHasOptions, isRecognisedFormFieldType, isSupportedFormField, isValidBcp47Locale, isValidIanaTimezone, makeBookingSchema, makeDeferredBookingSchema, normalizeFormField, normalizeFormFields, resolveFormFieldControl, splitMultiValue, withDeniedEmailDomains };
|
|
263
277
|
//# sourceMappingURL=meeting-booking-schema.js.map
|
|
264
278
|
//# sourceMappingURL=meeting-booking-schema.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/schemas/meeting-booking-schema.ts"],"names":[],"mappings":"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","file":"meeting-booking-schema.js","sourcesContent":["import { z } from 'zod';\n\n/**\n * Meeting-booking wire contracts + validation factory.\n *\n * SERVER-SAFE tsup entry (no \"use client\" banner) with its OWN per-file\n * subpath (`./schemas/meeting-booking-schema`) — same pattern and reasons as\n * `schemas/contact-schema`: used by BOTH the lib's `<HubSpotMeetingScheduler>`\n * (client-side validation) AND the host's server-side booking route, which\n * REBUILDS the schema from the link's own fetched metadata and never trusts a\n * client-shaped instance. zod is an optional peer quarantined to per-subpath\n * verticals — do NOT re-export this module through any broad barrel.\n *\n * Every cross-boundary type for the scheduling feature lives HERE (the lib\n * owns the contract; hosts import this subpath directly, type-only where\n * possible).\n */\n\n// ---------------------------------------------------------------------------\n// Wire types\n// ---------------------------------------------------------------------------\n\n/**\n * Sanitized availability payload served by the host proxy\n * (`GET {apiBaseUrl}/api/meetings/availability?meeting=<id>&monthOffset=<n>`).\n *\n * NO timezone field on purpose: slot starts are absolute epoch-ms instants\n * (verified timezone-independent against the live API), upstream fetches are\n * UTC-pinned, and ALL zone rendering happens client-side. Slots must be\n * whitelist-copied from HubSpot's `linkAvailability` ONLY — never derived\n * from busy-time data.\n */\nexport interface MeetingAvailability {\n meetingId: string;\n monthOffset: number;\n hasMore: boolean;\n /** Offered durations in ms — HubSpot's native unit for the booking POST. */\n durationsMs: number[];\n /** Bookable slot start times (epoch ms), keyed by duration in ms. */\n slotsByDurationMs: Record<string, number[]>;\n formFields: MeetingFormField[];\n /** Verbatim whitelist-copy of HubSpot's `legalConsentOptions` when consent is enabled; null when disabled. */\n legalConsent: MeetingLegalConsent | null;\n /**\n * Who the visitor is meeting — whitelisted DISPLAY projection the host DAL\n * builds from its own people data (e.g. a profiles table matched\n * server-side). NEVER carries emails or busy-time data; optional so\n * existing hosts stay wire-compatible.\n */\n hosts?: MeetingHost[];\n}\n\n/** Display-only host identity for the scheduler's context panel. */\nexport interface MeetingHost {\n name: string;\n avatarUrl: string | null;\n /** Job title / role line under the name (null → omitted). */\n title: string | null;\n}\n\n/**\n * One scheduling link on the DIRECTORY wire (`GET /api/meetings`) — the\n * host-DAL whitelist projection consumed by `MeetingSchedulerDirectory` and\n * host pages. Never carries organizer emails/busy-time data.\n */\nexport interface SchedulingLink {\n id: string;\n /** The link's public HubSpot booking URL — escape-hatch target only. */\n link: string;\n /** HubSpot slug path — the row's in-app destination is `<basePath>/<slug>`. */\n slug: string;\n /** Audience group key (slugified audience label; `\"other\"` in scope=all). */\n purpose: string;\n title: string;\n description: string | null;\n kind: 'personal' | 'team';\n /** Display-only minutes projection (booking stays ms end-to-end). */\n durationsMinutes: number[];\n hosts: MeetingHost[];\n /** Earliest bookable slot (epoch ms) from the current-month payload. */\n nextAvailableMs: number | null;\n}\n\nexport interface SchedulingLinksPayload {\n purposes: Array<{ purpose: string; label: string; links: SchedulingLink[] }>;\n fetchedAt: string;\n}\n\n/**\n * One declared question as the host forwards it. `type` is HubSpot's\n * `fieldType` (the control HubSpot draws: `phonenumber`, `booleancheckbox`…)\n * VERBATIM, and `dataType` its `type` (the stored value's kind: `string`,\n * `enumeration`, `phone_number`…). Neither is a closed set: the widget resolves\n * ANY pair to a control through `resolveFormFieldControl`, so a type HubSpot\n * adds tomorrow renders as the nearest control instead of taking the form down.\n */\nexport interface MeetingFormField {\n name: string;\n label: string;\n type: string;\n /** HubSpot's property data type — the fallback when `type` is unknown. Optional: older hosts omit it. */\n dataType?: string;\n required: boolean;\n /** Option VALUES (what is submitted). */\n options?: string[];\n /** Option value → display label, when HubSpot's label differs from the value. */\n optionLabels?: Record<string, string>;\n}\n\n/**\n * HubSpot's consent copy, rendered VERBATIM by the widget (GDPR surface —\n * never edited, never summarized). Responses are keyed by\n * `communicationTypeId`.\n */\nexport interface MeetingLegalConsent {\n processingConsentText: string;\n processingConsentCheckboxLabel: string | null;\n communicationConsentText: string | null;\n communicationConsentCheckboxes: Array<{\n communicationTypeId: string;\n label: string;\n required: boolean;\n }>;\n privacyPolicyText: string | null;\n isLegitimateInterest: boolean;\n}\n\n/**\n * Whitelisted booking result returned by the host proxy — the THIRD HubSpot\n * payload that reaches a browser, so it gets the same whitelist-copy\n * treatment as the two GETs. Nothing organizer-derived.\n */\nexport interface BookingConfirmation {\n meetingId: string;\n title: string;\n startTimeMs: number;\n durationMs: number;\n}\n\n/**\n * Typed domain errors the booking route emits; the widget keys its recovery\n * UI off these. `SLOT_TAKEN` → refetch-and-recover; `TEMPORARILY_UNAVAILABLE`\n * → retry affordance; `MEETING_UNAVAILABLE` → daily ceiling exhausted\n * (escape hatch, not a retry timer); `LINK_GONE` → link deleted upstream;\n * `INVALID_EMAIL` → HubSpot rejected the attendee address (its\n * MeetingsBookingCreatedError.INVALID_EMAIL class — fake/unreachable\n * mailboxes), so the widget tells the visitor to fix the email instead of\n * blaming the slot or their other details.\n */\nexport const MEETING_BOOKING_ERROR_CODES = [\n 'SLOT_TAKEN',\n 'VALIDATION',\n 'INVALID_EMAIL',\n 'LINK_GONE',\n 'TEMPORARILY_UNAVAILABLE',\n 'MEETING_UNAVAILABLE',\n] as const;\n\n/**\n * Derived from the runtime array ABOVE — the array is the single source of\n * truth. The booking hook validates server codes against it at runtime, so a\n * code added only to a hand-written type would be silently coerced to\n * TEMPORARILY_UNAVAILABLE by every deployed widget (exactly how the\n * INVALID_EMAIL rollout mis-rendered on 2026-08-27: type extended, runtime\n * allowlist stale — an array annotation is not exhaustiveness-checked).\n */\nexport type MeetingBookingErrorCode = (typeof MEETING_BOOKING_ERROR_CODES)[number];\n\n// ---------------------------------------------------------------------------\n// Field-type vocabulary — ONE set drives the renderer AND the validator\n// ---------------------------------------------------------------------------\n\n/**\n * THE registry of CONTROLS the native form can draw — one entry per control,\n * and the ONLY place a control is declared. Everything else derives from it:\n * `SupportedFormFieldType` is its key union, `SUPPORTED_FORM_FIELD_TYPES` its\n * keys, and `makeBookingSchema` maps each answer through the entry's\n * validator. The widget's control table (`booking-form.tsx`) is a `Record`\n * over the same key union, so a control added here without a renderer is a\n * COMPILE error, not a silent gap.\n *\n * These are controls, NOT HubSpot's type vocabulary. A HubSpot question\n * reaches a control through `resolveFormFieldControl` (exact `fieldType`, then\n * its data type, then a generic fallback), so the open set of HubSpot types\n * maps onto this closed set of controls and no type is ever \"unsupported\".\n * The one thing that still falls back to HubSpot's own page is a REQUIRED\n * question no control can answer (a file upload) — see `blocksNativeBooking`.\n *\n * Every string control rides the wire as a STRING (HubSpot's book endpoint\n * takes `{ name, value: string }`); `checkbox` is the one boolean. `number` is\n * a decimal literal, `multiselect` HubSpot's `;`-joined enumeration value,\n * `date` an ISO `YYYY-MM-DD`.\n */\nexport interface FormFieldTypeSpec {\n /** The answer's wire shape, which also decides how required/optional wraps it. */\n kind: 'string' | 'boolean';\n /** Whether HubSpot publishes `options` for the type (the pickers). */\n hasOptions: boolean;\n /**\n * The validator for ONE answer. String types EXTEND `base`, which already\n * carries the required-ness (`min(1)` when required) — so \"X is required\" is\n * the first issue reported for an empty answer, ahead of the type's own rule.\n * Boolean types ignore it.\n */\n validator: (field: MeetingFormField, base: z.ZodString) => z.ZodTypeAny;\n /** The control's placeholder, derived from the field (the mock's \"Enter Company Name\"). */\n placeholder?: (field: MeetingFormField) => string;\n}\n\nconst optionValidator: FormFieldTypeSpec['validator'] = (field, base) =>\n base.refine(v => !v || (field.options ?? []).includes(v), {\n message: `Please choose a valid option for ${field.label}`,\n });\n\n/** The wire shape of a number answer — what the form canonicalises TO and the validator checks. */\nexport const DECIMAL_LITERAL_RE = /^-?\\d+(\\.\\d+)?$/;\n\n/** HubSpot's separator for a multi-value enumeration (`a;b;c`). */\nexport const MULTI_VALUE_SEPARATOR = ';';\n\n/** Split a multiselect answer into its values (empty string → none). */\nexport const splitMultiValue = (value: unknown): string[] =>\n typeof value === 'string' && value ? value.split(MULTI_VALUE_SEPARATOR) : [];\n\n/**\n * A phone answer: digits with the punctuation people actually type (`+1 (415)\n * 555-2671`, `020 7946 0958 ext. 12`). Deliberately a SHAPE check, not a\n * numbering-plan check — HubSpot stores the string as typed, and rejecting a\n * valid number we failed to parse is worse than accepting an odd one.\n */\nexport const PHONE_RE = /^\\+?[\\d\\s().\\-/]+(?:\\s*(?:x|ext\\.?)\\s*\\d+)?$/i;\nconst PHONE_MIN_DIGITS = 5;\nconst PHONE_MAX_DIGITS = 20;\nconst phoneDigits = (v: string) => v.replace(/\\D/g, '').length;\n\n/** A date answer as `<input type=\"date\">` emits it. */\nexport const ISO_DATE_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst isRealIsoDate = (v: string) => {\n if (!ISO_DATE_RE.test(v)) return false;\n const d = new Date(`${v}T00:00:00Z`);\n return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === v;\n};\n\nexport const FORM_FIELD_TYPES = {\n text: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) => base.max(1000, { message: `${field.label} is too long` }),\n placeholder: field => `Enter ${field.label}`,\n },\n textarea: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) => base.max(5000, { message: `${field.label} is too long` }),\n placeholder: field => `Enter text${field.required ? '' : ' (optional)'}`,\n },\n number: {\n kind: 'string',\n hasOptions: false,\n // Canonical decimal literal — the control normalises what the browser\n // accepts (`1e3`, `007`) to this before it is validated or sent.\n validator: (field, base) =>\n base\n .max(32, { message: `${field.label} is too long` })\n .regex(DECIMAL_LITERAL_RE, { message: `${field.label} must be a number` }),\n },\n phone: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) =>\n base\n .max(40, { message: `${field.label} is too long` })\n .refine(\n v => !v || (PHONE_RE.test(v) && phoneDigits(v) >= PHONE_MIN_DIGITS && phoneDigits(v) <= PHONE_MAX_DIGITS),\n { message: `Please enter a valid phone number for ${field.label}` },\n ),\n placeholder: field => `Enter ${field.label}`,\n },\n date: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) => base.refine(v => !v || isRealIsoDate(v), { message: `${field.label} must be a date` }),\n },\n select: { kind: 'string', hasOptions: true, validator: optionValidator },\n radio: { kind: 'string', hasOptions: true, validator: optionValidator },\n multiselect: {\n kind: 'string',\n hasOptions: true,\n validator: (field, base) =>\n base.refine(v => splitMultiValue(v).every(part => (field.options ?? []).includes(part)), {\n message: `Please choose valid options for ${field.label}`,\n }),\n },\n checkbox: { kind: 'boolean', hasOptions: false, validator: () => z.boolean() },\n} as const satisfies Record<string, FormFieldTypeSpec>;\n\nexport type SupportedFormFieldType = keyof typeof FORM_FIELD_TYPES;\n\nexport const SUPPORTED_FORM_FIELD_TYPES = Object.keys(FORM_FIELD_TYPES) as readonly SupportedFormFieldType[];\n\n// ---------------------------------------------------------------------------\n// HubSpot type → control resolution (open set in, closed set out)\n// ---------------------------------------------------------------------------\n\n/**\n * What a HubSpot question resolves to: a drawable control, `display` (a\n * read-only block — `html`, `calculation_*` — never rendered, never\n * submitted) or `unanswerable` (nothing the booking POST can carry, e.g. a\n * file upload).\n */\nexport type FormFieldResolution = SupportedFormFieldType | 'display' | 'unanswerable';\n\n/**\n * HubSpot `fieldType` spellings → resolution. Keys are lower-cased. A spelling\n * missing here is NOT an error: resolution falls through to the data type and\n * then to a generic control. Add an entry only to draw a BETTER control for a\n * type, never to make one \"work\".\n *\n * `checkbox` is absent on purpose: in HubSpot it is \"multiple checkboxes\" (an\n * enumeration with options), while older links sent it for a single yes/no —\n * `resolveFormFieldControl` decides by whether options came with it.\n */\nconst FIELD_TYPE_CONTROLS: Readonly<Record<string, FormFieldResolution>> = {\n text: 'text',\n textarea: 'textarea',\n number: 'number',\n select: 'select',\n radio: 'radio',\n booleancheckbox: 'checkbox',\n phonenumber: 'phone',\n date: 'date',\n html: 'display',\n file: 'unanswerable',\n};\n\n/** HubSpot property data types (`type`) → resolution, used when `fieldType` is unknown. */\nconst DATA_TYPE_CONTROLS: Readonly<Record<string, FormFieldResolution>> = {\n string: 'text',\n number: 'number',\n enumeration: 'select',\n bool: 'checkbox',\n date: 'date',\n datetime: 'date',\n phone_number: 'phone',\n};\n\nconst hasDeclaredOptions = (field: MeetingFormField) => (field.options?.length ?? 0) > 0;\n\n/**\n * THE one mapping from a HubSpot question to what the form does with it.\n * Total: every input yields a resolution, so an unrecognised type can never\n * make a link unbookable — at worst it is a text box. An option control with\n * no options degrades to text (nothing to pick from, but still answerable).\n */\nexport function resolveFormFieldControl(field: MeetingFormField): FormFieldResolution {\n const fieldType = (field.type ?? '').toLowerCase();\n const withOptions = hasDeclaredOptions(field);\n const usable = (r: FormFieldResolution | undefined): FormFieldResolution | undefined => {\n if (!r || r === 'display' || r === 'unanswerable') return r;\n return FORM_FIELD_TYPES[r].hasOptions && !withOptions ? 'text' : r;\n };\n\n if (fieldType === 'checkbox') return withOptions ? 'multiselect' : 'checkbox';\n if (fieldType.startsWith('calculation')) return 'display';\n // A control name resolves to itself, so resolution is IDEMPOTENT: the form\n // resolves once to draw, then hands the resolved fields to the schema\n // factory, which resolves again (the server rebuild resolves raw ones).\n const asControl = Object.prototype.hasOwnProperty.call(FORM_FIELD_TYPES, fieldType)\n ? (fieldType as SupportedFormFieldType)\n : undefined;\n return (\n usable(FIELD_TYPE_CONTROLS[fieldType] ?? asControl) ??\n usable(DATA_TYPE_CONTROLS[(field.dataType ?? '').toLowerCase()]) ??\n (withOptions ? 'select' : 'text')\n );\n}\n\n/**\n * @deprecated Kept for hosts built against the closed six-type registry (they\n * asked this to decide whether to forward `options`). Answers for any HubSpot\n * type through the resolver: true when the type resolves to an option control\n * once options are present. New hosts forward options whenever HubSpot sends\n * them and do not need this.\n */\nexport function formFieldTypeHasOptions(type: string): boolean {\n const control = resolveFormFieldControl({ name: '', label: '', type, required: false, options: ['_'] });\n return control !== 'display' && control !== 'unanswerable' && FORM_FIELD_TYPES[control].hasOptions;\n}\n\n/** @deprecated The option controls (see `formFieldTypeHasOptions`). */\nexport const FORM_FIELD_TYPES_WITH_OPTIONS: readonly SupportedFormFieldType[] = SUPPORTED_FORM_FIELD_TYPES.filter(\n type => FORM_FIELD_TYPES[type].hasOptions,\n);\n\n/**\n * Whether the question's `fieldType` is one the resolver maps EXPLICITLY — as\n * opposed to guessing from its data type or falling back to text. A host logs\n * the false case: the form still works, but a better control may be one table\n * entry away.\n */\nexport function isRecognisedFormFieldType(type: string): boolean {\n const t = (type ?? '').toLowerCase();\n return (\n t === 'checkbox' ||\n t.startsWith('calculation') ||\n Object.prototype.hasOwnProperty.call(FIELD_TYPE_CONTROLS, t) ||\n Object.prototype.hasOwnProperty.call(FORM_FIELD_TYPES, t)\n );\n}\n\n/** A declared question after resolution: `type` is the CONTROL, `hubspotType` what HubSpot sent. */\nexport type SupportedMeetingFormField = MeetingFormField & { type: SupportedFormFieldType; hubspotType?: string };\n\n/** One question → its drawable form, or null when the form does not draw it (display / unanswerable). */\nexport function normalizeFormField(\n field: MeetingFormField & { hubspotType?: string },\n): SupportedMeetingFormField | null {\n const control = resolveFormFieldControl(field);\n if (control === 'display' || control === 'unanswerable') return null;\n // A control that takes no options drops them (a `booleancheckbox` arrives\n // with HubSpot's `true`/`false` pair), which also keeps a re-resolution of\n // the result on the same control.\n const normalized: SupportedMeetingFormField = {\n ...field,\n type: control,\n hubspotType: field.hubspotType ?? field.type,\n };\n if (!FORM_FIELD_TYPES[control].hasOptions) {\n delete normalized.options;\n delete normalized.optionLabels;\n }\n return normalized;\n}\n\n/** The questions the native form draws and validates, in declared order. */\nexport function normalizeFormFields(fields: readonly MeetingFormField[]): SupportedMeetingFormField[] {\n return fields.flatMap(f => normalizeFormField(f) ?? []);\n}\n\n/** Whether the native form draws this question (false for display blocks and unanswerable questions). */\nexport function isSupportedFormField(field: MeetingFormField): boolean {\n return normalizeFormField(field) !== null;\n}\n\n/**\n * The ONLY reason left to hand a link to HubSpot's own page: a REQUIRED\n * question no control can answer. An optional one is simply left out — the\n * visitor can still book, and HubSpot does not require it.\n */\nexport function blocksNativeBooking(field: MeetingFormField): boolean {\n return field.required && resolveFormFieldControl(field) === 'unanswerable';\n}\n\n/**\n * The scheduler's fixed identity fields. HubSpot's book endpoint takes them\n * TOP-LEVEL (`firstName`, `lastName`, `email`), its own booking page hardcodes\n * them, and the link's `formFields` never lists them — so they are data HERE,\n * rendered by the widget through the SAME control path as every declared\n * question, rather than three hand-written blocks. `inputType`/`autoComplete`\n * are the browser hints a text control takes; the wire and the validator do\n * not see them.\n */\nexport interface BuiltInBookingField extends MeetingFormField {\n type: 'text';\n required: true;\n inputType?: 'email';\n autoComplete: string;\n /** Own placeholder. Omitted, the control derives one from the label (the registry's rule). */\n placeholder?: string;\n /** The wire's own required/format message — kept verbatim from the schema it replaced. */\n requiredMessage: string;\n}\n\nexport const BUILT_IN_BOOKING_FIELDS = [\n {\n name: 'firstName',\n label: 'First Name',\n type: 'text',\n required: true,\n autoComplete: 'given-name',\n requiredMessage: 'First name is required',\n },\n {\n name: 'lastName',\n label: 'Last Name',\n type: 'text',\n required: true,\n autoComplete: 'family-name',\n requiredMessage: 'Last name is required',\n },\n {\n name: 'email',\n label: 'Email',\n type: 'text',\n required: true,\n inputType: 'email',\n autoComplete: 'email',\n placeholder: 'username@mail.com',\n requiredMessage: 'Please enter a valid email address',\n },\n] as const satisfies readonly BuiltInBookingField[];\n\n/** `'firstName' | 'lastName' | 'email'` — derived from the array, never restated. */\nexport type BuiltInBookingFieldName = (typeof BUILT_IN_BOOKING_FIELDS)[number]['name'];\n\n/** The registry entry for a supported type, widened to the spec so optional\n * members (`placeholder`) are readable without narrowing on the union. */\nexport function fieldTypeSpec(type: SupportedFormFieldType): FormFieldTypeSpec {\n return FORM_FIELD_TYPES[type];\n}\n\n/** The wire validator for one identity field, from ITS declaration above. */\nfunction identityValidator(name: BuiltInBookingFieldName) {\n const field: BuiltInBookingField | undefined = BUILT_IN_BOOKING_FIELDS.find(f => f.name === name);\n if (!field) throw new Error(`Unknown built-in booking field: ${name}`);\n // The field's own message FIRST: an empty value must fail `min`/`email` with\n // the wire's copy, not a length cap it cannot have hit.\n const tooLong = { message: `${field.label} is too long` };\n return field.inputType === 'email'\n ? z.string().email({ message: field.requiredMessage }).max(255, tooLong)\n : z.string().min(1, { message: field.requiredMessage }).max(255, tooLong);\n}\n\n// ---------------------------------------------------------------------------\n// Validators (single home — client widget and server rebuild both use these)\n// ---------------------------------------------------------------------------\n\nconst IANA_TZ_RE = /^(?:UTC|[A-Za-z_]+(?:\\/[A-Za-z0-9_+-]+)+)$/;\n\n/**\n * IANA timezone check. Shape prefilter, then the authoritative resolution\n * test: `Intl.DateTimeFormat` throws on unknown zones. NOT\n * `Intl.supportedValuesOf('timeZone')` — that list excludes `'UTC'` itself\n * (verified in Node), which is a legitimate booking zone. Reject, never coerce.\n */\nexport function isValidIanaTimezone(tz: string): boolean {\n if (!IANA_TZ_RE.test(tz)) return false;\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: tz });\n return true;\n } catch {\n return false;\n }\n}\n\n/** BCP-47 locale shape check via `Intl.getCanonicalLocales`. Reject, never coerce. */\nexport function isValidBcp47Locale(locale: string): boolean {\n try {\n return Intl.getCanonicalLocales(locale).length > 0;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Schema factory\n// ---------------------------------------------------------------------------\n\n/**\n * Build the booking-form schema for ONE link's declared questions + consent.\n *\n * A factory (not a static schema) because per-link required questions cannot\n * be expressed statically. The widget builds it from the availability payload\n * it rendered; the server REBUILDS it from the link's own fetched metadata —\n * required-consent enforcement flows from this rebuild, not a parallel check.\n *\n * Deliberately NOT `.strict()`: the humanity-signal fields\n * (`HUMANITY_SIGNAL_KEYS` from `utils/humanity-signals`) ride alongside in\n * the same POST body (read raw by the host's bot gate BEFORE parsing) and are\n * stripped server-side before anything reaches HubSpot. zod's default\n * unknown-key stripping means the parsed output never contains them.\n *\n * `timezone`/`locale` are POST-only presentation fields (the invite renders\n * in the visitor's local time) — this schema is the ONLY place a\n * client-supplied zone is accepted; the availability path is UTC-pinned.\n */\nfunction buildBookingSchema<TStart extends z.ZodTypeAny, TDuration extends z.ZodTypeAny, TZone extends z.ZodTypeAny>(\n formFields: MeetingFormField[],\n legalConsent: MeetingLegalConsent | null,\n // Passed IN rather than switched on a boolean: a `deferSlot: boolean`\n // parameter cannot be narrowed at type level, so every ternary inside would\n // infer a union and widen `startTimeMs`/`durationMs` to `number | null |\n // undefined` for BOTH schemas — including the server's wire type.\n slot: { startTimeMs: TStart; durationMs: TDuration; timezone: TZone },\n) {\n const answers: Record<string, z.ZodTypeAny> = {};\n // Resolved HERE, not by the caller: the server rebuild passes the link's raw\n // questions, so client and server resolve every type through the same rule.\n // Display blocks and unanswerable optional questions carry no answer.\n const drawn = normalizeFormFields(formFields);\n for (const field of drawn) {\n const spec: FormFieldTypeSpec = FORM_FIELD_TYPES[field.type];\n // required/optional wrapping follows the answer's wire KIND, not its type:\n // a boolean is true-or-absent, a string is non-empty-or-empty. For strings\n // the required check is the FIRST rule on the chain, so an empty answer\n // reports \"is required\" rather than the type's own message.\n let validator: z.ZodTypeAny;\n if (spec.kind === 'boolean') {\n validator = field.required ? z.literal(true, { message: `${field.label} is required` }) : z.boolean().optional();\n } else {\n const base = field.required ? z.string().min(1, { message: `${field.label} is required` }) : z.string();\n validator = spec.validator(field, base);\n if (!field.required) validator = validator.optional().or(z.literal(''));\n }\n answers[field.name] = validator;\n }\n\n const requiredConsentIds = (legalConsent?.communicationConsentCheckboxes ?? [])\n .filter(c => c.required)\n .map(c => c.communicationTypeId);\n\n // A required answer cannot be enforced by an `.optional()` parent object —\n // omitting the `formFields` key entirely would skip every per-question\n // rule. When the link declares at least one required supported question,\n // the object itself is required.\n const hasRequiredAnswers = drawn.some(f => f.required);\n const answersObject = z.object(answers);\n\n return (\n z\n .object({\n meetingId: z.string().min(1),\n startTimeMs: slot.startTimeMs,\n durationMs: slot.durationMs,\n // The identity trio's rules come from BUILT_IN_BOOKING_FIELDS — the\n // keys stay literal so the payload type keeps its named properties.\n firstName: identityValidator('firstName'),\n lastName: identityValidator('lastName'),\n email: identityValidator('email'),\n timezone: slot.timezone,\n locale: z.string().refine(isValidBcp47Locale, { message: 'Invalid locale' }).optional(),\n // Plain `.optional()` (no `.default()`) so zod's input and output types\n // match — react-hook-form's zodResolver needs them identical, and the\n // server handles `undefined` explicitly anyway. REQUIRED when the link\n // declares required questions (see `hasRequiredAnswers` above).\n formFields: hasRequiredAnswers ? answersObject : answersObject.optional(),\n legalConsentResponses: z\n .array(z.object({ communicationTypeId: z.string(), consented: z.boolean() }))\n .optional(),\n })\n // Object-level rule: `.refine` on an `.optional()` field is skipped when\n // the field is absent — required consents must reject even on a payload\n // that omits the array entirely.\n .superRefine((data, ctx) => {\n const responses = data.legalConsentResponses ?? [];\n const ok = requiredConsentIds.every(id =>\n responses.some(r => r.communicationTypeId === id && r.consented === true),\n );\n if (!ok) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['legalConsentResponses'],\n message: 'Required consent checkboxes must be accepted',\n });\n }\n })\n );\n}\n\n/** The slot's wire validators — ONE declaration for the strict and the deferred schema. */\nconst SLOT_INSTANT = z.number().int().positive();\nconst TIMEZONE = z.string().refine(isValidIanaTimezone, { message: 'Invalid timezone' });\n\n/**\n * The STRICT schema — the wire contract. The server rebuilds it from the link's\n * own fetched metadata and validates every booking against it; a slot and a\n * duration are not optional on anything that reaches HubSpot.\n */\nexport function makeBookingSchema(formFields: MeetingFormField[], legalConsent: MeetingLegalConsent | null) {\n return buildBookingSchema(formFields, legalConsent, {\n startTimeMs: SLOT_INSTANT,\n durationMs: SLOT_INSTANT,\n timezone: TIMEZONE,\n });\n}\n\n/**\n * The DEFERRED schema — same fields, with the slot/duration/timezone triple\n * relaxed. `flow=\"details-first\"` collects answers BEFORE a slot exists, so the\n * form validates against this and the parent re-attaches the authoritative\n * values at POST time.\n *\n * Two named exports over one private builder rather than an overload or an\n * options flag: `MeetingBookingPayload` is `z.infer<ReturnType<...>>`, and\n * either of those alternatives would widen `startTimeMs`/`durationMs` to\n * `number | undefined` for EVERY consumer — including the server's\n * `durationsMs.includes(data.durationMs)`.\n */\nexport function makeDeferredBookingSchema(formFields: MeetingFormField[], legalConsent: MeetingLegalConsent | null) {\n // `.nullish()`, not `.optional()`: the widget passes these straight into\n // `defaultValues`, and details-first has `timezone === null` on the server\n // render and the first client render (zone resolution is post-hydration).\n // Accepting only `undefined` would reject a field with no rendered control\n // and no FieldWrapper — the submit button silently dead, type-check clean.\n return buildBookingSchema(formFields, legalConsent, {\n startTimeMs: SLOT_INSTANT.nullish(),\n durationMs: SLOT_INSTANT.nullish(),\n timezone: TIMEZONE.nullish(),\n });\n}\n\n/** The wire payload. Pinned to the STRICT builder — see above. */\nexport type MeetingBookingPayload = z.infer<ReturnType<typeof makeBookingSchema>>;\n\n/**\n * The form's value type, used as the `useForm` generic in BOTH flows: a relaxed\n * resolver is not assignable to `Resolver<MeetingBookingPayload>`, and mixing\n * the two survives only on TS's bivariant method-parameter check.\n */\nexport type BookingFormValues = z.infer<ReturnType<typeof makeDeferredBookingSchema>>;\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/schemas/meeting-booking-schema.ts"],"names":[],"mappings":"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","file":"meeting-booking-schema.js","sourcesContent":["import { z } from 'zod';\n\n/**\n * Meeting-booking wire contracts + validation factory.\n *\n * SERVER-SAFE tsup entry (no \"use client\" banner) with its OWN per-file\n * subpath (`./schemas/meeting-booking-schema`) — same pattern and reasons as\n * `schemas/contact-schema`: used by BOTH the lib's `<HubSpotMeetingScheduler>`\n * (client-side validation) AND the host's server-side booking route, which\n * REBUILDS the schema from the link's own fetched metadata and never trusts a\n * client-shaped instance. zod is an optional peer quarantined to per-subpath\n * verticals — do NOT re-export this module through any broad barrel.\n *\n * Every cross-boundary type for the scheduling feature lives HERE (the lib\n * owns the contract; hosts import this subpath directly, type-only where\n * possible).\n */\n\n// ---------------------------------------------------------------------------\n// Wire types\n// ---------------------------------------------------------------------------\n\n/**\n * Sanitized availability payload served by the host proxy\n * (`GET {apiBaseUrl}/api/meetings/availability?meeting=<id>&monthOffset=<n>`).\n *\n * NO timezone field on purpose: slot starts are absolute epoch-ms instants\n * (verified timezone-independent against the live API), upstream fetches are\n * UTC-pinned, and ALL zone rendering happens client-side. Slots must be\n * whitelist-copied from HubSpot's `linkAvailability` ONLY — never derived\n * from busy-time data.\n */\nexport interface MeetingAvailability {\n meetingId: string;\n monthOffset: number;\n hasMore: boolean;\n /** Offered durations in ms — HubSpot's native unit for the booking POST. */\n durationsMs: number[];\n /** Bookable slot start times (epoch ms), keyed by duration in ms. */\n slotsByDurationMs: Record<string, number[]>;\n formFields: MeetingFormField[];\n /** Verbatim whitelist-copy of HubSpot's `legalConsentOptions` when consent is enabled; null when disabled. */\n legalConsent: MeetingLegalConsent | null;\n /**\n * Who the visitor is meeting — whitelisted DISPLAY projection the host DAL\n * builds from its own people data (e.g. a profiles table matched\n * server-side). NEVER carries emails or busy-time data; optional so\n * existing hosts stay wire-compatible.\n */\n hosts?: MeetingHost[];\n}\n\n/** Display-only host identity for the scheduler's context panel. */\nexport interface MeetingHost {\n name: string;\n avatarUrl: string | null;\n /** Job title / role line under the name (null → omitted). */\n title: string | null;\n}\n\n/**\n * One scheduling link on the DIRECTORY wire (`GET /api/meetings`) — the\n * host-DAL whitelist projection consumed by `MeetingSchedulerDirectory` and\n * host pages. Never carries organizer emails/busy-time data.\n */\nexport interface SchedulingLink {\n id: string;\n /** The link's public HubSpot booking URL — escape-hatch target only. */\n link: string;\n /** HubSpot slug path — the row's in-app destination is `<basePath>/<slug>`. */\n slug: string;\n /** Audience group key (slugified audience label; `\"other\"` in scope=all). */\n purpose: string;\n title: string;\n description: string | null;\n kind: 'personal' | 'team';\n /** Display-only minutes projection (booking stays ms end-to-end). */\n durationsMinutes: number[];\n hosts: MeetingHost[];\n /** Earliest bookable slot (epoch ms) from the current-month payload. */\n nextAvailableMs: number | null;\n}\n\nexport interface SchedulingLinksPayload {\n purposes: Array<{ purpose: string; label: string; links: SchedulingLink[] }>;\n fetchedAt: string;\n}\n\n/**\n * One declared question as the host forwards it. `type` is HubSpot's\n * `fieldType` (the control HubSpot draws: `phonenumber`, `booleancheckbox`…)\n * VERBATIM, and `dataType` its `type` (the stored value's kind: `string`,\n * `enumeration`, `phone_number`…). Neither is a closed set: the widget resolves\n * ANY pair to a control through `resolveFormFieldControl`, so a type HubSpot\n * adds tomorrow renders as the nearest control instead of taking the form down.\n */\nexport interface MeetingFormField {\n name: string;\n label: string;\n type: string;\n /** HubSpot's property data type — the fallback when `type` is unknown. Optional: older hosts omit it. */\n dataType?: string;\n required: boolean;\n /** Option VALUES (what is submitted). */\n options?: string[];\n /** Option value → display label, when HubSpot's label differs from the value. */\n optionLabels?: Record<string, string>;\n}\n\n/**\n * HubSpot's consent copy, rendered VERBATIM by the widget (GDPR surface —\n * never edited, never summarized). Responses are keyed by\n * `communicationTypeId`.\n */\nexport interface MeetingLegalConsent {\n processingConsentText: string;\n processingConsentCheckboxLabel: string | null;\n communicationConsentText: string | null;\n communicationConsentCheckboxes: Array<{\n communicationTypeId: string;\n label: string;\n required: boolean;\n }>;\n privacyPolicyText: string | null;\n isLegitimateInterest: boolean;\n}\n\n/**\n * Whitelisted booking result returned by the host proxy — the THIRD HubSpot\n * payload that reaches a browser, so it gets the same whitelist-copy\n * treatment as the two GETs. Nothing organizer-derived.\n */\nexport interface BookingConfirmation {\n meetingId: string;\n title: string;\n startTimeMs: number;\n durationMs: number;\n}\n\n/**\n * Typed domain errors the booking route emits; the widget keys its recovery\n * UI off these. `SLOT_TAKEN` → refetch-and-recover; `TEMPORARILY_UNAVAILABLE`\n * → retry affordance; `MEETING_UNAVAILABLE` → daily ceiling exhausted\n * (escape hatch, not a retry timer); `LINK_GONE` → link deleted upstream;\n * `INVALID_EMAIL` → HubSpot rejected the attendee address (its\n * MeetingsBookingCreatedError.INVALID_EMAIL class — fake/unreachable\n * mailboxes), so the widget tells the visitor to fix the email instead of\n * blaming the slot or their other details.\n */\nexport const MEETING_BOOKING_ERROR_CODES = [\n 'SLOT_TAKEN',\n 'VALIDATION',\n 'INVALID_EMAIL',\n 'LINK_GONE',\n 'TEMPORARILY_UNAVAILABLE',\n 'MEETING_UNAVAILABLE',\n] as const;\n\n/**\n * Derived from the runtime array ABOVE — the array is the single source of\n * truth. The booking hook validates server codes against it at runtime, so a\n * code added only to a hand-written type would be silently coerced to\n * TEMPORARILY_UNAVAILABLE by every deployed widget (exactly how the\n * INVALID_EMAIL rollout mis-rendered on 2026-08-27: type extended, runtime\n * allowlist stale — an array annotation is not exhaustiveness-checked).\n */\nexport type MeetingBookingErrorCode = (typeof MEETING_BOOKING_ERROR_CODES)[number];\n\n// ---------------------------------------------------------------------------\n// Field-type vocabulary — ONE set drives the renderer AND the validator\n// ---------------------------------------------------------------------------\n\n/**\n * THE registry of CONTROLS the native form can draw — one entry per control,\n * and the ONLY place a control is declared. Everything else derives from it:\n * `SupportedFormFieldType` is its key union, `SUPPORTED_FORM_FIELD_TYPES` its\n * keys, and `makeBookingSchema` maps each answer through the entry's\n * validator. The widget's control table (`booking-form.tsx`) is a `Record`\n * over the same key union, so a control added here without a renderer is a\n * COMPILE error, not a silent gap.\n *\n * These are controls, NOT HubSpot's type vocabulary. A HubSpot question\n * reaches a control through `resolveFormFieldControl` (exact `fieldType`, then\n * its data type, then a generic fallback), so the open set of HubSpot types\n * maps onto this closed set of controls and no type is ever \"unsupported\".\n * The one thing that still falls back to HubSpot's own page is a REQUIRED\n * question no control can answer (a file upload) — see `blocksNativeBooking`.\n *\n * Every string control rides the wire as a STRING (HubSpot's book endpoint\n * takes `{ name, value: string }`); `checkbox` is the one boolean. `number` is\n * a decimal literal, `multiselect` HubSpot's `;`-joined enumeration value,\n * `date` an ISO `YYYY-MM-DD`.\n */\nexport interface FormFieldTypeSpec {\n /** The answer's wire shape, which also decides how required/optional wraps it. */\n kind: 'string' | 'boolean';\n /** Whether HubSpot publishes `options` for the type (the pickers). */\n hasOptions: boolean;\n /**\n * The validator for ONE answer. String types EXTEND `base`, which already\n * carries the required-ness (`min(1)` when required) — so \"X is required\" is\n * the first issue reported for an empty answer, ahead of the type's own rule.\n * Boolean types ignore it.\n */\n validator: (field: MeetingFormField, base: z.ZodString) => z.ZodTypeAny;\n /** The control's placeholder, derived from the field (the mock's \"Enter Company Name\"). */\n placeholder?: (field: MeetingFormField) => string;\n}\n\nconst optionValidator: FormFieldTypeSpec['validator'] = (field, base) =>\n base.refine(v => !v || (field.options ?? []).includes(v), {\n message: `Please choose a valid option for ${field.label}`,\n });\n\n/** The wire shape of a number answer — what the form canonicalises TO and the validator checks. */\nexport const DECIMAL_LITERAL_RE = /^-?\\d+(\\.\\d+)?$/;\n\n/** HubSpot's separator for a multi-value enumeration (`a;b;c`). */\nexport const MULTI_VALUE_SEPARATOR = ';';\n\n/** Split a multiselect answer into its values (empty string → none). */\nexport const splitMultiValue = (value: unknown): string[] =>\n typeof value === 'string' && value ? value.split(MULTI_VALUE_SEPARATOR) : [];\n\n/**\n * A phone answer: digits with the punctuation people actually type (`+1 (415)\n * 555-2671`, `020 7946 0958 ext. 12`). Deliberately a SHAPE check, not a\n * numbering-plan check — HubSpot stores the string as typed, and rejecting a\n * valid number we failed to parse is worse than accepting an odd one.\n */\nexport const PHONE_RE = /^\\+?[\\d\\s().\\-/]+(?:\\s*(?:x|ext\\.?)\\s*\\d+)?$/i;\nconst PHONE_MIN_DIGITS = 5;\nconst PHONE_MAX_DIGITS = 20;\nconst phoneDigits = (v: string) => v.replace(/\\D/g, '').length;\n\n/** A date answer as `<input type=\"date\">` emits it. */\nexport const ISO_DATE_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst isRealIsoDate = (v: string) => {\n if (!ISO_DATE_RE.test(v)) return false;\n const d = new Date(`${v}T00:00:00Z`);\n return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === v;\n};\n\nexport const FORM_FIELD_TYPES = {\n text: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) => base.max(1000, { message: `${field.label} is too long` }),\n placeholder: field => `Enter ${field.label}`,\n },\n textarea: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) => base.max(5000, { message: `${field.label} is too long` }),\n placeholder: field => `Enter text${field.required ? '' : ' (optional)'}`,\n },\n number: {\n kind: 'string',\n hasOptions: false,\n // Canonical decimal literal — the control normalises what the browser\n // accepts (`1e3`, `007`) to this before it is validated or sent.\n validator: (field, base) =>\n base\n .max(32, { message: `${field.label} is too long` })\n .regex(DECIMAL_LITERAL_RE, { message: `${field.label} must be a number` }),\n },\n phone: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) =>\n base\n .max(40, { message: `${field.label} is too long` })\n .refine(\n v => !v || (PHONE_RE.test(v) && phoneDigits(v) >= PHONE_MIN_DIGITS && phoneDigits(v) <= PHONE_MAX_DIGITS),\n { message: `Please enter a valid phone number for ${field.label}` },\n ),\n placeholder: field => `Enter ${field.label}`,\n },\n date: {\n kind: 'string',\n hasOptions: false,\n validator: (field, base) => base.refine(v => !v || isRealIsoDate(v), { message: `${field.label} must be a date` }),\n },\n select: { kind: 'string', hasOptions: true, validator: optionValidator },\n radio: { kind: 'string', hasOptions: true, validator: optionValidator },\n multiselect: {\n kind: 'string',\n hasOptions: true,\n validator: (field, base) =>\n base.refine(v => splitMultiValue(v).every(part => (field.options ?? []).includes(part)), {\n message: `Please choose valid options for ${field.label}`,\n }),\n },\n checkbox: { kind: 'boolean', hasOptions: false, validator: () => z.boolean() },\n} as const satisfies Record<string, FormFieldTypeSpec>;\n\nexport type SupportedFormFieldType = keyof typeof FORM_FIELD_TYPES;\n\nexport const SUPPORTED_FORM_FIELD_TYPES = Object.keys(FORM_FIELD_TYPES) as readonly SupportedFormFieldType[];\n\n// ---------------------------------------------------------------------------\n// HubSpot type → control resolution (open set in, closed set out)\n// ---------------------------------------------------------------------------\n\n/**\n * What a HubSpot question resolves to: a drawable control, `display` (a\n * read-only block — `html`, `calculation_*` — never rendered, never\n * submitted) or `unanswerable` (nothing the booking POST can carry, e.g. a\n * file upload).\n */\nexport type FormFieldResolution = SupportedFormFieldType | 'display' | 'unanswerable';\n\n/**\n * HubSpot `fieldType` spellings → resolution. Keys are lower-cased. A spelling\n * missing here is NOT an error: resolution falls through to the data type and\n * then to a generic control. Add an entry only to draw a BETTER control for a\n * type, never to make one \"work\".\n *\n * `checkbox` is absent on purpose: in HubSpot it is \"multiple checkboxes\" (an\n * enumeration with options), while older links sent it for a single yes/no —\n * `resolveFormFieldControl` decides by whether options came with it.\n */\nconst FIELD_TYPE_CONTROLS: Readonly<Record<string, FormFieldResolution>> = {\n text: 'text',\n textarea: 'textarea',\n number: 'number',\n select: 'select',\n radio: 'radio',\n booleancheckbox: 'checkbox',\n phonenumber: 'phone',\n date: 'date',\n html: 'display',\n file: 'unanswerable',\n};\n\n/** HubSpot property data types (`type`) → resolution, used when `fieldType` is unknown. */\nconst DATA_TYPE_CONTROLS: Readonly<Record<string, FormFieldResolution>> = {\n string: 'text',\n number: 'number',\n enumeration: 'select',\n bool: 'checkbox',\n date: 'date',\n datetime: 'date',\n phone_number: 'phone',\n};\n\nconst hasDeclaredOptions = (field: MeetingFormField) => (field.options?.length ?? 0) > 0;\n\n/**\n * THE one mapping from a HubSpot question to what the form does with it.\n * Total: every input yields a resolution, so an unrecognised type can never\n * make a link unbookable — at worst it is a text box. An option control with\n * no options degrades to text (nothing to pick from, but still answerable).\n */\nexport function resolveFormFieldControl(field: MeetingFormField): FormFieldResolution {\n const fieldType = (field.type ?? '').toLowerCase();\n const withOptions = hasDeclaredOptions(field);\n const usable = (r: FormFieldResolution | undefined): FormFieldResolution | undefined => {\n if (!r || r === 'display' || r === 'unanswerable') return r;\n return FORM_FIELD_TYPES[r].hasOptions && !withOptions ? 'text' : r;\n };\n\n if (fieldType === 'checkbox') return withOptions ? 'multiselect' : 'checkbox';\n if (fieldType.startsWith('calculation')) return 'display';\n // A control name resolves to itself, so resolution is IDEMPOTENT: the form\n // resolves once to draw, then hands the resolved fields to the schema\n // factory, which resolves again (the server rebuild resolves raw ones).\n const asControl = Object.prototype.hasOwnProperty.call(FORM_FIELD_TYPES, fieldType)\n ? (fieldType as SupportedFormFieldType)\n : undefined;\n return (\n usable(FIELD_TYPE_CONTROLS[fieldType] ?? asControl) ??\n usable(DATA_TYPE_CONTROLS[(field.dataType ?? '').toLowerCase()]) ??\n (withOptions ? 'select' : 'text')\n );\n}\n\n/**\n * @deprecated Kept for hosts built against the closed six-type registry (they\n * asked this to decide whether to forward `options`). Answers for any HubSpot\n * type through the resolver: true when the type resolves to an option control\n * once options are present. New hosts forward options whenever HubSpot sends\n * them and do not need this.\n */\nexport function formFieldTypeHasOptions(type: string): boolean {\n const control = resolveFormFieldControl({ name: '', label: '', type, required: false, options: ['_'] });\n return control !== 'display' && control !== 'unanswerable' && FORM_FIELD_TYPES[control].hasOptions;\n}\n\n/** @deprecated The option controls (see `formFieldTypeHasOptions`). */\nexport const FORM_FIELD_TYPES_WITH_OPTIONS: readonly SupportedFormFieldType[] = SUPPORTED_FORM_FIELD_TYPES.filter(\n type => FORM_FIELD_TYPES[type].hasOptions,\n);\n\n/**\n * Whether the question's `fieldType` is one the resolver maps EXPLICITLY — as\n * opposed to guessing from its data type or falling back to text. A host logs\n * the false case: the form still works, but a better control may be one table\n * entry away.\n */\nexport function isRecognisedFormFieldType(type: string): boolean {\n const t = (type ?? '').toLowerCase();\n return (\n t === 'checkbox' ||\n t.startsWith('calculation') ||\n Object.prototype.hasOwnProperty.call(FIELD_TYPE_CONTROLS, t) ||\n Object.prototype.hasOwnProperty.call(FORM_FIELD_TYPES, t)\n );\n}\n\n/** A declared question after resolution: `type` is the CONTROL, `hubspotType` what HubSpot sent. */\nexport type SupportedMeetingFormField = MeetingFormField & { type: SupportedFormFieldType; hubspotType?: string };\n\n/** One question → its drawable form, or null when the form does not draw it (display / unanswerable). */\nexport function normalizeFormField(\n field: MeetingFormField & { hubspotType?: string },\n): SupportedMeetingFormField | null {\n const control = resolveFormFieldControl(field);\n if (control === 'display' || control === 'unanswerable') return null;\n // A control that takes no options drops them (a `booleancheckbox` arrives\n // with HubSpot's `true`/`false` pair), which also keeps a re-resolution of\n // the result on the same control.\n const normalized: SupportedMeetingFormField = {\n ...field,\n type: control,\n hubspotType: field.hubspotType ?? field.type,\n };\n if (!FORM_FIELD_TYPES[control].hasOptions) {\n delete normalized.options;\n delete normalized.optionLabels;\n }\n return normalized;\n}\n\n/** The questions the native form draws and validates, in declared order. */\nexport function normalizeFormFields(fields: readonly MeetingFormField[]): SupportedMeetingFormField[] {\n return fields.flatMap(f => normalizeFormField(f) ?? []);\n}\n\n/** Whether the native form draws this question (false for display blocks and unanswerable questions). */\nexport function isSupportedFormField(field: MeetingFormField): boolean {\n return normalizeFormField(field) !== null;\n}\n\n/**\n * The ONLY reason left to hand a link to HubSpot's own page: a REQUIRED\n * question no control can answer. An optional one is simply left out — the\n * visitor can still book, and HubSpot does not require it.\n */\nexport function blocksNativeBooking(field: MeetingFormField): boolean {\n return field.required && resolveFormFieldControl(field) === 'unanswerable';\n}\n\n/**\n * The scheduler's fixed identity fields. HubSpot's book endpoint takes them\n * TOP-LEVEL (`firstName`, `lastName`, `email`), its own booking page hardcodes\n * them, and the link's `formFields` never lists them — so they are data HERE,\n * rendered by the widget through the SAME control path as every declared\n * question, rather than three hand-written blocks. `inputType`/`autoComplete`\n * are the browser hints a text control takes; the wire and the validator do\n * not see them.\n */\nexport interface BuiltInBookingField extends MeetingFormField {\n type: 'text';\n required: true;\n inputType?: 'email';\n autoComplete: string;\n /** Own placeholder. Omitted, the control derives one from the label (the registry's rule). */\n placeholder?: string;\n /** The wire's own required/format message — kept verbatim from the schema it replaced. */\n requiredMessage: string;\n}\n\nexport const BUILT_IN_BOOKING_FIELDS = [\n {\n name: 'firstName',\n label: 'First Name',\n type: 'text',\n required: true,\n autoComplete: 'given-name',\n requiredMessage: 'First name is required',\n },\n {\n name: 'lastName',\n label: 'Last Name',\n type: 'text',\n required: true,\n autoComplete: 'family-name',\n requiredMessage: 'Last name is required',\n },\n {\n name: 'email',\n label: 'Email',\n type: 'text',\n required: true,\n inputType: 'email',\n autoComplete: 'email',\n placeholder: 'username@mail.com',\n requiredMessage: 'Please enter a valid email address',\n },\n] as const satisfies readonly BuiltInBookingField[];\n\n/** `'firstName' | 'lastName' | 'email'` — derived from the array, never restated. */\nexport type BuiltInBookingFieldName = (typeof BUILT_IN_BOOKING_FIELDS)[number]['name'];\n\n/** The registry entry for a supported type, widened to the spec so optional\n * members (`placeholder`) are readable without narrowing on the union. */\nexport function fieldTypeSpec(type: SupportedFormFieldType): FormFieldTypeSpec {\n return FORM_FIELD_TYPES[type];\n}\n\n/** The wire validator for one identity field, from ITS declaration above. */\nfunction identityValidator(name: BuiltInBookingFieldName) {\n const field: BuiltInBookingField | undefined = BUILT_IN_BOOKING_FIELDS.find(f => f.name === name);\n if (!field) throw new Error(`Unknown built-in booking field: ${name}`);\n // The field's own message FIRST: an empty value must fail `min`/`email` with\n // the wire's copy, not a length cap it cannot have hit.\n const tooLong = { message: `${field.label} is too long` };\n return field.inputType === 'email'\n ? z.string().email({ message: field.requiredMessage }).max(255, tooLong)\n : z.string().min(1, { message: field.requiredMessage }).max(255, tooLong);\n}\n\n// ---------------------------------------------------------------------------\n// Validators (single home — client widget and server rebuild both use these)\n// ---------------------------------------------------------------------------\n\nconst IANA_TZ_RE = /^(?:UTC|[A-Za-z_]+(?:\\/[A-Za-z0-9_+-]+)+)$/;\n\n/**\n * IANA timezone check. Shape prefilter, then the authoritative resolution\n * test: `Intl.DateTimeFormat` throws on unknown zones. NOT\n * `Intl.supportedValuesOf('timeZone')` — that list excludes `'UTC'` itself\n * (verified in Node), which is a legitimate booking zone. Reject, never coerce.\n */\nexport function isValidIanaTimezone(tz: string): boolean {\n if (!IANA_TZ_RE.test(tz)) return false;\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: tz });\n return true;\n } catch {\n return false;\n }\n}\n\n/** BCP-47 locale shape check via `Intl.getCanonicalLocales`. Reject, never coerce. */\nexport function isValidBcp47Locale(locale: string): boolean {\n try {\n return Intl.getCanonicalLocales(locale).length > 0;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Schema factory\n// ---------------------------------------------------------------------------\n\n/**\n * Build the booking-form schema for ONE link's declared questions + consent.\n *\n * A factory (not a static schema) because per-link required questions cannot\n * be expressed statically. The widget builds it from the availability payload\n * it rendered; the server REBUILDS it from the link's own fetched metadata —\n * required-consent enforcement flows from this rebuild, not a parallel check.\n *\n * Deliberately NOT `.strict()`: the humanity-signal fields\n * (`HUMANITY_SIGNAL_KEYS` from `utils/humanity-signals`) ride alongside in\n * the same POST body (read raw by the host's bot gate BEFORE parsing) and are\n * stripped server-side before anything reaches HubSpot. zod's default\n * unknown-key stripping means the parsed output never contains them.\n *\n * `timezone`/`locale` are POST-only presentation fields (the invite renders\n * in the visitor's local time) — this schema is the ONLY place a\n * client-supplied zone is accepted; the availability path is UTC-pinned.\n */\nfunction buildBookingSchema<TStart extends z.ZodTypeAny, TDuration extends z.ZodTypeAny, TZone extends z.ZodTypeAny>(\n formFields: MeetingFormField[],\n legalConsent: MeetingLegalConsent | null,\n // Passed IN rather than switched on a boolean: a `deferSlot: boolean`\n // parameter cannot be narrowed at type level, so every ternary inside would\n // infer a union and widen `startTimeMs`/`durationMs` to `number | null |\n // undefined` for BOTH schemas — including the server's wire type.\n slot: { startTimeMs: TStart; durationMs: TDuration; timezone: TZone },\n) {\n const answers: Record<string, z.ZodTypeAny> = {};\n // Resolved HERE, not by the caller: the server rebuild passes the link's raw\n // questions, so client and server resolve every type through the same rule.\n // Display blocks and unanswerable optional questions carry no answer.\n const drawn = normalizeFormFields(formFields);\n for (const field of drawn) {\n const spec: FormFieldTypeSpec = FORM_FIELD_TYPES[field.type];\n // required/optional wrapping follows the answer's wire KIND, not its type:\n // a boolean is true-or-absent, a string is non-empty-or-empty. For strings\n // the required check is the FIRST rule on the chain, so an empty answer\n // reports \"is required\" rather than the type's own message.\n let validator: z.ZodTypeAny;\n if (spec.kind === 'boolean') {\n validator = field.required ? z.literal(true, { message: `${field.label} is required` }) : z.boolean().optional();\n } else {\n const base = field.required ? z.string().min(1, { message: `${field.label} is required` }) : z.string();\n validator = spec.validator(field, base);\n if (!field.required) validator = validator.optional().or(z.literal(''));\n }\n answers[field.name] = validator;\n }\n\n const requiredConsentIds = (legalConsent?.communicationConsentCheckboxes ?? [])\n .filter(c => c.required)\n .map(c => c.communicationTypeId);\n\n // A required answer cannot be enforced by an `.optional()` parent object —\n // omitting the `formFields` key entirely would skip every per-question\n // rule. When the link declares at least one required supported question,\n // the object itself is required.\n const hasRequiredAnswers = drawn.some(f => f.required);\n const answersObject = z.object(answers);\n\n return (\n z\n .object({\n meetingId: z.string().min(1),\n startTimeMs: slot.startTimeMs,\n durationMs: slot.durationMs,\n // The identity trio's rules come from BUILT_IN_BOOKING_FIELDS — the\n // keys stay literal so the payload type keeps its named properties.\n firstName: identityValidator('firstName'),\n lastName: identityValidator('lastName'),\n email: identityValidator('email'),\n timezone: slot.timezone,\n locale: z.string().refine(isValidBcp47Locale, { message: 'Invalid locale' }).optional(),\n // Plain `.optional()` (no `.default()`) so zod's input and output types\n // match — react-hook-form's zodResolver needs them identical, and the\n // server handles `undefined` explicitly anyway. REQUIRED when the link\n // declares required questions (see `hasRequiredAnswers` above).\n formFields: hasRequiredAnswers ? answersObject : answersObject.optional(),\n legalConsentResponses: z\n .array(z.object({ communicationTypeId: z.string(), consented: z.boolean() }))\n .optional(),\n })\n // Object-level rule: `.refine` on an `.optional()` field is skipped when\n // the field is absent — required consents must reject even on a payload\n // that omits the array entirely.\n .superRefine((data, ctx) => {\n const responses = data.legalConsentResponses ?? [];\n const ok = requiredConsentIds.every(id =>\n responses.some(r => r.communicationTypeId === id && r.consented === true),\n );\n if (!ok) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['legalConsentResponses'],\n message: 'Required consent checkboxes must be accepted',\n });\n }\n })\n );\n}\n\n/** The slot's wire validators — ONE declaration for the strict and the deferred schema. */\nconst SLOT_INSTANT = z.number().int().positive();\nconst TIMEZONE = z.string().refine(isValidIanaTimezone, { message: 'Invalid timezone' });\n\n/**\n * The STRICT schema — the wire contract. The server rebuilds it from the link's\n * own fetched metadata and validates every booking against it; a slot and a\n * duration are not optional on anything that reaches HubSpot.\n */\nexport function makeBookingSchema(formFields: MeetingFormField[], legalConsent: MeetingLegalConsent | null) {\n return buildBookingSchema(formFields, legalConsent, {\n startTimeMs: SLOT_INSTANT,\n durationMs: SLOT_INSTANT,\n timezone: TIMEZONE,\n });\n}\n\n/**\n * The DEFERRED schema — same fields, with the slot/duration/timezone triple\n * relaxed. `flow=\"details-first\"` collects answers BEFORE a slot exists, so the\n * form validates against this and the parent re-attaches the authoritative\n * values at POST time.\n *\n * Two named exports over one private builder rather than an overload or an\n * options flag: `MeetingBookingPayload` is `z.infer<ReturnType<...>>`, and\n * either of those alternatives would widen `startTimeMs`/`durationMs` to\n * `number | undefined` for EVERY consumer — including the server's\n * `durationsMs.includes(data.durationMs)`.\n */\nexport function makeDeferredBookingSchema(formFields: MeetingFormField[], legalConsent: MeetingLegalConsent | null) {\n // `.nullish()`, not `.optional()`: the widget passes these straight into\n // `defaultValues`, and details-first has `timezone === null` on the server\n // render and the first client render (zone resolution is post-hydration).\n // Accepting only `undefined` would reject a field with no rendered control\n // and no FieldWrapper — the submit button silently dead, type-check clean.\n return buildBookingSchema(formFields, legalConsent, {\n startTimeMs: SLOT_INSTANT.nullish(),\n durationMs: SLOT_INSTANT.nullish(),\n timezone: TIMEZONE.nullish(),\n });\n}\n\n/**\n * Email domains a host's form does not accept, with the message it shows when\n * one is typed. The HOST owns the list (its own personal/free-provider rule)\n * and re-checks on its own server — this is the answer at the point of typing,\n * never the gate.\n */\nexport interface DeniedEmailDomains {\n domains: readonly string[];\n message: string;\n}\n\n/** The address's domain, lower-cased, matched WHOLE against the list — a\n * subdomain is a different domain, which is how a host's own list reads it. */\nexport function emailDomainDenied(email: unknown, rule: DeniedEmailDomains): boolean {\n if (typeof email !== 'string') return false;\n const domain = email.split('@')[1]?.trim().toLowerCase();\n if (domain === undefined || domain === '') return false;\n return rule.domains.includes(domain);\n}\n\n/** `schema` refined to reject those domains ON the email field, so the message\n * lands under the control the visitor typed in rather than as a form error. */\nexport function withDeniedEmailDomains(\n schema: ReturnType<typeof makeDeferredBookingSchema>,\n rule: DeniedEmailDomains | undefined,\n) {\n if (!rule) return schema;\n return schema.superRefine((values, ctx) => {\n if (emailDomainDenied(values.email, rule)) {\n ctx.addIssue({ code: 'custom', path: ['email'], message: rule.message });\n }\n });\n}\n\n/** The wire payload. Pinned to the STRICT builder — see above. */\nexport type MeetingBookingPayload = z.infer<ReturnType<typeof makeBookingSchema>>;\n\n/**\n * The form's value type, used as the `useForm` generic in BOTH flows: a relaxed\n * resolver is not assignable to `Resolver<MeetingBookingPayload>`, and mixing\n * the two survives only on TS's bivariant method-parameter check.\n */\nexport type BookingFormValues = z.infer<ReturnType<typeof makeDeferredBookingSchema>>;\n"]}
|