@aranova/tracking-next 1.1.0 → 1.2.1

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/README.md CHANGED
@@ -407,7 +407,54 @@ its Origin allowlist is a browser-enforced header a script forges trivially. A j
407
407
  a database row you can delete; a junk **booking** puts an event on a real person's calendar
408
408
  and emails their contacts.
409
409
 
410
- `bookings.create()` returns a one-time `manage_token`. Put it in your confirmation email and
410
+ Doing your own work around a booking (validation, emails, a CRM)? Call the write client from
411
+ your own route instead of mounting the handler:
412
+
413
+ ```ts
414
+ // app/api/book/route.ts
415
+ import { createCalendarWriteClient } from "@aranova/tracking-next/calendar-server";
416
+ import { isCalendarError } from "@aranova/tracking-next/calendar";
417
+
418
+ const calendar = createCalendarWriteClient<AranovaCalendarKey>({
419
+ apiKey: process.env.ARANOVA_TRACKING_SECRET_KEY!,
420
+ endpoint: process.env.ARANOVA_TRACKING_ENDPOINT!,
421
+ });
422
+
423
+ const booking = await calendar.create({
424
+ calendar_key: "bookings",
425
+ start_at: "2026-10-14T13:00:00-04:00", // timed: start_at + duration_minutes (or end_at)
426
+ duration_minutes: 60,
427
+ // date: "2026-10-14", // all-day instead: a local date, no start/end/buffers
428
+ time_zone: "America/Toronto",
429
+ customer: { email, name, phone },
430
+ description: "<b>Arrive 1–2 PM</b>", // reaches Google exactly as sent
431
+ idempotency_key: submissionId,
432
+ metadata: body.attribution, // getBookingAttribution(), sent from the browser
433
+ });
434
+ ```
435
+
436
+ - **Forward the visit.** `getBookingAttribution()` (from `./calendar`, browser only) returns the
437
+ visit's `session_id`, `visitor_id`, `gclid`/`fbclid` and `utm_*`. It is what ties a booking to the
438
+ visitor's journey and form submission in the dashboard, and what missed-booking detection matches
439
+ on. It reads browser storage, so a custom route can't compute it: the browser sends it with the
440
+ form and the route passes it into `create({ metadata })`. `useBookingForm` merges it in for you
441
+ (your `metadata` keys win).
442
+
443
+ - **Overlaps are per calendar.** `ARANOVA_CALENDARS[].allowOverlaps` (from the CLI) says whether a
444
+ calendar accepts double bookings. When it is `false`, `create()` can throw `slot_unavailable`:
445
+ `isCalendarError(err, "slot_unavailable")`. Other codes: `calendar_not_connected`,
446
+ `calendar_disconnected`, `booking_not_found`, `booking_not_cancellable`,
447
+ `booking_not_reschedulable`, `manage_token_invalid`, `rate_limited`, `google_error`.
448
+ - **No server-side booking window.** Notice and how far ahead customers may book are your policy.
449
+ - **Your email can be the calendar invite.** The customer is always an attendee of the Google
450
+ event. Pass `send_invites: false` so Google sends nothing, then attach an ICS (`METHOD:REQUEST`)
451
+ with `UID` = `booking.ical_uid` and `ORGANIZER` = `booking.organizer_email`, so the customer's
452
+ copy is the same event as the calendar owner's. For `booking.all_day`, use
453
+ `DTSTART;VALUE=DATE`. `ical_uid` is null if Google hasn't confirmed the write; send a plain
454
+ confirmation then.
455
+ - **All-day bookings** are moved in Google Calendar, not through `reschedule()`.
456
+
457
+ `create()` returns a one-time `manage_token`. Put it in your confirmation email and
411
458
  send it as `X-Aranova-Manage-Token` to reschedule or cancel — it authorises exactly that one
412
459
  booking, which is why a public key alone still manages nothing.
413
460
 
@@ -1,4 +1,4 @@
1
- import { C as CalendarTransportConfig, B as BookingCreateInput, a as Booking, b as BookingRescheduleInput, c as BookingCancelInput } from './transport-I_cMSFQ6.mjs';
1
+ import { C as CalendarTransportConfig, B as BookingCreateInput, a as Booking, b as BookingRescheduleInput, c as BookingCancelInput } from './transport-DvAEZ1xf.mjs';
2
2
  import 'zod/mini';
