@fayz-ai/plugin-agenda 0.26.1 → 0.27.0

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.
@@ -0,0 +1,96 @@
1
+ // src/integrations/google-calendar/mapping.ts
2
+ function bookingSummary(b) {
3
+ const services = (b.services ?? []).map((s) => s.name).filter(Boolean).join(", ");
4
+ const who = b.clientName ?? "";
5
+ if (services && who) return `${services} \u2014 ${who}`;
6
+ return services || who || "Appointment";
7
+ }
8
+ function bookingToEvent(b, timeZone) {
9
+ if (!b.startsAt || !b.endsAt) return null;
10
+ const notes = [b.notes, b.professionalName ? `Pro: ${b.professionalName}` : "", b.locationName ?? ""].filter(Boolean).join("\n");
11
+ return {
12
+ summary: bookingSummary(b),
13
+ description: notes || void 0,
14
+ start: { dateTime: b.startsAt, timeZone },
15
+ end: { dateTime: b.endsAt, timeZone }
16
+ };
17
+ }
18
+ function eventToBookingPatch(ev) {
19
+ if (ev.status === "cancelled") return { status: "cancelled" };
20
+ const t = eventTimes(ev);
21
+ return { startsAt: t.startsAt, endsAt: t.endsAt };
22
+ }
23
+ function appointmentRowSummary(row) {
24
+ const m = row.metadata ?? {};
25
+ const services = m.serviceNames ?? "";
26
+ const who = m.contactName ?? m.clientName ?? "";
27
+ if (services && who) return `${services} \u2014 ${who}`;
28
+ return services || who || "Appointment";
29
+ }
30
+ function appointmentRowToEvent(row) {
31
+ return {
32
+ summary: appointmentRowSummary(row),
33
+ description: row.notes ?? void 0,
34
+ start: { dateTime: row.starts_at },
35
+ end: { dateTime: row.ends_at ?? row.starts_at }
36
+ };
37
+ }
38
+ function eventTimes(ev) {
39
+ const allDay = !ev.start?.dateTime && !!ev.start?.date;
40
+ const startsAt = ev.start?.dateTime ?? ev.start?.date ?? "";
41
+ const endsAt = ev.end?.dateTime ?? ev.end?.date ?? startsAt;
42
+ return { startsAt, endsAt, allDay };
43
+ }
44
+ function channelRowToChannel(r) {
45
+ return {
46
+ id: r.id,
47
+ integrationId: r.integration_id,
48
+ tenantId: r.tenant_id,
49
+ googleCalendarId: r.google_calendar_id,
50
+ summary: r.summary ?? null,
51
+ color: r.color ?? null,
52
+ direction: r.direction,
53
+ targetKind: r.target_kind ?? null,
54
+ targetId: r.target_id ?? null,
55
+ importMode: r.import_mode ?? "appointment",
56
+ syncToken: r.sync_token ?? null,
57
+ channelId: r.channel_id ?? null,
58
+ resourceId: r.resource_id ?? null,
59
+ channelExpiresAt: r.channel_expires_at ?? null,
60
+ isActive: r.is_active ?? true,
61
+ createdAt: r.created_at ?? "",
62
+ updatedAt: r.updated_at ?? ""
63
+ };
64
+ }
65
+ function resolveTargetChannel(channels, booking) {
66
+ const eligible = channels.filter(
67
+ (c) => c.isActive && (c.direction === "outbound" || c.direction === "bidirectional")
68
+ );
69
+ const serviceIds = booking.serviceIds ?? [];
70
+ if (booking.assigneeId) {
71
+ const byAssignee = eligible.find((c) => c.targetKind === "assignee" && c.targetId === booking.assigneeId);
72
+ if (byAssignee) return byAssignee;
73
+ }
74
+ if (serviceIds.length) {
75
+ const byService = eligible.find((c) => c.targetKind === "service" && !!c.targetId && serviceIds.includes(c.targetId));
76
+ if (byService) return byService;
77
+ }
78
+ if (booking.locationId) {
79
+ const byLocation = eligible.find((c) => c.targetKind === "location" && c.targetId === booking.locationId);
80
+ if (byLocation) return byLocation;
81
+ }
82
+ return eligible.find((c) => c.targetKind === null) ?? null;
83
+ }
84
+
85
+ // src/integrations/google-calendar/data/tables.ts
86
+ var GCAL_TABLES = {
87
+ integrations: "plg_calendar_integrations",
88
+ channels: "plg_calendar_channels",
89
+ syncLog: "plg_calendar_sync_log",
90
+ webhookDeliveries: "plg_calendar_webhook_deliveries",
91
+ automationOutbox: "plg_calendar_automation_outbox"
92
+ };
93
+
94
+ export { GCAL_TABLES, appointmentRowToEvent, bookingToEvent, channelRowToChannel, eventTimes, eventToBookingPatch, resolveTargetChannel };
95
+ //# sourceMappingURL=chunk-AKALKGB2.js.map
96
+ //# sourceMappingURL=chunk-AKALKGB2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/integrations/google-calendar/mapping.ts","../src/integrations/google-calendar/data/tables.ts"],"names":[],"mappings":"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","file":"chunk-AKALKGB2.js","sourcesContent":["// Booking ↔ Google Calendar event mapping — the SINGLE SOURCE OF TRUTH.\n//\n// These helpers are pure and Deno-friendly (only `import type`, no runtime\n// node/react/https imports), so the Supabase edge function imports this exact\n// file via a relative path (`../../src/integrations/google-calendar/mapping.ts`)\n// instead of keeping a divergent mirror. `import type` is elided by the Deno /\n// esbuild bundler, so the plugin `types.ts`/`CalendarBooking` type imports never\n// become runtime dependencies of the function bundle.\n//\n// Two callers, two input shapes:\n// • the SDK/UI works with the rich `CalendarBooking` (camelCase, joined) — see\n// bookingToEvent / eventToBookingPatch;\n// • the edge function + RPCs work with the raw `AppointmentRow` (snake_case DB\n// columns) — see appointmentRowToEvent / eventToAppointmentPatch.\n// Field decisions (title, all-day, cancel) are made once, here.\nimport type { CalendarBooking } from '../../types'\nimport type { GCalEvent, AppointmentRow, CalendarChannel, CalendarChannelRow } from './types'\n\nexport interface GCalEventInput {\n summary: string\n description?: string\n // Timed events use dateTime; all-day events use date (YYYY-MM-DD).\n start: { dateTime?: string; date?: string; timeZone?: string }\n end: { dateTime?: string; date?: string; timeZone?: string }\n}\n\n/** Normalized time window read off a Google event, all-day aware. */\nexport interface EventTimes {\n startsAt: string\n endsAt: string\n allDay: boolean\n}\n\n// --- Booking (rich SDK shape) → event -------------------------------------\n\n/** Build the event title from the booking's services + client. */\nexport function bookingSummary(b: Pick<CalendarBooking, 'services' | 'clientName'>): string {\n const services = (b.services ?? []).map((s) => s.name).filter(Boolean).join(', ')\n const who = b.clientName ?? ''\n if (services && who) return `${services} — ${who}`\n return services || who || 'Appointment'\n}\n\n/** Map a Fayz booking to a Google Calendar event input. Returns null when the booking has no end. */\nexport function bookingToEvent(b: CalendarBooking, timeZone?: string): GCalEventInput | null {\n if (!b.startsAt || !b.endsAt) return null\n const notes = [b.notes, b.professionalName ? `Pro: ${b.professionalName}` : '', b.locationName ?? '']\n .filter(Boolean).join('\\n')\n return {\n summary: bookingSummary(b),\n description: notes || undefined,\n start: { dateTime: b.startsAt, timeZone },\n end: { dateTime: b.endsAt, timeZone },\n }\n}\n\n/** Map an inbound Google event change to a partial booking update (reschedule/cancel), all-day aware. */\nexport function eventToBookingPatch(ev: GCalEvent): { startsAt?: string; endsAt?: string; status?: 'cancelled' } {\n if (ev.status === 'cancelled') return { status: 'cancelled' }\n const t = eventTimes(ev)\n return { startsAt: t.startsAt, endsAt: t.endsAt }\n}\n\n// --- Appointment row (raw DB shape) → event -------------------------------\n\n/**\n * Build the event title from an appointments row's denormalized metadata.\n * The raw row has no joined services/client, so we read the names the app\n * stamps onto metadata (serviceNames, contactName/clientName).\n */\nexport function appointmentRowSummary(row: AppointmentRow): string {\n const m = row.metadata ?? {}\n const services = (m.serviceNames as string) ?? ''\n const who = (m.contactName as string) ?? (m.clientName as string) ?? ''\n if (services && who) return `${services} — ${who}`\n return services || who || 'Appointment'\n}\n\n/** Map a raw appointments row to a Google event input (outbound push from the edge function). */\nexport function appointmentRowToEvent(row: AppointmentRow): GCalEventInput {\n return {\n summary: appointmentRowSummary(row),\n description: row.notes ?? undefined,\n start: { dateTime: row.starts_at },\n end: { dateTime: row.ends_at ?? row.starts_at },\n }\n}\n\n/**\n * Map an inbound Google event to a DB-friendly appointment patch (snake_case\n * times), all-day aware, cancellation included. Consumed by the edge function\n * when calling gcal_apply_event_patch.\n */\nexport function eventToAppointmentPatch(ev: GCalEvent): {\n starts_at?: string\n ends_at?: string\n summary?: string\n allDay?: boolean\n cancelled: boolean\n} {\n if (ev.status === 'cancelled') return { cancelled: true }\n const t = eventTimes(ev)\n return { starts_at: t.startsAt, ends_at: t.endsAt, summary: ev.summary, allDay: t.allDay, cancelled: false }\n}\n\n// --- Shared time normalization (all-day aware) ----------------------------\n\n/**\n * Read a normalized {startsAt, endsAt, allDay} window off a Google event.\n * All-day events carry `start.date`/`end.date` (YYYY-MM-DD, end exclusive) and\n * no dateTime; timed events carry dateTime. Falls back start→end when one side\n * is missing so callers always get a usable pair.\n */\nexport function eventTimes(ev: GCalEvent): EventTimes {\n const allDay = !ev.start?.dateTime && !!ev.start?.date\n const startsAt = ev.start?.dateTime ?? ev.start?.date ?? ''\n const endsAt = ev.end?.dateTime ?? ev.end?.date ?? startsAt\n return { startsAt, endsAt, allDay }\n}\n\n// --- Channel resolution (outbound destination priority) -------------------\n\n/** Convert a raw plg_calendar_channels row (snake_case) to the camelCase CalendarChannel. */\nexport function channelRowToChannel(r: CalendarChannelRow): CalendarChannel {\n return {\n id: r.id,\n integrationId: r.integration_id,\n tenantId: r.tenant_id,\n googleCalendarId: r.google_calendar_id,\n summary: r.summary ?? null,\n color: r.color ?? null,\n direction: r.direction,\n targetKind: r.target_kind ?? null,\n targetId: r.target_id ?? null,\n importMode: r.import_mode ?? 'appointment',\n syncToken: r.sync_token ?? null,\n channelId: r.channel_id ?? null,\n resourceId: r.resource_id ?? null,\n channelExpiresAt: r.channel_expires_at ?? null,\n isActive: r.is_active ?? true,\n createdAt: r.created_at ?? '',\n updatedAt: r.updated_at ?? '',\n }\n}\n\nexport interface ChannelMatchInput {\n assigneeId?: string | null\n serviceIds?: string[]\n locationId?: string | null\n}\n\n/**\n * Pick the outbound destination channel for a booking, by priority:\n * 1. a channel targeting the booking's assignee,\n * 2. else a channel targeting one of the booking's services,\n * 3. else a channel targeting the booking's location,\n * 4. else the whole-agenda channel (targetKind === null).\n * Only active outbound/bidirectional channels are eligible. Returns null when\n * nothing matches (caller falls back to the integration's default calendar).\n */\nexport function resolveTargetChannel(\n channels: CalendarChannel[],\n booking: ChannelMatchInput,\n): CalendarChannel | null {\n const eligible = channels.filter(\n (c) => c.isActive && (c.direction === 'outbound' || c.direction === 'bidirectional'),\n )\n const serviceIds = booking.serviceIds ?? []\n\n if (booking.assigneeId) {\n const byAssignee = eligible.find((c) => c.targetKind === 'assignee' && c.targetId === booking.assigneeId)\n if (byAssignee) return byAssignee\n }\n if (serviceIds.length) {\n const byService = eligible.find((c) => c.targetKind === 'service' && !!c.targetId && serviceIds.includes(c.targetId))\n if (byService) return byService\n }\n if (booking.locationId) {\n const byLocation = eligible.find((c) => c.targetKind === 'location' && c.targetId === booking.locationId)\n if (byLocation) return byLocation\n }\n return eligible.find((c) => c.targetKind === null) ?? null\n}\n","// Canonical DB object names for the Google Calendar connector.\n//\n// Plugin-owned tables carry the platform `plg_<plugin>_` prefix (DATA-MODEL.md\n// Ring 1). Single source of truth so the connector data layer (supabase.ts) and\n// the google-calendar-sync edge function never drift on a table name. The SQL\n// side is owned by ./migrations/001_google_calendar.sql + 002_smart_sync.sql\n// (fresh installs) and 003_plg_rename.sql (legacy-pool rename).\n// webhookDeliveries/automationOutbox were recovered from the deployed bundles on\n// 2026-08-12 (FAY-1379): both live functions read them off this map, but the two\n// tables existed only in the pool — 006 is the migration that creates them.\nexport const GCAL_TABLES = {\n integrations: 'plg_calendar_integrations',\n channels: 'plg_calendar_channels',\n syncLog: 'plg_calendar_sync_log',\n webhookDeliveries: 'plg_calendar_webhook_deliveries',\n automationOutbox: 'plg_calendar_automation_outbox',\n} as const\n"]}
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { createMockAgendaProvider, createSupabasePublicBookingProvider } from './chunk-A77PUFY5.js';
2
2
  export { createMockAgendaProvider } from './chunk-A77PUFY5.js';
