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. */ export type MeetingBookingErrorCode = 'SLOT_TAKEN' | 'VALIDATION' | 'LINK_GONE' | 'TEMPORARILY_UNAVAILABLE' | 'MEETING_UNAVAILABLE'; /** * HubSpot custom-question types the native form supports. The widget's * renderer switches over THIS set and `makeBookingSchema` maps over THIS set; * fail-closed = a field whose `type` is not in the set (the widget then * renders the "Open in HubSpot" escape hatch for that link instead of a * half-working native form). Exact upstream type strings are pinned against * the rollout fixture link — extend here (renderer + validator move together). */ export declare const SUPPORTED_FORM_FIELD_TYPES: readonly ["text", "textarea", "select", "radio", "checkbox"]; export type SupportedFormFieldType = (typeof SUPPORTED_FORM_FIELD_TYPES)[number]; export declare function isSupportedFormField(field: MeetingFormField): boolean; /** * 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; /** * Build the booking-form schema for ONE link's declared questions + consent. * * A factory (not a static schema) because per-link required questions cannot * be expressed statically. The widget builds it from the availability payload * it rendered; the server REBUILDS it from the link's own fetched metadata — * required-consent enforcement flows from this rebuild, not a parallel check. * * Deliberately NOT `.strict()`: the humanity-signal fields * (`HUMANITY_SIGNAL_KEYS` from `utils/humanity-signals`) ride alongside in * the same POST body (read raw by the host's bot gate BEFORE parsing) and are * stripped server-side before anything reaches HubSpot. zod's default * unknown-key stripping means the parsed output never contains them. * * `timezone`/`locale` are POST-only presentation fields (the invite renders * in the visitor's local time) — this schema is the ONLY place a * client-supplied zone is accepted; the availability path is UTC-pinned. */ 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.ZodType>; }, z.core.$strip> | z.ZodOptional>; }, z.core.$strip>>; legalConsentResponses: z.ZodOptional>>; }, z.core.$strip>; export type MeetingBookingPayload = z.infer>; //# sourceMappingURL=meeting-booking-schema.d.ts.map