3
3
  import './request-Doq36Rt0.mjs';
4
4
 
@@ -1,4 +1,4 @@
1
- import { C as CalendarTransportConfig, B as BookingCreateInput, a as Booking, b as BookingRescheduleInput, c as BookingCancelInput } from './transport-CDmGOiCq.js';
1
+ import { C as CalendarTransportConfig, B as BookingCreateInput, a as Booking, b as BookingRescheduleInput, c as BookingCancelInput } from './transport-ChCL6vPv.js';
2
2
  import 'zod/mini';
3
3
  import './request-Doq36Rt0.js';
4
4
 
@@ -1,10 +1,21 @@
1
+ import { j as BookingMetadata, r as CalendarSummary, m as BusyResponse, C as CalendarTransportConfig, q as CalendarErrorCode, d as BusyInterval } from './transport-DvAEZ1xf.mjs';
2
+ export { e as BOOKING_SOURCES, f as BOOKING_STATUSES, a as Booking, g as BookingAttendee, c as BookingCancelInput, B as BookingCreateInput, h as BookingCustomer, i as BookingLocation, k as BookingReminders, b as BookingRescheduleInput, l as BookingStatus, n as CALENDAR_ERROR_CODES, o as CALENDAR_METADATA_SCHEMA_VERSION, p as CALENDAR_SYNC_STATUSES, s as CalendarSyncStatus, G as GOOGLE_SYNC_STATES, t as GoogleSyncState, J as JsonScalar, M as MAX_ATTENDEES, u as MAX_BOOKING_MINUTES, v as MAX_REMINDER_MINUTES, w as MAX_REMINDER_OVERRIDES, x as bookingAttendeeSchema, y as bookingCancelSchema, z as bookingCreateSchema, A as bookingCustomerSchema, D as bookingLocationNullableSchema, E as bookingLocationSchema, F as bookingMetadataSchema, H as bookingReminderSchema, I as bookingRemindersSchema, K as bookingRescheduleSchema } from './transport-DvAEZ1xf.mjs';
1
3
  import { A as AranovaApiError } from './date-BHetnL7s.mjs';
2
4
  export { f as formatDateInTz } from './date-BHetnL7s.mjs';
3
- import { r as CalendarSummary, m as BusyResponse, C as CalendarTransportConfig, q as CalendarErrorCode, d as BusyInterval } from './transport-I_cMSFQ6.mjs';
4
- export { e as BOOKING_SOURCES, f as BOOKING_STATUSES, a as Booking, g as BookingAttendee, c as BookingCancelInput, B as BookingCreateInput, h as BookingCustomer, i as BookingLocation, j as BookingMetadata, k as BookingReminders, b as BookingRescheduleInput, l as BookingStatus, n as CALENDAR_ERROR_CODES, o as CALENDAR_METADATA_SCHEMA_VERSION, p as CALENDAR_SYNC_STATUSES, s as CalendarSyncStatus, G as GOOGLE_SYNC_STATES, t as GoogleSyncState, J as JsonScalar, M as MAX_ATTENDEES, u as MAX_BOOKING_MINUTES, v as MAX_REMINDER_MINUTES, w as MAX_REMINDER_OVERRIDES, x as bookingAttendeeSchema, y as bookingCancelSchema, z as bookingCreateSchema, A as bookingCustomerSchema, D as bookingLocationNullableSchema, E as bookingLocationSchema, F as bookingMetadataSchema, H as bookingReminderSchema, I as bookingRemindersSchema, K as bookingRescheduleSchema } from './transport-I_cMSFQ6.mjs';
5
5
  import 'zod/mini';
6
6
  import './request-Doq36Rt0.mjs';
7
7
 
