{"version":3,"sources":["../../src/schemas/meeting-booking-schema.ts"],"names":["z"],"mappings":";;;;;AAuJO,IAAM,6BAA6B,CAAC,MAAA,EAAQ,UAAA,EAAY,QAAA,EAAU,SAAS,UAAU;AAGrF,SAAS,qBAAqB,KAAA,EAAkC;AACrE,EAAA,OAAQ,0BAAA,CAAiD,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA;AAC9E;AAMA,IAAM,UAAA,GAAa,6CAAA;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;AAwBO,SAAS,iBAAA,CACd,YACA,YAAA,EACA;AACA,EAAA,MAAM,UAAwC,EAAC;AAC/C,EAAA,KAAA,MAAW,SAAS,UAAA,EAAY;AAC9B,IAAA,IAAI,CAAC,oBAAA,CAAqB,KAAK,CAAA,EAAG;AAClC,IAAA,IAAI,SAAA;AACJ,IAAA,QAAQ,MAAM,IAAA;AAAgC,MAC5C,KAAK,UAAA;AACH,QAAA,SAAA,GAAYA,MAAE,OAAA,EAAQ;AACtB,QAAA;AAAA,MACF,KAAK,QAAA;AAAA,MACL,KAAK,OAAA;AACH,QAAA,SAAA,GAAYA,KAAA,CACT,MAAA,EAAO,CACP,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,CAAA,IAAA,CAAM,KAAA,CAAM,OAAA,IAAW,EAAC,EAAG,QAAA,CAAS,CAAC,CAAA,EAAG;AAAA,UACtD,OAAA,EAAS,CAAA,iCAAA,EAAoC,KAAA,CAAM,KAAK,CAAA;AAAA,SACzD,CAAA;AACH,QAAA;AAAA,MACF,KAAK,UAAA;AACH,QAAA,SAAA,GAAYA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,CAAI,GAAA,EAAM,EAAE,OAAA,EAAS,CAAA,EAAG,KAAA,CAAM,KAAK,CAAA,YAAA,CAAA,EAAgB,CAAA;AAC1E,QAAA;AAAA,MACF,KAAK,MAAA;AAAA,MACL;AACE,QAAA,SAAA,GAAYA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,CAAI,GAAA,EAAM,EAAE,OAAA,EAAS,CAAA,EAAG,KAAA,CAAM,KAAK,CAAA,YAAA,CAAA,EAAgB,CAAA;AAC1E,QAAA;AAAA;AAEJ,IAAA,IAAI,MAAM,QAAA,EAAU;AAClB,MAAA,SAAA,GACE,KAAA,CAAM,SAAS,UAAA,GACXA,KAAA,CAAE,QAAQ,IAAA,EAAM,EAAE,OAAA,EAAS,CAAA,EAAG,KAAA,CAAM,KAAK,gBAAgB,CAAA,GACxD,SAAA,CAA0B,GAAA,CAAI,CAAA,EAAG,EAAE,SAAS,CAAA,EAAG,KAAA,CAAM,KAAK,CAAA,YAAA,CAAA,EAAgB,CAAA;AAAA,IACnF,CAAA,MAAA,IAAW,KAAA,CAAM,IAAA,KAAS,UAAA,EAAY;AACpC,MAAA,SAAA,GAAa,UAA0B,QAAA,EAAS,CAAE,GAAGA,KAAA,CAAE,OAAA,CAAQ,EAAE,CAAC,CAAA;AAAA,IACpE,CAAA,MAAO;AACL,MAAA,SAAA,GAAYA,KAAA,CAAE,OAAA,EAAQ,CAAE,QAAA,EAAS;AAAA,IACnC;AACA,IAAA,OAAA,CAAQ,KAAA,CAAM,IAAI,CAAA,GAAI,SAAA;AAAA,EACxB;AAEA,EAAA,MAAM,kBAAA,GAAA,CAAsB,YAAA,EAAc,8BAAA,IAAkC,IACzE,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,QAAQ,CAAA,CACxB,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,mBAAmB,CAAA;AAMnC,EAAA,MAAM,kBAAA,GAAqB,WAAW,IAAA,CAAK,CAAC,MAAM,oBAAA,CAAqB,CAAC,CAAA,IAAK,CAAA,CAAE,QAAQ,CAAA;AACvF,EAAA,MAAM,aAAA,GAAgBA,KAAA,CAAE,MAAA,CAAO,OAAO,CAAA;AAEtC,EAAA,OAAOA,MACJ,MAAA,CAAO;AAAA,IACN,SAAA,EAAWA,KAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,IAC3B,aAAaA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,GAAM,QAAA,EAAS;AAAA,IACvC,YAAYA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,GAAM,QAAA,EAAS;AAAA,IACtC,SAAA,EAAWA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,CAAI,CAAA,EAAG,EAAE,OAAA,EAAS,wBAAA,EAA0B,CAAA,CAAE,GAAA,CAAI,GAAG,CAAA;AAAA,IAC3E,QAAA,EAAUA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,CAAI,CAAA,EAAG,EAAE,OAAA,EAAS,uBAAA,EAAyB,CAAA,CAAE,GAAA,CAAI,GAAG,CAAA;AAAA,IACzE,KAAA,EAAOA,KAAA,CAAE,MAAA,EAAO,CAAE,KAAA,CAAM,EAAE,OAAA,EAAS,oCAAA,EAAsC,CAAA,CAAE,GAAA,CAAI,GAAG,CAAA;AAAA,IAClF,QAAA,EAAUA,MAAE,MAAA,EAAO,CAAE,OAAO,mBAAA,EAAqB,EAAE,OAAA,EAAS,kBAAA,EAAoB,CAAA;AAAA,IAChF,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,CAAC,EAAA,KACnC,SAAA,CAAU,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,mBAAA,KAAwB,EAAA,IAAM,CAAA,CAAE,SAAA,KAAc,IAAI;AAAA,KAC5E;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;AACL","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\nexport interface MeetingFormField {\n  name: string\n  label: string\n  type: string\n  required: boolean\n  options?: 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 */\nexport type MeetingBookingErrorCode =\n  | 'SLOT_TAKEN'\n  | 'VALIDATION'\n  | 'LINK_GONE'\n  | 'TEMPORARILY_UNAVAILABLE'\n  | 'MEETING_UNAVAILABLE'\n\n// ---------------------------------------------------------------------------\n// Field-type vocabulary — ONE set drives the renderer AND the validator\n// ---------------------------------------------------------------------------\n\n/**\n * HubSpot custom-question types the native form supports. The widget's\n * renderer switches over THIS set and `makeBookingSchema` maps over THIS set;\n * fail-closed = a field whose `type` is not in the set (the widget then\n * renders the \"Open in HubSpot\" escape hatch for that link instead of a\n * half-working native form). Exact upstream type strings are pinned against\n * the rollout fixture link — extend here (renderer + validator move together).\n */\nexport const SUPPORTED_FORM_FIELD_TYPES = ['text', 'textarea', 'select', 'radio', 'checkbox'] as const\nexport type SupportedFormFieldType = (typeof SUPPORTED_FORM_FIELD_TYPES)[number]\n\nexport function isSupportedFormField(field: MeetingFormField): boolean {\n  return (SUPPORTED_FORM_FIELD_TYPES as readonly string[]).includes(field.type)\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 */\nexport function makeBookingSchema(\n  formFields: MeetingFormField[],\n  legalConsent: MeetingLegalConsent | null,\n) {\n  const answers: Record<string, z.ZodTypeAny> = {}\n  for (const field of formFields) {\n    if (!isSupportedFormField(field)) continue // unsupported types are fail-closed at render time\n    let validator: z.ZodTypeAny\n    switch (field.type as SupportedFormFieldType) {\n      case 'checkbox':\n        validator = z.boolean()\n        break\n      case 'select':\n      case 'radio':\n        validator = z\n          .string()\n          .refine((v) => !v || (field.options ?? []).includes(v), {\n            message: `Please choose a valid option for ${field.label}`,\n          })\n        break\n      case 'textarea':\n        validator = z.string().max(5000, { message: `${field.label} is too long` })\n        break\n      case 'text':\n      default:\n        validator = z.string().max(1000, { message: `${field.label} is too long` })\n        break\n    }\n    if (field.required) {\n      validator =\n        field.type === 'checkbox'\n          ? z.literal(true, { message: `${field.label} is required` })\n          : (validator as z.ZodString).min(1, { message: `${field.label} is required` })\n    } else if (field.type !== 'checkbox') {\n      validator = (validator as z.ZodString).optional().or(z.literal(''))\n    } else {\n      validator = z.boolean().optional()\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 = formFields.some((f) => isSupportedFormField(f) && f.required)\n  const answersObject = z.object(answers)\n\n  return z\n    .object({\n      meetingId: z.string().min(1),\n      startTimeMs: z.number().int().positive(),\n      durationMs: z.number().int().positive(),\n      firstName: z.string().min(1, { message: 'First name is required' }).max(255),\n      lastName: z.string().min(1, { message: 'Last name is required' }).max(255),\n      email: z.string().email({ message: 'Please enter a valid email address' }).max(255),\n      timezone: z.string().refine(isValidIanaTimezone, { message: 'Invalid 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\nexport type MeetingBookingPayload = z.infer<ReturnType<typeof makeBookingSchema>>\n"]}