3
+ import { GCAL_TABLES, channelRowToChannel } from './chunk-AKALKGB2.js';
4
+ export { bookingToEvent, eventToBookingPatch } from './chunk-AKALKGB2.js';
3
5
  import React23, { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect } from 'react';
4
6
  import { tl, recordRailFields, RECORD_GROUP, activeFacet, connectionStatus, createConnectionStore, getActiveTenantId, createSafeDataProvider, todayLocal, getSupabaseClientOptional, registerTranslations, registerMessageDefaults, connectorRuntimeToken, CONNECTOR_RUNTIME_TOKEN_HEADER, getActiveUnitId, assertWrote, emitDataChanged, globalCache, usePluginRuntimeOptional, useTranslation, formatCurrency, useActiveUnitId, emitDomainEvent, getCurrentLocale, localDateKey, useDataChanged, resolveDataProvider, describeError, getWidgetsForZone } from '@fayz-ai/core';
5
7
  import { createPluginContext, defineSettingsTab, attendanceSettingsParent, fayzConnectorOAuth, UNIT_DETAIL_ZONES, setScheduleBlockConfig, getScheduleBlockConfig, registerPersonActions, PluginSettingsPanel, adminNavigateTo, createArchetypeLookup, dedup, usePluginPrefs, PersonLink, SettingsGroup, MessageTemplateRow, useTenantPluginSettings, ToggleRow, SelectRow, usePermissionOptional, useAdminPath, PageActions, useRightRailStore, useFieldRules, deriveEntityKey, WidgetSlot, openEntity, ScheduleEditor, useLimitGuard, useLookupCreate, CrudFormPage, CrudDetailPage, ContactPicker, PaymentStatusBadge as PaymentStatusBadge$1, RightRailPage, CrudRecordModal, invalidateLimit } from '@fayz-ai/admin';