8
+ /** The metadata keys a browser can fill: which visit made the booking, and how it arrived. */
9
+ type BookingAttribution = Pick<BookingMetadata, "session_id" | "visitor_id" | "gclid" | "fbclid" | "utm_source" | "utm_medium" | "utm_campaign" | "utm_term" | "utm_content">;
10
+ /**
11
+ * This browser's visit and click attribution, for a booking's `metadata`.
12
+ *
13
+ * `useBookingForm` merges it in for you. A site posting to its own route (e.g. `/api/book`)
14
+ * sends it along from the browser and passes it into `create({ metadata })`: that is what lets
15
+ * the dashboard tie the booking to the visitor's journey. Only keys with a value are present.
16
+ */
17
+ declare function getBookingAttribution(): BookingAttribution;
18
+
8
19
  /**
9
20
  * The browser-side calendar client — reads only.
10
21
  *
@@ -31,7 +42,10 @@ interface BusyQuery {
31
42
  timeZone?: string;
32
43
  }
33
44
  interface CalendarReadClient<TCalendarKey extends string = string> {
34
- /** Labels, timezones and status — everything a widget needs before booking. */
45
+ /**
46
+ * Labels, timezones and status — everything a widget needs before booking.
47
+ * Sandboxes are listed too (`is_sandbox`); a real calendar names its own in `sandbox_key`.
48
+ */
35
49
  listCalendars(): Promise<CalendarSummary[]>;
36
50
  /** Busy intervals only. Never the contents of the grantee's events. */
37
51
  busy(calendarKey: TCalendarKey, query: BusyQuery): Promise<BusyResponse>;
@@ -109,4 +123,4 @@ declare function groupSlotsByDay(slots: Slot[], timeZone: string): {
109
123
  slots: Slot[];
110
124
  }[];
111
125
 
112
- export { AranovaApiError, BusyInterval, type BusyQuery, BusyResponse, CalendarErrorCode, type CalendarReadClient, type CalendarReadClientConfig, CalendarSummary, CalendarTransportConfig, type ComputeSlotsOptions, type ComputeSlotsResult, type OpeningHours, type OpeningInterval, type Slot, computeSlots, createCalendarReadClient, durationMinutes, endOfDayInTz, formatSlotRange, groupSlotsByDay, instantFromWallClock, isCalendarError, localDateKey, offsetMinutesAt, resolveBrowserTimeZone, startOfDayInTz };
126
+ export { AranovaApiError, type BookingAttribution, BookingMetadata, BusyInterval, type BusyQuery, BusyResponse, CalendarErrorCode, type CalendarReadClient, type CalendarReadClientConfig, CalendarSummary, CalendarTransportConfig, type ComputeSlotsOptions, type ComputeSlotsResult, type OpeningHours, type OpeningInterval, type Slot, computeSlots, createCalendarReadClient, durationMinutes, endOfDayInTz, formatSlotRange, getBookingAttribution, groupSlotsByDay, instantFromWallClock, isCalendarError, localDateKey, offsetMinutesAt, resolveBrowserTimeZone, startOfDayInTz };
@@ -1,10 +1,21 @@
1
+ import { j as BookingMetadata, r as CalendarSummary, m as BusyResponse, C as CalendarTransportConfig, q as CalendarErrorCode, d as BusyInterval } from './transport-ChCL6vPv.js';
2
+ export { e as BOOKING_SOURCES, f as BOOKING_STATUSES, a as Booking, g as BookingAttendee, c as BookingCancelInput, B as BookingCreateInput, h as BookingCustomer, i as BookingLocation, k as BookingReminders, b as BookingRescheduleInput, l as BookingStatus, n as CALENDAR_ERROR_CODES, o as CALENDAR_METADATA_SCHEMA_VERSION, p as CALENDAR_SYNC_STATUSES, s as CalendarSyncStatus, G as GOOGLE_SYNC_STATES, t as GoogleSyncState, J as JsonScalar, M as MAX_ATTENDEES, u as MAX_BOOKING_MINUTES, v as MAX_REMINDER_MINUTES, w as MAX_REMINDER_OVERRIDES, x as bookingAttendeeSchema, y as bookingCancelSchema, z as bookingCreateSchema, A as bookingCustomerSchema, D as bookingLocationNullableSchema, E as bookingLocationSchema, F as bookingMetadataSchema, H as bookingReminderSchema, I as bookingRemindersSchema, K as bookingRescheduleSchema } from './transport-ChCL6vPv.js';
1
3
  import { A as AranovaApiError } from './date-BHetnL7s.js';
2
4
  export { f as formatDateInTz } from './date-BHetnL7s.js';
