import { z } from 'zod'; /** * Meeting-booking wire contracts + validation factory. * * SERVER-SAFE tsup entry (no "use client" banner) with its OWN per-file * subpath (`./schemas/meeting-booking-schema`) — same pattern and reasons as * `schemas/contact-schema`: used by BOTH the lib's `` * (client-side validation) AND the host's server-side booking route, which * REBUILDS the schema from the link's own fetched metadata and never trusts a * client-shaped instance. zod is an optional peer quarantined to per-subpath * verticals — do NOT re-export this module through any broad barrel. * * Every cross-boundary type for the scheduling feature lives HERE (the lib * owns the contract; hosts import this subpath directly, type-only where * possible). */ /** * Sanitized availability payload served by the host proxy * (`GET {apiBaseUrl}/api/meetings/availability?meeting=&monthOffset=`). * * NO timezone field on purpose: slot starts are absolute epoch-ms instants * (verified timezone-independent against the live API), upstream fetches are * UTC-pinned, and ALL zone rendering happens client-side. Slots must be * whitelist-copied from HubSpot's `linkAvailability` ONLY — never derived * from busy-time data. */ export interface MeetingAvailability { meetingId: string; monthOffset: number; hasMore: boolean; /** Offered durations in ms — HubSpot's native unit for the booking POST. */ durationsMs: number[]; /** Bookable slot start times (epoch ms), keyed by duration in ms. */ slotsByDurationMs: Record; formFields: MeetingFormField[]; /** Verbatim whitelist-copy of HubSpot's `legalConsentOptions` when consent is enabled; null when disabled. */ legalConsent: MeetingLegalConsent | null; /** * Who the visitor is meeting — whitelisted DISPLAY projection the host DAL * builds from its own people data (e.g. a profiles table matched * server-side). NEVER carries emails or busy-time data; optional so * existing hosts stay wire-compatible. */ hosts?: MeetingHost[]; } /** Display-only host identity for the scheduler's context panel. */ export interface MeetingHost { name: string; avatarUrl: string | null; /** Job title / role line under the name (null → omitted). */ title: string | null; } /** * One scheduling link on the DIRECTORY wire (`GET /api/meetings`) — the * host-DAL whitelist projection consumed by `MeetingSchedulerDirectory` and * host pages. Never carries organizer emails/busy-time data. */ export interface SchedulingLink { id: string; /** The link's public HubSpot booking URL — escape-hatch target only. */ link: string; /** HubSpot slug path — the row's in-app destination is `/`. */ slug: string; /** Audience group key (slugified audience label; `"other"` in scope=all). */ purpose: string; title: string; description: string | null; kind: 'personal' | 'team'; /** Display-only minutes projection (booking stays ms end-to-end). */ durationsMinutes: number[]; hosts: MeetingHost[]; /** Earliest bookable slot (epoch ms) from the current-month payload. */ nextAvailableMs: number | null; } export interface SchedulingLinksPayload { purposes: Array<{ purpose: string; label: string; links: SchedulingLink[]; }>; fetchedAt: string; } export interface MeetingFormField { name: string; label: string; type: string; required: boolean; options?: string[]; } /** * HubSpot's consent copy, rendered VERBATIM by the widget (GDPR surface — * never edited, never summarized). Responses are keyed by * `communicationTypeId`. */ export interface MeetingLegalConsent { processingConsentText: string; processingConsentCheckboxLabel: string | null; communicationConsentText: string | null; communicationConsentCheckboxes: Array<{ communicationTypeId: string; label: string; required: boolean; }>; privacyPolicyText: string | null; isLegitimateInterest: boolean; } /** * Whitelisted booking result returned by the host proxy — the THIRD HubSpot * payload that reaches a browser, so it gets the same whitelist-copy * treatment as the two GETs. Nothing organizer-derived. */ export interface BookingConfirmation { meetingId: string; title: string; startTimeMs: number; durationMs: number; } /** * Typed domain errors the booking route emits; the widget keys its recovery * UI off these. `SLOT_TAKEN` → refetch-and-recover; `TEMPORARILY_UNAVAILABLE` * → retry affordance; `MEETING_UNAVAILABLE` → daily ceiling exhausted * (escape hatch, not a retry timer); `LINK_GONE` → link deleted upstream; * `INVALID_EMAIL` → HubSpot rejected the attendee address (its * MeetingsBookingCreatedError.INVALID_EMAIL class — fake/unreachable * mailboxes), so the widget tells the visitor to fix the email instead of * blaming the slot or their other details. */ export declare const MEETING_BOOKING_ERROR_CODES: readonly ["SLOT_TAKEN", "VALIDATION", "INVALID_EMAIL", "LINK_GONE", "TEMPORARILY_UNAVAILABLE", "MEETING_UNAVAILABLE"]; /** * Derived from the runtime array ABOVE — the array is the single source of * truth. The booking hook validates server codes against it at runtime, so a * code added only to a hand-written type would be silently coerced to * TEMPORARILY_UNAVAILABLE by every deployed widget (exactly how the * INVALID_EMAIL rollout mis-rendered on 2026-08-27: type extended, runtime * allowlist stale — an array annotation is not exhaustiveness-checked). */ export type MeetingBookingErrorCode = (typeof MEETING_BOOKING_ERROR_CODES)[number]; /** * THE registry of HubSpot question types the native form supports — one entry * per `fieldType`, and the ONLY place a type is declared. Everything else * derives from it: `SupportedFormFieldType` is its key union, * `SUPPORTED_FORM_FIELD_TYPES` its keys, `FORM_FIELD_TYPES_WITH_OPTIONS` the * entries that carry `options`, and `makeBookingSchema` maps each answer * through the entry's validator. The widget's control table * (`booking-form.tsx`) is a `Record` over the same key union, so a type added * here without a control is a COMPILE error, not a silent gap. * * Fail-closed: a `fieldType` with no entry makes the link "not natively * bookable" and the card falls back to the HubSpot escape hatch. * * Every string type rides the wire as a STRING (HubSpot's book endpoint takes * `{ name, value: string }`); `checkbox` is the one boolean. `number` is a * Number property validated as a decimal literal — what `` * emits and what the property stores. */ export interface FormFieldTypeSpec { /** The answer's wire shape, which also decides how required/optional wraps it. */ kind: 'string' | 'boolean'; /** Whether HubSpot publishes `options` for the type (the pickers). */ hasOptions: boolean; /** * The validator for ONE answer. String types EXTEND `base`, which already * carries the required-ness (`min(1)` when required) — so "X is required" is * the first issue reported for an empty answer, ahead of the type's own rule. * Boolean types ignore it. */ validator: (field: MeetingFormField, base: z.ZodString) => z.ZodTypeAny; /** The control's placeholder, derived from the field (the mock's "Enter Company Name"). */ placeholder?: (field: MeetingFormField) => string; } /** The wire shape of a number answer — what the form canonicalises TO and the validator checks. */ export declare const DECIMAL_LITERAL_RE: RegExp; export declare const FORM_FIELD_TYPES: { readonly text: { readonly kind: "string"; readonly hasOptions: false; readonly validator: (field: MeetingFormField, base: z.ZodString) => z.ZodString; readonly placeholder: (field: MeetingFormField) => string; }; readonly textarea: { readonly kind: "string"; readonly hasOptions: false; readonly validator: (field: MeetingFormField, base: z.ZodString) => z.ZodString; readonly placeholder: (field: MeetingFormField) => string; }; readonly number: { readonly kind: "string"; readonly hasOptions: false; readonly validator: (field: MeetingFormField, base: z.ZodString) => z.ZodString; }; readonly select: { readonly kind: "string"; readonly hasOptions: true; readonly validator: (field: MeetingFormField, base: z.ZodString) => z.ZodTypeAny; }; readonly radio: { readonly kind: "string"; readonly hasOptions: true; readonly validator: (field: MeetingFormField, base: z.ZodString) => z.ZodTypeAny; }; readonly checkbox: { readonly kind: "boolean"; readonly hasOptions: false; readonly validator: () => z.ZodBoolean; }; }; export type SupportedFormFieldType = keyof typeof FORM_FIELD_TYPES; export declare const SUPPORTED_FORM_FIELD_TYPES: readonly SupportedFormFieldType[]; /** The types whose `options` the host must forward (select, radio). */ export declare const FORM_FIELD_TYPES_WITH_OPTIONS: readonly SupportedFormFieldType[]; /** Whether a raw HubSpot field type is one whose answers come from declared options. */ export declare function formFieldTypeHasOptions(type: string): boolean; export declare function isSupportedFormField(field: MeetingFormField): field is SupportedMeetingFormField; /** A declared question whose `type` is in the registry. */ export type SupportedMeetingFormField = MeetingFormField & { type: SupportedFormFieldType; }; /** * The scheduler's fixed identity fields. HubSpot's book endpoint takes them * TOP-LEVEL (`firstName`, `lastName`, `email`), its own booking page hardcodes * them, and the link's `formFields` never lists them — so they are data HERE, * rendered by the widget through the SAME control path as every declared * question, rather than three hand-written blocks. `inputType`/`autoComplete` * are the browser hints a text control takes; the wire and the validator do * not see them. */ export interface BuiltInBookingField extends MeetingFormField { type: 'text'; required: true; inputType?: 'email'; autoComplete: string; /** Own placeholder. Omitted, the control derives one from the label (the registry's rule). */ placeholder?: string; /** The wire's own required/format message — kept verbatim from the schema it replaced. */ requiredMessage: string; } export declare const BUILT_IN_BOOKING_FIELDS: readonly [{ readonly name: "firstName"; readonly label: "First Name"; readonly type: "text"; readonly required: true; readonly autoComplete: "given-name"; readonly requiredMessage: "First name is required"; }, { readonly name: "lastName"; readonly label: "Last Name"; readonly type: "text"; readonly required: true; readonly autoComplete: "family-name"; readonly requiredMessage: "Last name is required"; }, { readonly name: "email"; readonly label: "Email"; readonly type: "text"; readonly required: true; readonly inputType: "email"; readonly autoComplete: "email"; readonly placeholder: "username@mail.com"; readonly requiredMessage: "Please enter a valid email address"; }]; /** `'firstName' | 'lastName' | 'email'` — derived from the array, never restated. */ export type BuiltInBookingFieldName = (typeof BUILT_IN_BOOKING_FIELDS)[number]['name']; /** The registry entry for a supported type, widened to the spec so optional * members (`placeholder`) are readable without narrowing on the union. */ export declare function fieldTypeSpec(type: SupportedFormFieldType): FormFieldTypeSpec; /** * IANA timezone check. Shape prefilter, then the authoritative resolution * test: `Intl.DateTimeFormat` throws on unknown zones. NOT * `Intl.supportedValuesOf('timeZone')` — that list excludes `'UTC'` itself * (verified in Node), which is a legitimate booking zone. Reject, never coerce. */ export declare function isValidIanaTimezone(tz: string): boolean; /** BCP-47 locale shape check via `Intl.getCanonicalLocales`. Reject, never coerce. */ export declare function isValidBcp47Locale(locale: string): boolean; /** * The STRICT schema — the wire contract. The server rebuilds it from the link's * own fetched metadata and validates every booking against it; a slot and a * duration are not optional on anything that reaches HubSpot. */ export declare function makeBookingSchema(formFields: MeetingFormField[], legalConsent: MeetingLegalConsent | null): z.ZodObject<{ meetingId: z.ZodString; startTimeMs: z.ZodNumber; durationMs: z.ZodNumber; firstName: z.ZodString; lastName: z.ZodString; email: z.ZodString; timezone: z.ZodString; locale: z.ZodOptional; formFields: z.ZodObject<{ [x: string]: z.ZodTypeAny>; }, z.core.$strip> | z.ZodOptional>; }, z.core.$strip>>; legalConsentResponses: z.ZodOptional>>; }, z.core.$strip>; /** * The DEFERRED schema — same fields, with the slot/duration/timezone triple * relaxed. `flow="details-first"` collects answers BEFORE a slot exists, so the * form validates against this and the parent re-attaches the authoritative * values at POST time. * * Two named exports over one private builder rather than an overload or an * options flag: `MeetingBookingPayload` is `z.infer>`, and * either of those alternatives would widen `startTimeMs`/`durationMs` to * `number | undefined` for EVERY consumer — including the server's * `durationsMs.includes(data.durationMs)`. */ export declare function makeDeferredBookingSchema(formFields: MeetingFormField[], legalConsent: MeetingLegalConsent | null): z.ZodObject<{ meetingId: z.ZodString; startTimeMs: z.ZodOptional>; durationMs: z.ZodOptional>; firstName: z.ZodString; lastName: z.ZodString; email: z.ZodString; timezone: z.ZodOptional>; locale: z.ZodOptional; formFields: z.ZodObject<{ [x: string]: z.ZodTypeAny>; }, z.core.$strip> | z.ZodOptional>; }, z.core.$strip>>; legalConsentResponses: z.ZodOptional>>; }, z.core.$strip>; /** The wire payload. Pinned to the STRICT builder — see above. */ export type MeetingBookingPayload = z.infer>; /** * The form's value type, used as the `useForm` generic in BOTH flows: a relaxed * resolver is not assignable to `Resolver`, and mixing * the two survives only on TS's bivariant method-parameter check. */ export type BookingFormValues = z.infer>; //# sourceMappingURL=meeting-booking-schema.d.ts.map