@@ -4366,7 +4368,7 @@ function readableTextOn(hex) {
4366
4368
  const L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
4367
4369
  return (L + 0.05) / 0.05 > 1.05 / (L + 0.05) ? "#111827" : "#ffffff";
4368
4370
  }
4369
- function bookingToEvent(booking, statusColors, visibleResourceIds = [], serviceLooks) {
4371
+ function bookingToEvent2(booking, statusColors, visibleResourceIds = [], serviceLooks) {
4370
4372
  const color = statusColors[booking.status] ?? "#6b7280";
4371
4373
  const kindColor = serviceLooks ? bookingCardColor(booking, serviceLooks) : null;
4372
4374
  const serviceNames = booking.services?.map((service) => service.name).join(", ") ?? "";
@@ -5078,7 +5080,7 @@ function CalendarView() {
5078
5080
  };
5079
5081
  }, [serviceIdsKey, provider2]);
5080
5082
  const bookingEvents = useMemo(
5081
- () => filteredBookings.map((b) => bookingToEvent(b, statusColors, visibleResourceIds, colorByKind ? serviceLooks : void 0)),
5083
+ () => filteredBookings.map((b) => bookingToEvent2(b, statusColors, visibleResourceIds, colorByKind ? serviceLooks : void 0)),
5082
5084
  [filteredBookings, statusColors, visibleResourceIds, colorByKind, serviceLooks]
5083
5085
  );