3
- import { r as CalendarSummary, m as BusyResponse, C as CalendarTransportConfig, q as CalendarErrorCode, d as BusyInterval } from './transport-CDmGOiCq.js';
4
- export { e as BOOKING_SOURCES, f as BOOKING_STATUSES, a as Booking, g as BookingAttendee, c as BookingCancelInput, B as BookingCreateInput, h as BookingCustomer, i as BookingLocation, j as BookingMetadata, k as BookingReminders, b as BookingRescheduleInput, l as BookingStatus, n as CALENDAR_ERROR_CODES, o as CALENDAR_METADATA_SCHEMA_VERSION, p as CALENDAR_SYNC_STATUSES, s as CalendarSyncStatus, G as GOOGLE_SYNC_STATES, t as GoogleSyncState, J as JsonScalar, M as MAX_ATTENDEES, u as MAX_BOOKING_MINUTES, v as MAX_REMINDER_MINUTES, w as MAX_REMINDER_OVERRIDES, x as bookingAttendeeSchema, y as bookingCancelSchema, z as bookingCreateSchema, A as bookingCustomerSchema, D as bookingLocationNullableSchema, E as bookingLocationSchema, F as bookingMetadataSchema, H as bookingReminderSchema, I as bookingRemindersSchema, K as bookingRescheduleSchema } from './transport-CDmGOiCq.js';
5
5
  import 'zod/mini';
6
6
  import './request-Doq36Rt0.js';
7
7
 
8
+ /** The metadata keys a browser can fill: which visit made the booking, and how it arrived. */
9
+ type BookingAttribution = Pick<BookingMetadata, "session_id" | "visitor_id" | "gclid" | "fbclid" | "utm_source" | "utm_medium" | "utm_campaign" | "utm_term" | "utm_content">;
10
+ /**
11
+ * This browser's visit and click attribution, for a booking's `metadata`.
12
+ *
13
+ * `useBookingForm` merges it in for you. A site posting to its own route (e.g. `/api/book`)
14
+ * sends it along from the browser and passes it into `create({ metadata })`: that is what lets
15
+ * the dashboard tie the booking to the visitor's journey. Only keys with a value are present.
16
+ */
17
+ declare function getBookingAttribution(): BookingAttribution;
18
+
8
19
  /**
9
20
  * The browser-side calendar client — reads only.
10
21
  *
@@ -31,7 +42,10 @@ interface BusyQuery {
31
42
  timeZone?: string;
32
43
  }
33
44
  interface CalendarReadClient<TCalendarKey extends string = string> {
34
- /** Labels, timezones and status — everything a widget needs before booking. */
45
+ /**
46
+ * Labels, timezones and status — everything a widget needs before booking.
47
+ * Sandboxes are listed too (`is_sandbox`); a real calendar names its own in `sandbox_key`.
48
+ */
35
49
  listCalendars(): Promise<CalendarSummary[]>;
36
50
  /** Busy intervals only. Never the contents of the grantee's events. */
37
51
  busy(calendarKey: TCalendarKey, query: BusyQuery): Promise<BusyResponse>;
@@ -109,4 +123,4 @@ declare function groupSlotsByDay(slots: Slot[], timeZone: string): {
109
123
  slots: Slot[];
110
124
  }[];
111
125
 
112
- export { AranovaApiError, BusyInterval, type BusyQuery, BusyResponse, CalendarErrorCode, type CalendarReadClient, type CalendarReadClientConfig, CalendarSummary, CalendarTransportConfig, type ComputeSlotsOptions, type ComputeSlotsResult, type OpeningHours, type OpeningInterval, type Slot, computeSlots, createCalendarReadClient, durationMinutes, endOfDayInTz, formatSlotRange, groupSlotsByDay, instantFromWallClock, isCalendarError, localDateKey, offsetMinutesAt, resolveBrowserTimeZone, startOfDayInTz };
126
+ export { AranovaApiError, type BookingAttribution, BookingMetadata, BusyInterval, type BusyQuery, BusyResponse, CalendarErrorCode, type CalendarReadClient, type CalendarReadClientConfig, CalendarSummary, CalendarTransportConfig, type ComputeSlotsOptions, type ComputeSlotsResult, type OpeningHours, type OpeningInterval, type Slot, computeSlots, createCalendarReadClient, durationMinutes, endOfDayInTz, formatSlotRange, getBookingAttribution, groupSlotsByDay, instantFromWallClock, isCalendarError, localDateKey, offsetMinutesAt, resolveBrowserTimeZone, startOfDayInTz };
package/dist/calendar.js CHANGED
@@ -57,6 +57,7 @@ __export(calendar_exports, {
57
57
  endOfDayInTz: () => endOfDayInTz,
58
58
  formatDateInTz: () => formatDateInTz,
59
59
  formatSlotRange: () => formatSlotRange,
60
+ getBookingAttribution: () => getBookingAttribution,
60
61
  groupSlotsByDay: () => groupSlotsByDay,
61
62
  instantFromWallClock: () => instantFromWallClock,
62
63
  isCalendarError: () => isCalendarError,
@@ -67,6 +68,135 @@ __export(calendar_exports, {
67
68
  });
68
69
  module.exports = __toCommonJS(calendar_exports);
69
70
 
71
+ // ../tracking-core/src/tracking.ts
72
+ var TRACKING_PARAM_KEYS = [
73
+ "gclid",
74
+ // Google's iOS/Safari replacement click IDs — issued when privacy features
75
+ // withhold gclid (wbraid: web-to-web, gbraid: app-to-web). First-class
76
+ // citizens: captured, persisted, and attributed exactly like gclid.
77
+ "wbraid",
78
+ "gbraid",
79
+ // Yelp Ads click id (40 hex chars), appended to ad-click URLs.
80
+ "ylpcid",
81
+ "fbclid",
82
+ "utm_source",
83
+ "utm_medium",
84
+ "utm_campaign",
85
+ "utm_term",
86
+ "utm_content"
87
+ ];
88
+ function createEmptyTrackingParams() {
89
+ return {
90
+ gclid: null,
91
+ wbraid: null,
92
+ gbraid: null,
93
+ ylpcid: null,
94
+ fbclid: null,
95
+ utm_source: null,
96
+ utm_medium: null,
97
+ utm_campaign: null,
98
+ utm_term: null,
99
+ utm_content: null
100
+ };
101
+ }
102
+ function normalizeTrackingCookieValue(value) {
103
+ return typeof value === "string" && value.length > 0 ? value : null;
104
+ }
105
+ function getTrackingParamsFromCookieReader(readCookie) {
106
+ return TRACKING_PARAM_KEYS.reduce((params, key) => {
107
+ params[key] = normalizeTrackingCookieValue(readCookie(key));
108
+ return params;
109
+ }, createEmptyTrackingParams());
110
+ }
111
+ var FALLBACK_STORAGE_PREFIX = "_aranova_track_";
112
+ function fallbackKey(name) {
113
+ return `${FALLBACK_STORAGE_PREFIX}${name}`;
114
+ }
115
+ function readCookieValue(name) {
116
+ if (typeof document !== "undefined") {
117
+ const cookies = document.cookie ? document.cookie.split("; ") : [];
118
+ const match = cookies.find((cookie) => cookie.startsWith(`${name}=`));
119
+ if (match) {
120
+ const [, rawValue = ""] = match.split("=");
121
+ return normalizeTrackingCookieValue(decodeURIComponent(rawValue));
122
+ }
123
+ }
124
+ if (typeof window !== "undefined") {
125
+ try {
126
+ const stored = window.localStorage.getItem(fallbackKey(name));
127
+ if (stored) return normalizeTrackingCookieValue(decodeURIComponent(stored));
128
+ } catch {
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+ function getCookieValueFromDocument(key) {
134
+ return readCookieValue(key);
135
+ }
136
+
137
+ // ../tracking-core/src/session.ts
138
+ var VISITOR_STORAGE_KEY = "aranova_tracking_visitor";
139
+ var SESSION_STORAGE_KEY = "aranova_tracking_session";
140
+ var SESSION_IDLE_MS = 30 * 60 * 1e3;
141
+ function readLocalStorage(key) {
142
+ try {
143
+ return window.localStorage.getItem(key);
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+ function peekVisitorId() {
149
+ if (typeof window === "undefined") return null;
150
+ const existing = readLocalStorage(VISITOR_STORAGE_KEY);
151
+ return existing && existing.length > 0 ? existing : null;
152
+ }
153
+ function peekSessionId(now = Date.now()) {
154
+ if (typeof window === "undefined") return null;
155
+ const raw = readLocalStorage(SESSION_STORAGE_KEY);
156
+ if (!raw) return null;
157
+ try {
158
+ const parsed = JSON.parse(raw);
159
+ if (typeof parsed.id !== "string" || typeof parsed.last_event_at !== "number") return null;
160
+ return now - parsed.last_event_at <= SESSION_IDLE_MS ? parsed.id : null;
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ // ../tracking-core/src/resources/calendar/attribution.ts
167
+ var LIMITS = {
168
+ session_id: 64,
169
+ visitor_id: 64,
170
+ gclid: 256,
171
+ fbclid: 256,
172
+ utm_source: 128,
173
+ utm_medium: 128,
174
+ utm_campaign: 128,
175
+ utm_term: 128,
176
+ utm_content: 128
177
+ };
178
+ function getBookingAttribution() {
179
+ if (typeof window === "undefined") return {};
180
+ const params = getTrackingParamsFromCookieReader(getCookieValueFromDocument);
181
+ const values = {
182
+ session_id: peekSessionId(),
183
+ visitor_id: peekVisitorId(),
184
+ gclid: params.gclid,
185
+ fbclid: params.fbclid,
186
+ utm_source: params.utm_source,
187
+ utm_medium: params.utm_medium,
188
+ utm_campaign: params.utm_campaign,
189
+ utm_term: params.utm_term,
190
+ utm_content: params.utm_content
191
+ };
192
+ const attribution = {};
193
+ for (const key of Object.keys(LIMITS)) {
194
+ const value = values[key];
195
+ if (value) attribution[key] = value.slice(0, LIMITS[key]);
196
+ }
197
+ return attribution;
198
+ }
199
+
70
200
  // ../tracking-core/src/capabilities.ts
71
201
  var registered = /* @__PURE__ */ new Set();
72
202
  function registerCapability(capability) {
@@ -280,22 +410,27 @@ var zBookingRescheduleSchema = z.strictObject({
280
410
  time_zone: z.string().check(z.minLength(1), z.maxLength(64), z.regex(/^[A-Za-z0-9_+\-\/]+$/))
281
411
  });
282
412
  var zBookingResponse = z.strictObject({
413
+ all_day: z.boolean(),
283
414
  appointment_type: z.nullable(z.string()),
284
415
  calendar_key: z.string(),
416
+ created_at: z.iso.datetime({ offset: true }),
285
417
  customer: zBookingCustomerOutput,
286
418
  end_at: z.iso.datetime({ offset: true }),
419
+ google_event_id: z.nullable(z.string()),
287
420
  google_html_link: z.nullable(z.string()),
288
- google_sync_state: z.string(),
421
+ google_sync_state: z.enum(["queued", "synced", "uncertain", "failed", "drifted"]),
422
+ ical_uid: z.nullable(z.string()),
289
423
  id: z.string(),
290
424
  manage_token: z.nullable(z.string()),
291
425
  meet_url: z.nullable(z.string()),
426
+ organizer_email: z.nullable(z.string()),
292
427
  start_at: z.iso.datetime({ offset: true }),
293
- status: z.string(),
428
+ status: z.enum(["pending", "confirmed", "cancelled", "no_show"]),
294
429
  time_zone: z.string(),
295
430
  title: z.string()
296
431
  });
297
432
  var zBookingSource = z.enum(["website", "phone", "walk_in", "import", "operator"]);
298
- var zBookingMetadata = z.strictObject({
433
+ var zBookingMetadataInput = z.strictObject({
299
434
  custom: z.nullish(
300
435
  z.record(z.string(), z.nullable(z.union([z.string(), z.int(), z.number(), z.boolean()])))
301
436
  ),
@@ -315,6 +450,42 @@ var zBookingMetadata = z.strictObject({
315
450
  utm_term: z.nullish(z.string().check(z.maxLength(128))),
316
451
  visitor_id: z.nullish(z.string().check(z.maxLength(64)))
317
452
  });
453
+ var zBusySource = z.enum(["google", "booking"]);
454
+ var zBusyInterval = z.strictObject({
455
+ end_at: z.iso.datetime({ offset: true }),
456
+ source: zBusySource,
457
+ start_at: z.iso.datetime({ offset: true })
458
+ });
459
+ var zBusyWindow = z.strictObject({
460
+ since: z.iso.datetime({ offset: true }),
461
+ until: z.iso.datetime({ offset: true })
462
+ });
463
+ var zBusyResponse = z.strictObject({
464
+ busy: z.array(zBusyInterval),
465
+ calendar_key: z.string(),
466
+ generated_at: z.iso.datetime({ offset: true }),
467
+ time_zone: z.string(),
468
+ window: zBusyWindow
469
+ });
470
+ var zCalendarSummary = z.strictObject({
471
+ allow_overlaps: z.boolean(),
472
+ calendar_key: z.string(),
473
+ connected_at: z.nullable(z.iso.datetime({ offset: true })),
474
+ google_account_email: z.nullable(z.string()),
475
+ granted_scopes: z.nullable(z.array(z.string())),
476
+ is_sandbox: z.boolean(),
477
+ label: z.string(),
478
+ sandbox_key: z.nullable(z.string()),
479
+ status: z.enum([
480
+ "pending_invite",
481
+ "awaiting_calendar_selection",
482
+ "connected",
483
+ "needs_reconnect",
484
+ "revoked",
485
+ "disabled"
486
+ ]),
487
+ time_zone: z.string()
488
+ });
318
489
  var zCustomLocation = z.strictObject({
319
490
  kind: z.literal("custom"),
320
491
  text: z.string().check(z.maxLength(1024))
@@ -334,9 +505,23 @@ var zErrorCode = z.enum([
334
505
  "bad_request",
335
506
  "not_found",
336
507
  "conflict",
508
+ "gone",
337
509
  "incompatible_payload",
338
510
  "repeat_confirmation_required",
339
511
  "idempotency_conflict",
512
+ "slot_unavailable",
513
+ "calendar_not_connected",
514
+ "calendar_disconnected",
515
+ "booking_not_found",
516
+ "booking_not_cancellable",
517
+ "booking_not_reschedulable",
518
+ "manage_token_invalid",
519
+ "google_error",
520
+ "calendar_not_owned",
521
+ "calendar_is_sandbox",
522
+ "sandbox_exists",
523
+ "sandbox_key_too_long",
524
+ "sandbox_not_found",
340
525
  "validation_error",
341
526
  "unknown_service",
342
527
  "invalid_sale_properties",
@@ -371,15 +556,16 @@ var zBookingCreateSchema = z.strictObject({
371
556
  buffer_before_minutes: z._default(z.optional(z.int().check(z.gte(0), z.lte(1440))), 0),
372
557
  calendar_key: z.string().check(z.minLength(1), z.maxLength(64)),
373
558
  customer: zBookingCustomerInput,
559
+ date: z.nullish(z.iso.date()),
374
560
  description: z.nullish(z.string().check(z.maxLength(8e3))),
375
561
  duration_minutes: z.nullish(z.int().check(z.gt(0), z.lte(1440))),
376
562
  end_at: z.nullish(z.iso.datetime({ offset: true })),
377
563
  idempotency_key: z.nullish(z.string().check(z.maxLength(128))),
378
564
  location: z.nullish(z.union([zMeetLocation, zPhysicalLocation, zPhoneLocation, zCustomLocation])),
379
- metadata: z.nullish(zBookingMetadata),
565
+ metadata: z.nullish(zBookingMetadataInput),
380
566
  reminders: z.nullish(zBookingReminders),
381
567
  send_invites: z._default(z.optional(z.boolean()), true),
382
- start_at: z.iso.datetime({ offset: true }),
568
+ start_at: z.nullish(z.iso.datetime({ offset: true })),
383
569
  time_zone: z.string().check(z.minLength(1), z.maxLength(64), z.regex(/^[A-Za-z0-9_+\-\/]+$/)),
384
570
  title: z.nullish(z.string().check(z.maxLength(300)))
385
571
  });
@@ -413,22 +599,17 @@ var MAX_REMINDER_OVERRIDES = 5;
413
599
  var MAX_REMINDER_MINUTES = 40320;
414
600
  var MAX_BOOKING_MINUTES = 1440;
415
601
  var BOOKING_SOURCES = ["website", "phone", "walk_in", "import", "operator"];
416
- var BOOKING_STATUSES = ["pending", "confirmed", "cancelled", "no_show"];
417
- var GOOGLE_SYNC_STATES = ["queued", "synced", "uncertain", "failed", "drifted"];
418
- var CALENDAR_SYNC_STATUSES = [
419
- "pending_invite",
420
- "awaiting_calendar_selection",
421
- "connected",
422
- "needs_reconnect",
423
- "revoked",
424
- "disabled"
425
- ];
602
+ var BOOKING_STATUSES = zBookingResponse.shape.status.options;
603
+ var GOOGLE_SYNC_STATES = zBookingResponse.shape.google_sync_state.options;
604
+ var CALENDAR_SYNC_STATUSES = zCalendarSummary.shape.status.options;
426
605
  var CALENDAR_ERROR_CODES = [
427
606
  "slot_unavailable",
428
607
  "calendar_not_connected",
429
608
  "calendar_disconnected",
430
609
  "booking_not_found",
431
- "outside_booking_window",
610
+ "booking_not_cancellable",
611
+ "booking_not_reschedulable",
612
+ "manage_token_invalid",
432
613
  "rate_limited",
433
614
  "google_error"
434
615
  ];
@@ -449,7 +630,7 @@ var ianaTz = z2.string().check(
449
630
  var bookingAttendeeSchema = zBookingAttendee;
450
631
  var bookingReminderSchema = zBookingReminder;
451
632
  var bookingCustomerSchema = zBookingCustomerInput;
452
- var bookingMetadataSchema = z2.extend(zBookingMetadata, {
633
+ var bookingMetadataSchema = z2.extend(zBookingMetadataInput, {
453
634
  custom: z2.nullish(
454
635
  z2.record(z2.string(), z2.union([z2.string(), z2.int(), z2.number(), z2.boolean(), z2.null()]))
455
636
  )
@@ -478,6 +659,24 @@ function refineEndXor(value, ctx) {
478
659
  addIssue(ctx, ["end_at"], "end_at must be after start_at");
479
660
  }
480
661
  }
662
+ function refineStartXor(value, ctx) {
663
+ const hasStart = value.start_at !== void 0 && value.start_at !== null;
664
+ const hasDate = value.date !== void 0 && value.date !== null;
665
+ if (hasStart === hasDate) {
666
+ addIssue(ctx, ["start_at"], "provide exactly one of start_at or date");
667
+ return;
668
+ }
669
+ if (hasStart) {
670
+ refineEndXor(value, ctx);
671
+ return;
672
+ }
673
+ if (value.end_at != null || value.duration_minutes != null) {
674
+ addIssue(ctx, ["date"], "an all-day booking (date) takes no end_at or duration_minutes");
675
+ }
676
+ if (value.buffer_before_minutes || value.buffer_after_minutes) {
677
+ addIssue(ctx, ["date"], "an all-day booking (date) takes no buffers");
678
+ }
679
+ }
481
680
  var bookingRemindersSchema = z2.extend(zBookingReminders, {
482
681
  overrides: z2._default(
483
682
  z2.array(bookingReminderSchema).check(z2.maxLength(MAX_REMINDER_OVERRIDES)),
@@ -496,7 +695,7 @@ var bookingCreateSchema = z2.extend(zBookingCreateSchema, {
496
695
  // our compositions into its own output.
497
696
  reminders: z2.nullish(bookingRemindersSchema),
498
697
  metadata: z2.nullish(bookingMetadataSchema)
499
- }).check((ctx) => refineEndXor(ctx.value, ctx));
698
+ }).check((ctx) => refineStartXor(ctx.value, ctx));
500
699
  var bookingRescheduleSchema = z2.extend(zBookingRescheduleSchema, { time_zone: ianaTz }).check((ctx) => refineEndXor(ctx.value, ctx));
501
700
  var bookingCancelSchema = zBookingCancelSchema;
502
701
 
@@ -614,6 +813,7 @@ function groupSlotsByDay(slots, timeZone) {
614
813
  endOfDayInTz,
615
814
  formatDateInTz,
616
815
  formatSlotRange,
816
+ getBookingAttribution,
617
817
  groupSlotsByDay,
618
818
  instantFromWallClock,
619
819
  isCalendarError,