5084
5086
  const allLocations = useMemo(() => {
@@ -19709,56 +19711,6 @@ function mapInvoiceStatus(status) {
19709
19711
  }
19710
19712
  }
19711
19713
 
19712
- // src/integrations/google-calendar/mapping.ts
19713
- function bookingSummary(b) {
19714
- const services = (b.services ?? []).map((s) => s.name).filter(Boolean).join(", ");
19715
- const who = b.clientName ?? "";
19716
- if (services && who) return `${services} \u2014 ${who}`;
19717
- return services || who || "Appointment";
19718
- }
19719
- function bookingToEvent2(b, timeZone) {
19720
- if (!b.startsAt || !b.endsAt) return null;
19721
- const notes = [b.notes, b.professionalName ? `Pro: ${b.professionalName}` : "", b.locationName ?? ""].filter(Boolean).join("\n");
19722
- return {
19723
- summary: bookingSummary(b),
19724
- description: notes || void 0,
19725
- start: { dateTime: b.startsAt, timeZone },
19726
- end: { dateTime: b.endsAt, timeZone }
19727
- };
19728
- }
19729
- function eventToBookingPatch(ev) {
19730
- if (ev.status === "cancelled") return { status: "cancelled" };
19731
- const t2 = eventTimes(ev);
19732
- return { startsAt: t2.startsAt, endsAt: t2.endsAt };
19733
- }
19734
- function eventTimes(ev) {
19735
- const allDay = !ev.start?.dateTime && !!ev.start?.date;
19736
- const startsAt = ev.start?.dateTime ?? ev.start?.date ?? "";
19737
- const endsAt = ev.end?.dateTime ?? ev.end?.date ?? startsAt;
19738
- return { startsAt, endsAt, allDay };
19739
- }
19740
- function channelRowToChannel(r) {
19741
- return {
19742
- id: r.id,
19743
- integrationId: r.integration_id,
19744
- tenantId: r.tenant_id,
19745
- googleCalendarId: r.google_calendar_id,
19746
- summary: r.summary ?? null,
19747
- color: r.color ?? null,
19748
- direction: r.direction,
19749
- targetKind: r.target_kind ?? null,
19750
- targetId: r.target_id ?? null,
19751
- importMode: r.import_mode ?? "appointment",
19752
- syncToken: r.sync_token ?? null,
19753
- channelId: r.channel_id ?? null,
19754
- resourceId: r.resource_id ?? null,
19755
- channelExpiresAt: r.channel_expires_at ?? null,
19756
- isActive: r.is_active ?? true,
19757
- createdAt: r.created_at ?? "",
19758
- updatedAt: r.updated_at ?? ""
19759
- };
19760
- }
19761
-
19762
19714
  // src/integrations/google-calendar/credentials.ts
19763
19715
  var CONNECTION_COLUMNS = "id, provider, calendar_id, active, last_sync_at";
19764
19716
  function isIntegrationActive(row) {
@@ -19768,12 +19720,6 @@ function disconnectPatch() {
19768
19720
  return { active: false, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
19769
19721
  }
19770
19722
 
19771
- // src/integrations/google-calendar/data/tables.ts
19772
- var GCAL_TABLES = {
19773
- integrations: "plg_calendar_integrations",
19774
- channels: "plg_calendar_channels",
19775
- syncLog: "plg_calendar_sync_log"};
19776
-
19777
19723
  // src/integrations/google-calendar/data/supabase.ts
19778
19724
  var FUNCTION = "google-calendar-sync";
19779
19725
  function sb() {
@@ -22622,6 +22568,58 @@ CREATE TRIGGER trg_gcal_push
22622
22568
  COMMENT ON FUNCTION public.fn_gcal_push_booking() IS
22623
22569
  'Pushes a booking to Google Calendar over pg_net, authorised by the vault secret gcal_outbound_secret. Inert while the vault secrets are absent (008).';
22624
22570
  `;
22571
+ var MIGRATION_009_THE_SERVICE_RUNS_THE_SYNC = `-- ============================================================================
22572
+ -- google-calendar 009_the_service_runs_the_sync \u2014 the Google Calendar data
22573
+ -- plane moves to the integration service (ADR 0003 phase 2, wave 3).
22574
+ --
22575
+ -- User actions run AS the verified caller and machine actions (a booking push,
22576
+ -- a webhook pull, watch renewal) AS the store's owner, both under RLS: the
22577
+ -- integrations, channels and sync log already admit a member. What only a
22578
+ -- machine writes gets the service role, table by table, with an explicit
22579
+ -- policy: the webhook delivery log and the automation outbox. And the four
22580
+ -- server-only RPCs the sync calls (002/003: service_role only) get EXECUTE for
22581
+ -- the service role, where they exist on the cluster.
22582
+ --
22583
+ -- Guarded: the role is an operator step, and a cluster may not have the RPCs.
22584
+ -- Replay-safe.
22585
+ -- ============================================================================
22586
+
22587
+ DO $$
22588
+ DECLARE
22589
+ t text;
22590
+ f text;
22591
+ BEGIN
22592
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') THEN
22593
+ RETURN;
22594
+ END IF;
22595
+
22596
+ FOREACH t IN ARRAY ARRAY['plg_calendar_webhook_deliveries', 'plg_calendar_automation_outbox'] LOOP
22597
+ IF to_regclass('public.' || t) IS NULL THEN CONTINUE; END IF;
22598
+ EXECUTE format('GRANT SELECT, INSERT, UPDATE ON public.%I TO fayz_integration', t);
22599
+ EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_service', t);
22600
+ EXECUTE format('CREATE POLICY %I ON public.%I FOR ALL TO fayz_integration USING (true) WITH CHECK (true)', t || '_service', t);
22601
+ END LOOP;
22602
+
22603
+ -- The webhook resolves a channel by its Google channel id before it knows the
22604
+ -- tenant: a read of the channels table, and nothing else on it.
22605
+ IF to_regclass('public.plg_calendar_channels') IS NOT NULL THEN
22606
+ GRANT SELECT ON public.plg_calendar_channels TO fayz_integration;
22607
+ DROP POLICY IF EXISTS plg_calendar_channels_service_read ON public.plg_calendar_channels;
22608
+ CREATE POLICY plg_calendar_channels_service_read ON public.plg_calendar_channels FOR SELECT TO fayz_integration USING (true);
22609
+ END IF;
22610
+
22611
+ FOREACH f IN ARRAY ARRAY[
22612
+ 'public.gcal_import_event(uuid, uuid, text, text, text, timestamptz, timestamptz, boolean, text)',
22613
+ 'public.gcal_apply_event_patch(uuid, text, text, timestamptz, timestamptz, text, boolean)',
22614
+ 'public.gcal_stamp_outbound(uuid, uuid, text, text, text)',
22615
+ 'public.gcal_unlink_channel(uuid)'
22616
+ ] LOOP
22617
+ IF to_regprocedure(f) IS NOT NULL THEN
22618
+ EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO fayz_integration', f);
22619
+ END IF;
22620
+ END LOOP;
22621
+ END $$;
22622
+ `;
22625
22623
 
22626
22624
  // src/integrations/google-calendar/connector.ts
22627
22625
  var googleCalendarConnector = {
@@ -22766,6 +22764,12 @@ function createGoogleCalendarPlugin() {
22766
22764
  version: "0.16.2",
22767
22765
  sql: MIGRATION_008_THE_OUTBOUND_HALF,
22768
22766
  description: "Install trg_gcal_push and the 005 push function on a pool that came up through 007. 007 creates the tables but not the trigger, and the only files that create it (001, 002) also create the credential columns 007 exists to prevent \u2014 so a fresh pool silently had inbound only. Inert until the vault secrets exist"
22767
+ },
22768
+ {
22769
+ id: "google-calendar-009_the_service_runs_the_sync",
22770
+ version: "0.27.0",
22771
+ sql: MIGRATION_009_THE_SERVICE_RUNS_THE_SYNC,
22772
+ description: "Grant the integration service role (ADR 0003 phase 2) the webhook delivery log, the automation outbox, a read of the channels table and EXECUTE on the server-only gcal_* RPCs where they exist, so the sync and its webhook run in the service without a service-role key"
22769
22773
  }
22770
22774
  ]
22771
22775
  };
@@ -23243,6 +23247,6 @@ function createAgendaPlugin(options) {
23243
23247
  };
23244
23248
  }
23245
23249
 
23246
- export { bookingToEvent2 as bookingToEvent, buildAgendaDomainEvents, buildAgendaMessageDefaults, computeCommissionAmount, createAgendaPlugin, createAgendaTurnQueuePlugin, createFinancialBridge, createGoogleCalendarPlugin, createGoogleCalendarProvider, createMockTurnQueueProvider, createSupabaseTurnQueueProvider, eventToBookingPatch, googleCalendarConnector };
23250
+ export { buildAgendaDomainEvents, buildAgendaMessageDefaults, computeCommissionAmount, createAgendaPlugin, createAgendaTurnQueuePlugin, createFinancialBridge, createGoogleCalendarPlugin, createGoogleCalendarProvider, createMockTurnQueueProvider, createSupabaseTurnQueueProvider, googleCalendarConnector };
23247
23251
  //# sourceMappingURL=index.js.map
23248
23252
  //# sourceMappingURL=index.js.map