@fayz-ai/plugin-agenda 0.9.1 → 0.10.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.
Files changed (51) hide show
  1. package/dist/{chunk-76ZOHRBM.js → chunk-ISLJXP7O.js} +7 -2
  2. package/dist/chunk-ISLJXP7O.js.map +1 -0
  3. package/dist/components/AppointmentModal.d.ts.map +1 -1
  4. package/dist/data/mock.d.ts.map +1 -1
  5. package/dist/data/realtime.test.d.ts +2 -0
  6. package/dist/data/realtime.test.d.ts.map +1 -0
  7. package/dist/data/supabase.d.ts.map +1 -1
  8. package/dist/data/types.d.ts +7 -0
  9. package/dist/data/types.d.ts.map +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2349 -184
  12. package/dist/index.js.map +1 -1
  13. package/dist/integrations/google-calendar/conflict.d.ts +25 -0
  14. package/dist/integrations/google-calendar/conflict.d.ts.map +1 -0
  15. package/dist/integrations/google-calendar/connectorDef.d.ts +12 -0
  16. package/dist/integrations/google-calendar/connectorDef.d.ts.map +1 -1
  17. package/dist/integrations/google-calendar/credentials.d.ts +27 -0
  18. package/dist/integrations/google-calendar/credentials.d.ts.map +1 -0
  19. package/dist/integrations/google-calendar/data/supabase.d.ts +10 -2
  20. package/dist/integrations/google-calendar/data/supabase.d.ts.map +1 -1
  21. package/dist/integrations/google-calendar/data/tables.d.ts +2 -0
  22. package/dist/integrations/google-calendar/data/tables.d.ts.map +1 -1
  23. package/dist/integrations/google-calendar/health.d.ts +23 -0
  24. package/dist/integrations/google-calendar/health.d.ts.map +1 -0
  25. package/dist/integrations/google-calendar/health.test.d.ts +2 -0
  26. package/dist/integrations/google-calendar/health.test.d.ts.map +1 -0
  27. package/dist/integrations/google-calendar/index.d.ts +6 -0
  28. package/dist/integrations/google-calendar/index.d.ts.map +1 -1
  29. package/dist/integrations/google-calendar/migrations/index.d.ts +12 -0
  30. package/dist/integrations/google-calendar/migrations/index.d.ts.map +1 -0
  31. package/dist/integrations/google-calendar/panel-data.d.ts +26 -0
  32. package/dist/integrations/google-calendar/panel-data.d.ts.map +1 -0
  33. package/dist/integrations/google-calendar/panel-data.test.d.ts +2 -0
  34. package/dist/integrations/google-calendar/panel-data.test.d.ts.map +1 -0
  35. package/dist/integrations/google-calendar/types.d.ts +17 -0
  36. package/dist/integrations/google-calendar/types.d.ts.map +1 -1
  37. package/dist/lib/booking-events.d.ts +5 -0
  38. package/dist/lib/booking-events.d.ts.map +1 -0
  39. package/dist/lib/calendar-events.d.ts +8 -0
  40. package/dist/lib/calendar-events.d.ts.map +1 -0
  41. package/dist/lib/calendar-events.test.d.ts +2 -0
  42. package/dist/lib/calendar-events.test.d.ts.map +1 -0
  43. package/dist/locales/en.d.ts.map +1 -1
  44. package/dist/locales/pt-BR.d.ts.map +1 -1
  45. package/dist/public/index.js +2 -2
  46. package/dist/store.d.ts.map +1 -1
  47. package/dist/views/CalendarView.d.ts.map +1 -1
  48. package/package.json +8 -7
  49. package/dist/chunk-76ZOHRBM.js.map +0 -1
  50. package/dist/integrations/google-calendar/GoogleCalendarSettings.d.ts +0 -3
  51. package/dist/integrations/google-calendar/GoogleCalendarSettings.d.ts.map +0 -1
@@ -0,0 +1,25 @@
1
+ /** Clock skew below which two edits are treated as simultaneous. */
2
+ export declare const CONFLICT_SKEW_TOLERANCE_MS = 2000;
3
+ export type ConflictWinner = 'local' | 'remote';
4
+ export interface BookingConflictInput {
5
+ /** `appointments.updated_at` — when Fayz last changed the booking. */
6
+ localUpdatedAt?: string | null;
7
+ /** The Google event's `updated` — when Google last changed the event. */
8
+ remoteUpdatedAt?: string | null;
9
+ }
10
+ export interface BookingConflictDecision {
11
+ winner: ConflictWinner;
12
+ /** True when the decision came from the tie-break rather than from the clock. */
13
+ tie: boolean;
14
+ /** Machine-readable reason, written into the sync log alongside the run. */
15
+ reason: 'local_newer' | 'remote_newer' | 'tie_local_wins' | 'no_remote_stamp' | 'no_local_stamp';
16
+ /** The stamp that lost, for the SyncRun record. Undefined when there was no contest. */
17
+ losingUpdatedAt?: string;
18
+ }
19
+ /**
20
+ * Decide which side of a booking↔event pair to keep. Deterministic and
21
+ * symmetric: the same two inputs always produce the same winner, whichever
22
+ * direction the sync is running in.
23
+ */
24
+ export declare function resolveBookingConflict(input: BookingConflictInput): BookingConflictDecision;
25
+ //# sourceMappingURL=conflict.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conflict.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/conflict.ts"],"names":[],"mappings":"AAsBA,oEAAoE;AACpE,eAAO,MAAM,0BAA0B,OAAQ,CAAA;AAE/C,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAA;AAE/C,MAAM,WAAW,oBAAoB;IACnC,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,cAAc,CAAA;IACtB,iFAAiF;IACjF,GAAG,EAAE,OAAO,CAAA;IACZ,4EAA4E;IAC5E,MAAM,EAAE,aAAa,GAAG,cAAc,GAAG,gBAAgB,GAAG,iBAAiB,GAAG,gBAAgB,CAAA;IAChG,wFAAwF;IACxF,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAQD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,oBAAoB,GAAG,uBAAuB,CAe3F"}
@@ -1,3 +1,15 @@
1
1
  import type { ConnectorDefinition } from '@fayz-ai/core';
2
+ export declare const GOOGLE_CALENDAR_CONNECTOR_ID = "google-calendar";
3
+ /**
4
+ * "Is Google Calendar on for this tenant?" — asked of `plg_connections`, the
5
+ * one table every connector answers it from.
6
+ *
7
+ * It used to be asked of `plg_calendar_integrations.token_expires_at`: a
8
+ * timestamp stamped whenever a token was stored, which meant the answer was a
9
+ * by-product of the credential living in this pool. The credential is in the
10
+ * platform vault now, so that column is gone (migration 007) and so is the
11
+ * question it answered.
12
+ */
13
+ export declare const googleCalendarConnectionStore: () => import("@fayz-ai/core").ConnectionStore;
2
14
  export declare const googleCalendarConnectorDef: ConnectorDefinition;
3
15
  //# sourceMappingURL=connectorDef.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"connectorDef.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/connectorDef.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AAyJxD,eAAO,MAAM,0BAA0B,EAAE,mBAsBxC,CAAA"}
1
+ {"version":3,"file":"connectorDef.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/connectorDef.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,mBAAmB,EAAwB,MAAM,eAAe,CAAA;AAY9E,eAAO,MAAM,4BAA4B,oBAAoB,CAAA;AAE7D;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,+CAGtC,CAAA;AA+HJ,eAAO,MAAM,0BAA0B,EAAE,mBA4NxC,CAAA"}
@@ -0,0 +1,27 @@
1
+ import type { CalendarIntegrationRow } from './types';
2
+ /**
3
+ * The columns the browser may read off the integration row. An explicit list,
4
+ * not `*` — `select('*')` is how the refresh token reached the browser in the
5
+ * first place, and a column added later should have to be let in on purpose.
6
+ *
7
+ * There is no credential column left to exclude. The list stays anyway: it is
8
+ * cheap, and the day someone adds one, this is what makes them notice.
9
+ */
10
+ export declare const CONNECTION_COLUMNS = "id, provider, calendar_id, active, last_sync_at";
11
+ /**
12
+ * Whether the calendar side of the integration is switched on.
13
+ *
14
+ * NOT "is the connector connected" — that question belongs to
15
+ * `plg_connections` and is asked through the connection store. This one only
16
+ * says whether the tenant's own sync state row is live, which is what the
17
+ * health rule needs before it complains about a stale cursor.
18
+ */
19
+ export declare function isIntegrationActive(row: Pick<CalendarIntegrationRow, 'active'> | null | undefined): boolean;
20
+ /**
21
+ * The patch that stops the calendar side.
22
+ *
23
+ * It does not revoke anything: the grant is the platform's, and revoking it at
24
+ * Google is the platform's disconnect. This turns off the sync state row.
25
+ */
26
+ export declare function disconnectPatch(): Record<string, unknown>;
27
+ //# sourceMappingURL=credentials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/credentials.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAA;AAErD;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,oDAAoD,CAAA;AAEnF;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,IAAI,CAAC,sBAAsB,EAAE,QAAQ,CAAC,GAAG,IAAI,GAAG,SAAS,GAC7D,OAAO,CAET;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzD"}
@@ -14,8 +14,16 @@ export interface CalendarChannelInput {
14
14
  }
15
15
  export interface GoogleCalendarProvider {
16
16
  getIntegration(): Promise<CalendarIntegration | null>;
17
- /** Start the OAuth connect flow — returns the Google consent URL to redirect to. */
18
- getConnectUrl(redirectTo?: string): Promise<string>;
17
+ /**
18
+ * Make sure this tenant HAS a sync state row, and switch it on.
19
+ *
20
+ * The OAuth callback used to create it, as a side effect of writing the
21
+ * refresh token into it. That callback is the platform's now and knows
22
+ * nothing about this pool's tables, so the row has to be created on the way
23
+ * back from consent — a channel FKs to it, and a de-para with no integration
24
+ * to hang from is the shape of "conectei e não dá para escolher a agenda".
25
+ */
26
+ ensureIntegration(): Promise<void>;
19
27
  setCalendar(calendarId: string): Promise<void>;
20
28
  disconnect(): Promise<void>;
21
29
  /** Manually pull external changes now (inbound). */
@@ -1 +1 @@
1
- {"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../../../src/integrations/google-calendar/data/supabase.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,mBAAmB,EACnB,oBAAoB,EACpB,eAAe,EACf,oBAAoB,EACpB,uBAAuB,EACvB,kBAAkB,EACnB,MAAM,UAAU,CAAA;AAUjB,sEAAsE;AACtE,MAAM,WAAW,oBAAoB;IACnC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gBAAgB,EAAE,MAAM,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,eAAe,CAAC,WAAW,CAAC,CAAA;IACxC,UAAU,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAA;IAC1C,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,UAAU,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAA;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,cAAc,IAAI,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAA;IACrD,oFAAoF;IACpF,aAAa,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACnD,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3B,oDAAoD;IACpD,OAAO,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC7E,UAAU,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAA;IAE7C,0FAA0F;IAC1F,mBAAmB,IAAI,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAAA;IACzD,yCAAyC;IACzC,YAAY,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAAA;IAC1C,mFAAmF;IACnF,WAAW,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvD,oCAAoC;IACpC,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,8EAA8E;IAC9E,kBAAkB,CAAC,MAAM,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAA;IACtG,uDAAuD;IACvD,YAAY,CAAC,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;CAC3F;AAED,wBAAgB,4BAA4B,IAAI,sBAAsB,CAwGrE"}
1
+ {"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../../../src/integrations/google-calendar/data/supabase.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EACV,mBAAmB,EACnB,oBAAoB,EACpB,eAAe,EACf,oBAAoB,EACpB,uBAAuB,EACvB,kBAAkB,EACnB,MAAM,UAAU,CAAA;AA+BjB,sEAAsE;AACtE,MAAM,WAAW,oBAAoB;IACnC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gBAAgB,EAAE,MAAM,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,eAAe,CAAC,WAAW,CAAC,CAAA;IACxC,UAAU,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAA;IAC1C,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,UAAU,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAA;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,cAAc,IAAI,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAA;IACrD;;;;;;;;OAQG;IACH,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3B,oDAAoD;IACpD,OAAO,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC7E,UAAU,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAA;IAE7C,0FAA0F;IAC1F,mBAAmB,IAAI,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAAA;IACzD,yCAAyC;IACzC,YAAY,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAAA;IAC1C,mFAAmF;IACnF,WAAW,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvD,oCAAoC;IACpC,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,8EAA8E;IAC9E,kBAAkB,CAAC,MAAM,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAA;IACtG,uDAAuD;IACvD,YAAY,CAAC,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;CAC3F;AAED,wBAAgB,4BAA4B,IAAI,sBAAsB,CAqHrE"}
@@ -2,5 +2,7 @@ export declare const GCAL_TABLES: {
2
2
  readonly integrations: "plg_calendar_integrations";
3
3
  readonly channels: "plg_calendar_channels";
4
4
  readonly syncLog: "plg_calendar_sync_log";
5
+ readonly webhookDeliveries: "plg_calendar_webhook_deliveries";
6
+ readonly automationOutbox: "plg_calendar_automation_outbox";
5
7
  };
6
8
  //# sourceMappingURL=tables.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../../../src/integrations/google-calendar/data/tables.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,WAAW;;;;CAId,CAAA"}
1
+ {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../../../src/integrations/google-calendar/data/tables.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,WAAW;;;;;;CAMd,CAAA"}
@@ -0,0 +1,23 @@
1
+ import type { CalendarChannel, CalendarSyncLogEntry } from './types';
2
+ export type GoogleCalendarHealthStatus = 'disconnected' | 'healthy' | 'attention' | 'error';
3
+ export type GoogleCalendarHealthIssueCode = 'sync_error' | 'sync_missing' | 'sync_stale' | 'inbound_channel_missing' | 'watch_missing' | 'watch_expiring' | 'timezone_mismatch';
4
+ export interface GoogleCalendarHealthIssue {
5
+ code: GoogleCalendarHealthIssueCode;
6
+ detail?: string;
7
+ }
8
+ export interface GoogleCalendarHealth {
9
+ status: GoogleCalendarHealthStatus;
10
+ issues: GoogleCalendarHealthIssue[];
11
+ }
12
+ export declare function deriveGoogleCalendarHealth(input: {
13
+ connected: boolean;
14
+ lastSyncAt?: string;
15
+ logs: CalendarSyncLogEntry[];
16
+ channels: CalendarChannel[];
17
+ calendarTimeZones?: string[];
18
+ localTimeZone?: string;
19
+ now?: number;
20
+ staleAfterMs?: number;
21
+ renewBeforeMs?: number;
22
+ }): GoogleCalendarHealth;
23
+ //# sourceMappingURL=health.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/health.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAEpE,MAAM,MAAM,0BAA0B,GAAG,cAAc,GAAG,SAAS,GAAG,WAAW,GAAG,OAAO,CAAA;AAE3F,MAAM,MAAM,6BAA6B,GACrC,YAAY,GACZ,cAAc,GACd,YAAY,GACZ,yBAAyB,GACzB,eAAe,GACf,gBAAgB,GAChB,mBAAmB,CAAA;AAEvB,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,6BAA6B,CAAA;IACnC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,0BAA0B,CAAA;IAClC,MAAM,EAAE,yBAAyB,EAAE,CAAA;CACpC;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IAChD,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,oBAAoB,EAAE,CAAA;IAC5B,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,GAAG,oBAAoB,CAwDvB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=health.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.test.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/health.test.ts"],"names":[],"mappings":""}
@@ -2,6 +2,12 @@ import type { PluginManifest } from '@fayz-ai/core';
2
2
  export { createGoogleCalendarProvider } from './data/supabase';
3
3
  export { googleCalendarConnector } from './connector';
4
4
  export { googleCalendarConnectorDef } from './connectorDef';
5
+ export { deriveGoogleCalendarHealth } from './health';
6
+ export type { GoogleCalendarHealth, GoogleCalendarHealthIssue, GoogleCalendarHealthIssueCode, GoogleCalendarHealthStatus } from './health';
7
+ export { resolveBookingConflict, CONFLICT_SKEW_TOLERANCE_MS } from './conflict';
8
+ export type { BookingConflictInput, BookingConflictDecision, ConflictWinner } from './conflict';
9
+ export { CONNECTION_COLUMNS, isIntegrationActive, disconnectPatch } from './credentials';
10
+ export { googleCalendarConnectionStore, GOOGLE_CALENDAR_CONNECTOR_ID } from './connectorDef';
5
11
  export { bookingToEvent, eventToBookingPatch, bookingSummary, appointmentRowToEvent, eventToAppointmentPatch, eventTimes, resolveTargetChannel, channelRowToChannel, } from './mapping';
6
12
  export * from './types';
7
13
  export declare function createGoogleCalendarPlugin(): PluginManifest;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAGnD,OAAO,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAA;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACvB,UAAU,EACV,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,WAAW,CAAA;AAClB,cAAc,SAAS,CAAA;AAKvB,wBAAgB,0BAA0B,IAAI,cAAc,CAa3D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAYnD,OAAO,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAA;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAA;AACrD,YAAY,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,6BAA6B,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAA;AAC1I,OAAO,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAA;AAC/E,YAAY,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC/F,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACxF,OAAO,EAAE,6BAA6B,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAA;AAC5F,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACvB,UAAU,EACV,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,WAAW,CAAA;AAClB,cAAc,SAAS,CAAA;AAKvB,wBAAgB,0BAA0B,IAAI,cAAc,CAgI3D"}
@@ -0,0 +1,12 @@
1
+ export declare const MIGRATION_001_GOOGLE_CALENDAR = "-- ---------------------------------------------------------------------------\n-- Google Calendar integration (bidirectional) \u2014 connection + audit + triggers\n-- ---------------------------------------------------------------------------\n-- Outbound (Fayz booking \u2192 Google event) fires from a trigger on public.appointments\n-- via pg_net to the google-calendar-sync edge function (push_event). Inbound\n-- (Google \u2192 booking) runs on a pg_cron schedule (pull_events) and/or a Google\n-- watch-channel webhook. The booking\u2194event link is stored on\n-- public.appointments.metadata.googleCalendarEventId (no schema change there).\n\n-- Plugin-owned tables carry the platform `plg_<plugin>_` prefix (DATA-MODEL.md\n-- Ring 1). Legacy pools provisioned under the old `calendar_*` names are migrated\n-- by 003_plg_rename.sql; fresh installs are born with the prefixed names here.\nCREATE TABLE IF NOT EXISTS public.plg_calendar_integrations (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n provider text NOT NULL DEFAULT 'google',\n oauth_refresh_token text,\n oauth_access_token text,\n token_expires_at timestamptz,\n calendar_id text NOT NULL DEFAULT 'primary',\n -- Incremental pull cursor + Google watch channel bookkeeping.\n sync_token text,\n channel_id text,\n channel_resource_id text,\n channel_expires_at timestamptz,\n active boolean NOT NULL DEFAULT true,\n last_sync_at timestamptz,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (tenant_id, provider)\n);\nALTER TABLE public.plg_calendar_integrations ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_calendar_integrations_tenant ON public.plg_calendar_integrations(tenant_id);\n\nCREATE TABLE IF NOT EXISTS public.plg_calendar_sync_log (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n direction text NOT NULL, -- inbound | outbound\n trigger text, -- on-write | scheduled | webhook | manual\n status text NOT NULL DEFAULT 'success', -- success | partial | error\n fetched integer NOT NULL DEFAULT 0,\n written integer NOT NULL DEFAULT 0,\n error text,\n created_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_calendar_sync_log ENABLE ROW LEVEL SECURITY;\n\n-- RLS: tenant isolation.\nDO $$\nDECLARE t text;\nBEGIN\n FOR t IN SELECT unnest(ARRAY['plg_calendar_integrations','plg_calendar_sync_log'])\n LOOP\n EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO authenticated', t);\n IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_rls') THEN\n EXECUTE format('CREATE POLICY %I ON public.%I FOR ALL TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids())) WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_rls', t);\n END IF;\n END LOOP;\nEND $$;\n\n-- ---------------------------------------------------------------------------\n-- Outbound trigger: booking change \u2192 edge function (push_event).\n-- Requires pg_net + a configured function URL. Set once per project:\n-- ALTER DATABASE postgres SET app.gcal_sync_url = 'https://<ref>.functions.supabase.co/google-calendar-sync';\n-- ALTER DATABASE postgres SET app.gcal_service_key = '<service-role-key>';\n-- Safe to skip in environments without pg_net \u2014 outbound also works via the\n-- client-side withGoogleCalendarSync() decorator.\n-- ---------------------------------------------------------------------------\nCREATE OR REPLACE FUNCTION public.fn_gcal_push_booking() RETURNS trigger\nLANGUAGE plpgsql SECURITY DEFINER AS $$\nDECLARE\n url text := current_setting('app.gcal_sync_url', true);\n key text := current_setting('app.gcal_service_key', true);\n rec record := COALESCE(NEW, OLD);\n op text := lower(TG_OP);\nBEGIN\n IF url IS NULL OR url = '' THEN RETURN COALESCE(NEW, OLD); END IF;\n PERFORM net.http_post(\n url := url,\n headers := jsonb_build_object('Content-Type', 'application/json', 'Authorization', 'Bearer ' || COALESCE(key, '')),\n body := jsonb_build_object('action', 'push_event', 'op', op, 'bookingId', rec.id, 'tenantId', rec.tenant_id)\n );\n RETURN COALESCE(NEW, OLD);\nEND $$;\n\nDROP TRIGGER IF EXISTS trg_gcal_push ON public.appointments;\nCREATE TRIGGER trg_gcal_push\n AFTER INSERT OR UPDATE OR DELETE ON public.appointments\n FOR EACH ROW EXECUTE FUNCTION public.fn_gcal_push_booking();\n\n-- Inbound schedule (optional): pull external changes every 10 minutes.\n-- SELECT cron.schedule('gcal-pull', '*/10 * * * *', $$\n-- SELECT net.http_post(\n-- url := current_setting('app.gcal_sync_url'),\n-- headers := jsonb_build_object('Content-Type','application/json','Authorization','Bearer '||current_setting('app.gcal_service_key')),\n-- body := jsonb_build_object('action','pull_events'));\n-- $$);\n";
2
+ export declare const MIGRATION_002_SMART_SYNC = "-- ---------------------------------------------------------------------------\n-- Google Calendar SMART SYNC \u2014 multi-calendar de-para + loop-safe two-way sync\n-- ---------------------------------------------------------------------------\n-- Builds on 001_google_calendar.sql. Where 001 modelled a single calendar per\n-- tenant (plg_calendar_integrations, one outbound trigger, a pull template), 002\n-- makes the sync vertical-agnostic and bidirectional-safe:\n--\n-- \u00A71 public.plg_calendar_channels \u2014 de-para: one Google calendar \u2192 one sync\n-- target (whole tenant / assignee / service\n-- / location), per direction + import mode\n-- \u00A72 fn_gcal_push_booking (REPLACE)\u2014 loop-prevention guard + correlation id\n-- \u00A73 plg_calendar_sync_log \u2014 correlation_id + channel_id_ref columns\n-- \u00A74 inbound write functions \u2014 the ONLY path Google\u2192booking writes take\n-- (gcal_import_event / gcal_apply_event_patch\n-- / gcal_unlink_channel), SECURITY DEFINER,\n-- service_role-only\n-- \u00A75 get_available_slots (REPLACE) \u2014 tenant-wide imported blocks now also\n-- remove slots for every professional\n-- \u00A76 pg_cron template \u2014 per-channel pull\n--\n-- Idempotent / re-runnable: IF NOT EXISTS, CREATE OR REPLACE, DROP \u2026 IF EXISTS.\n-- Written against the canonical public names (appointments uses starts_at/ends_at,\n-- metadata jsonb, party_id/assignee_id \u2014 see packages/db 004_archetypes.sql).\n-- ---------------------------------------------------------------------------\n\n-- ===========================================================================\n-- \u00A71 \u2014 plg_calendar_channels: multi-calendar de-para (vertical-agnostic)\n-- target_kind NULL \u2192 whole tenant agenda (agency-os): every professional\n-- target_kind 'assignee' \u2192 calendar maps to one professional (school: teacher)\n-- target_kind 'service' | 'location' \u2192 clinic-style mapping\n-- import_mode 'block' \u2192 inbound events become opaque busy blocks\n-- import_mode 'appointment' \u2192 inbound events become real appointments\n-- ===========================================================================\nCREATE TABLE IF NOT EXISTS public.plg_calendar_channels (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n integration_id uuid NOT NULL REFERENCES public.plg_calendar_integrations(id) ON DELETE CASCADE,\n tenant_id uuid NOT NULL,\n google_calendar_id text NOT NULL,\n summary text,\n color text,\n direction text NOT NULL DEFAULT 'bidirectional'\n CHECK (direction IN ('inbound','outbound','bidirectional','off')),\n target_kind text CHECK (target_kind IN ('assignee','service','location')),\n target_id uuid,\n import_mode text NOT NULL DEFAULT 'block'\n CHECK (import_mode IN ('block','appointment')),\n sync_token text,\n channel_id text,\n resource_id text,\n channel_expires_at timestamptz,\n is_active boolean NOT NULL DEFAULT true,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (integration_id, google_calendar_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_plg_calendar_channels_tenant ON public.plg_calendar_channels(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_plg_calendar_channels_integration ON public.plg_calendar_channels(integration_id);\n\nALTER TABLE public.plg_calendar_channels ENABLE ROW LEVEL SECURITY;\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_calendar_channels TO authenticated;\n-- service_role bypasses RLS, but this table is born after 004's blanket grant,\n-- so grant it explicitly (the inbound functions run as definer/service_role).\nGRANT ALL ON public.plg_calendar_channels TO service_role;\n\nDROP TRIGGER IF EXISTS plg_calendar_channels_updated_at ON public.plg_calendar_channels;\nCREATE TRIGGER plg_calendar_channels_updated_at BEFORE UPDATE ON public.plg_calendar_channels\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\n-- RLS: tenant isolation (padr\u00E3o 001) + explicit service_role FOR ALL policy\n-- (matches the vigente service_role policy pattern from 001_core payment_events).\nDROP POLICY IF EXISTS plg_calendar_channels_rls ON public.plg_calendar_channels;\nCREATE POLICY plg_calendar_channels_rls ON public.plg_calendar_channels\n FOR ALL TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()))\n WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\n\nDROP POLICY IF EXISTS plg_calendar_channels_service ON public.plg_calendar_channels;\nCREATE POLICY plg_calendar_channels_service ON public.plg_calendar_channels\n FOR ALL TO service_role USING (true) WITH CHECK (true);\n\n-- ===========================================================================\n-- \u00A72 \u2014 Outbound trigger with loop-prevention + correlation id.\n-- CREATE OR REPLACE of 001's fn_gcal_push_booking. Same pg_net \u2192 edge function\n-- payload (app.gcal_sync_url / app.gcal_service_key), but now:\n-- * SKIP the push when the write itself came from the Google sync, detected by\n-- the TRANSACTION-LOCAL GUC app.booking_origin = 'google' \u2014 set by every \u00A74\n-- writer via set_config(..., true) and gone when the transaction ends.\n-- This is what stops the Google\u2192booking\u2192Google\u2192\u2026 echo loop.\n-- Deliberately NOT a persistent metadata check: a sticky marker\n-- (metadata.syncOrigin) would permanently mute pushes for imported events,\n-- so a later admin edit would never reach Google. metadata.syncOrigin is\n-- kept by \u00A74 as provenance only \u2014 this trigger does not read it.\n-- * carry a correlation_id so both directions can be stitched in the sync log\n-- (from GUC app.booking_correlation_id, else a fresh uuid \u2014 design ported\n-- from PR #7).\n-- ===========================================================================\nCREATE OR REPLACE FUNCTION public.fn_gcal_push_booking() RETURNS trigger\nLANGUAGE plpgsql SECURITY DEFINER AS $$\nDECLARE\n url text := current_setting('app.gcal_sync_url', true);\n key text := current_setting('app.gcal_service_key', true);\n rec record := COALESCE(NEW, OLD);\n op text := lower(TG_OP);\n corr text := NULLIF(current_setting('app.booking_correlation_id', true), '');\nBEGIN\n -- Loop guard: never bounce a Google-originated write back to Google.\n -- Transaction-local GUC only (see \u00A72 header for why not persistent metadata).\n IF current_setting('app.booking_origin', true) = 'google' THEN\n RETURN COALESCE(NEW, OLD);\n END IF;\n\n IF url IS NULL OR url = '' THEN RETURN COALESCE(NEW, OLD); END IF;\n\n PERFORM net.http_post(\n url := url,\n headers := jsonb_build_object('Content-Type', 'application/json',\n 'Authorization', 'Bearer ' || COALESCE(key, '')),\n body := jsonb_build_object(\n 'action', 'push_event',\n 'op', op,\n 'bookingId', rec.id,\n 'tenantId', rec.tenant_id,\n 'correlationId', COALESCE(corr, gen_random_uuid()::text)\n )\n );\n RETURN COALESCE(NEW, OLD);\nEND $$;\n\n-- Re-assert the trigger (idempotent; unchanged from 001).\nDROP TRIGGER IF EXISTS trg_gcal_push ON public.appointments;\nCREATE TRIGGER trg_gcal_push\n AFTER INSERT OR UPDATE OR DELETE ON public.appointments\n FOR EACH ROW EXECUTE FUNCTION public.fn_gcal_push_booking();\n\n-- ===========================================================================\n-- \u00A73 \u2014 sync log: correlate the two directions + reference the source channel.\n-- ===========================================================================\nALTER TABLE public.plg_calendar_sync_log\n ADD COLUMN IF NOT EXISTS correlation_id uuid;\nALTER TABLE public.plg_calendar_sync_log\n ADD COLUMN IF NOT EXISTS channel_id_ref uuid\n REFERENCES public.plg_calendar_channels(id) ON DELETE SET NULL;\n-- Unlinked future events seen during a pull (import-wizard candidates), so the\n-- log can show \"N eventos aguardando import\" without re-hitting Google.\nALTER TABLE public.plg_calendar_sync_log\n ADD COLUMN IF NOT EXISTS discovered integer;\n\n-- ===========================================================================\n-- \u00A74 \u2014 Inbound write path (Google \u2192 booking). THE single entry point used by\n-- the edge function (scheduled pull, webhook, and the import wizard). All three\n-- functions are SECURITY DEFINER with SET search_path = public and are granted\n-- to service_role ONLY (anon/authenticated explicitly revoked) \u2014 they bypass the\n-- tenant RLS on purpose and must never be reachable from the anon/authenticated\n-- PostgREST surface. Every writer sets the TRANSACTION-LOCAL GUC\n-- app.booking_origin='google' (set_config ..., true) so \u00A72's trigger suppresses\n-- the echo for THIS write only; metadata.syncOrigin is provenance, not a guard.\n-- ===========================================================================\n\n-- gcal_import_event: create (or idempotently update) a booking from a Google\n-- event, honouring the channel's import_mode + target mapping.\n-- kind = 'block' | 'appointment' (from channel.import_mode)\n-- assignee_id = channel.target_id when target_kind='assignee', else NULL\n-- (NULL = tenant-wide \u2192 blocks every professional, see \u00A75)\n-- location_id = channel.target_id when target_kind='location', else NULL\n-- status = 'scheduled'\n-- No order is created \u2014 an external import is not a sale. Idempotent on\n-- (tenant_id, metadata.googleCalendarEventId): update in place if seen before.\nCREATE OR REPLACE FUNCTION public.gcal_import_event(\n p_tenant_id uuid,\n p_channel_id uuid,\n p_event_id text,\n p_etag text,\n p_summary text,\n p_starts_at timestamptz,\n p_ends_at timestamptz,\n p_all_day boolean,\n p_description text DEFAULT NULL\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path = public\nAS $$\nDECLARE\n v_ch record;\n v_kind text;\n v_assignee uuid;\n v_location uuid;\n v_ends timestamptz;\n v_meta jsonb;\n v_existing uuid;\n v_appt uuid;\nBEGIN\n IF p_tenant_id IS NULL OR p_event_id IS NULL OR p_starts_at IS NULL THEN\n RAISE EXCEPTION 'gcal_import_event: tenant, event id and start are required';\n END IF;\n\n -- Transaction-local: \u00A72's trigger skips the echo for this write only.\n PERFORM set_config('app.booking_origin', 'google', true);\n\n -- Resolve the channel mapping (must belong to the same tenant).\n SELECT c.import_mode, c.target_kind, c.target_id\n INTO v_ch\n FROM plg_calendar_channels c\n WHERE c.id = p_channel_id AND c.tenant_id = p_tenant_id;\n IF NOT FOUND THEN RAISE EXCEPTION 'gcal_import_event: unknown channel for tenant'; END IF;\n\n v_kind := CASE WHEN v_ch.import_mode = 'appointment' THEN 'appointment' ELSE 'block' END;\n v_assignee := CASE WHEN v_ch.target_kind = 'assignee' THEN v_ch.target_id ELSE NULL END;\n v_location := CASE WHEN v_ch.target_kind = 'location' THEN v_ch.target_id ELSE NULL END;\n\n -- All-day events without an explicit end block the whole day so \u00A75 excludes it.\n v_ends := COALESCE(\n p_ends_at,\n CASE WHEN COALESCE(p_all_day, false) THEN p_starts_at + interval '1 day' ELSE p_starts_at END\n );\n\n v_meta := jsonb_build_object(\n 'syncOrigin', 'google',\n 'googleCalendarEventId', p_event_id,\n 'gcalEtag', p_etag,\n 'gcalChannelId', p_channel_id::text,\n 'allDay', COALESCE(p_all_day, false),\n 'title', p_summary,\n 'source', 'google_import'\n );\n\n -- Idempotency: has this event already been imported for the tenant?\n SELECT b.id INTO v_existing\n FROM appointments b\n WHERE b.tenant_id = p_tenant_id\n AND b.metadata->>'googleCalendarEventId' = p_event_id\n LIMIT 1;\n\n IF v_existing IS NOT NULL THEN\n UPDATE appointments b\n SET kind = v_kind,\n assignee_id = v_assignee,\n location_id = v_location,\n starts_at = p_starts_at,\n ends_at = v_ends,\n status = 'scheduled',\n notes = COALESCE(p_description, p_summary),\n metadata = COALESCE(b.metadata, '{}'::jsonb) || v_meta,\n updated_at = now()\n WHERE b.id = v_existing\n RETURNING b.id INTO v_appt;\n ELSE\n INSERT INTO appointments (tenant_id, kind, assignee_id, location_id,\n starts_at, ends_at, status, notes, metadata)\n VALUES (p_tenant_id, v_kind, v_assignee, v_location,\n p_starts_at, v_ends, 'scheduled', COALESCE(p_description, p_summary), v_meta)\n RETURNING id INTO v_appt;\n END IF;\n\n RETURN v_appt;\nEND $$;\n\n-- gcal_apply_event_patch: apply an inbound reschedule/cancel to an already-linked\n-- booking. Located by metadata.googleCalendarEventId. Echo suppression: if the\n-- stored gcalEtag already equals the incoming etag, this is our own write coming\n-- back \u2014 skip and return NULL. Every real write re-stamps syncOrigin='google'.\nCREATE OR REPLACE FUNCTION public.gcal_apply_event_patch(\n p_tenant_id uuid,\n p_event_id text,\n p_etag text,\n p_starts_at timestamptz,\n p_ends_at timestamptz,\n p_summary text,\n p_cancelled boolean\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path = public\nAS $$\nDECLARE\n v_appt uuid;\n v_curr_etag text;\nBEGIN\n IF p_tenant_id IS NULL OR p_event_id IS NULL THEN\n RAISE EXCEPTION 'gcal_apply_event_patch: tenant and event id are required';\n END IF;\n\n -- Transaction-local: \u00A72's trigger skips the echo for this write only.\n PERFORM set_config('app.booking_origin', 'google', true);\n\n SELECT b.id, b.metadata->>'gcalEtag'\n INTO v_appt, v_curr_etag\n FROM appointments b\n WHERE b.tenant_id = p_tenant_id\n AND b.metadata->>'googleCalendarEventId' = p_event_id\n LIMIT 1;\n\n IF v_appt IS NULL THEN RETURN NULL; END IF; -- not linked here\n IF p_etag IS NOT NULL AND p_etag = v_curr_etag THEN\n RETURN NULL; -- echo: nothing changed\n END IF;\n\n IF COALESCE(p_cancelled, false) THEN\n UPDATE appointments b\n SET status = 'cancelled',\n metadata = COALESCE(b.metadata, '{}'::jsonb)\n || jsonb_build_object('syncOrigin', 'google', 'gcalEtag', p_etag),\n updated_at = now()\n WHERE b.id = v_appt;\n ELSE\n UPDATE appointments b\n SET starts_at = COALESCE(p_starts_at, b.starts_at),\n ends_at = COALESCE(p_ends_at, b.ends_at),\n notes = COALESCE(p_summary, b.notes),\n metadata = COALESCE(b.metadata, '{}'::jsonb)\n || jsonb_build_object('syncOrigin', 'google',\n 'gcalEtag', p_etag,\n 'title', COALESCE(p_summary, b.metadata->>'title')),\n updated_at = now()\n WHERE b.id = v_appt;\n END IF;\n\n RETURN v_appt;\nEND $$;\n\n-- gcal_stamp_outbound: after the edge function creates the Google event for an\n-- outbound push, it stamps the fresh event id/etag back onto the appointment.\n-- Bookkeeping only \u2014 must not re-fire \u00A72's trigger, hence the GUC. Does NOT set\n-- metadata.syncOrigin (the booking's provenance is still the app/site).\nCREATE OR REPLACE FUNCTION public.gcal_stamp_outbound(\n p_tenant_id uuid,\n p_booking_id uuid,\n p_event_id text,\n p_etag text DEFAULT NULL,\n p_calendar_id text DEFAULT NULL\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path = public\nAS $$\nDECLARE\n v_id uuid;\nBEGIN\n IF p_tenant_id IS NULL OR p_booking_id IS NULL OR p_event_id IS NULL THEN\n RAISE EXCEPTION 'gcal_stamp_outbound: tenant, booking and event id are required';\n END IF;\n\n -- Transaction-local: \u00A72's trigger skips the echo for this write only.\n PERFORM set_config('app.booking_origin', 'google', true);\n\n UPDATE appointments b\n SET metadata = COALESCE(b.metadata, '{}'::jsonb)\n || jsonb_build_object('googleCalendarEventId', p_event_id,\n 'gcalEtag', p_etag,\n 'gcalSyncedAt', now())\n || CASE WHEN p_calendar_id IS NULL THEN '{}'::jsonb\n ELSE jsonb_build_object('gcalCalendarId', p_calendar_id) END,\n updated_at = now()\n WHERE b.id = p_booking_id AND b.tenant_id = p_tenant_id\n RETURNING b.id INTO v_id;\n RETURN v_id;\nEND $$;\n\n-- gcal_unlink_channel: forget the incremental cursor + Google watch bookkeeping\n-- for a channel (used on disconnect / re-watch). Leaves the mapping row in place.\nCREATE OR REPLACE FUNCTION public.gcal_unlink_channel(\n p_channel_id uuid\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path = public\nAS $$\nDECLARE\n v_id uuid;\nBEGIN\n UPDATE plg_calendar_channels c\n SET sync_token = NULL,\n channel_id = NULL,\n resource_id = NULL,\n channel_expires_at = NULL,\n updated_at = now()\n WHERE c.id = p_channel_id\n RETURNING c.id INTO v_id;\n RETURN v_id;\nEND $$;\n\n-- Lock the inbound writers to service_role only.\nDO $$\nDECLARE fn text;\nBEGIN\n FOR fn IN SELECT unnest(ARRAY[\n 'public.gcal_import_event(uuid, uuid, text, text, text, timestamptz, timestamptz, boolean, text)',\n 'public.gcal_apply_event_patch(uuid, text, text, timestamptz, timestamptz, text, boolean)',\n 'public.gcal_stamp_outbound(uuid, uuid, text, text, text)',\n 'public.gcal_unlink_channel(uuid)'\n ])\n LOOP\n EXECUTE format('REVOKE ALL ON FUNCTION %s FROM public, anon, authenticated', fn);\n EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', fn);\n END LOOP;\nEND $$;\n\n-- ===========================================================================\n-- \u00A75 \u2014 get_available_slots: imported Google blocks must remove availability.\n-- CREATE OR REPLACE keeping the EXACT signature (anon + admin both call it, and\n-- it is graded on the same shape as plugin-agenda/src/migrations/001_public_booking.sql).\n--\n-- Conceptual diff vs 001 \u2014 ONE condition in the overlap NOT EXISTS:\n-- before: AND b.assignee_id = c.assignee_id\n-- after: AND (b.assignee_id = c.assignee_id\n-- OR (b.assignee_id IS NULL AND b.kind = 'block'))\n-- Rationale: a tenant-wide BLOCK imported from Google (target_kind NULL \u2192\n-- assignee_id IS NULL, kind 'block', see \u00A74) must busy-out the slot for EVERY\n-- professional. Restricted to kind='block' so an unassigned admin appointment\n-- keeps the 001 behaviour (blocks nobody). Per-assignee rows keep matching on\n-- assignee_id.\n-- Everything else is byte-identical to 001: the status filter already covers\n-- \"any non-cancelled kind\" (no_show also excluded), and the p_assignee_id-NULL\n-- fan-out across staff via day_schedules is preserved untouched.\n-- ===========================================================================\nCREATE OR REPLACE FUNCTION public.get_available_slots(\n p_tenant_id uuid,\n p_date date,\n p_duration_minutes integer,\n p_assignee_id uuid DEFAULT NULL,\n p_slot_interval integer DEFAULT 30\n) RETURNS TABLE (slot_start timestamptz, slot_end timestamptz)\nLANGUAGE plpgsql STABLE SECURITY DEFINER\nSET search_path = public\nAS $$\nDECLARE\n v_tz text;\nBEGIN\n -- tight validation: this function is anon-callable\n IF p_duration_minutes IS NULL OR p_duration_minutes NOT BETWEEN 5 AND 480 THEN\n RAISE EXCEPTION 'invalid duration';\n END IF;\n IF p_slot_interval IS NULL OR p_slot_interval NOT BETWEEN 5 AND 240 THEN\n RAISE EXCEPTION 'invalid interval';\n END IF;\n IF p_date IS NULL OR p_date < current_date OR p_date > current_date + 90 THEN\n RETURN;\n END IF;\n\n SELECT COALESCE(t.settings->>'timezone', 'America/Sao_Paulo') INTO v_tz\n FROM tenants t WHERE t.id = p_tenant_id;\n IF v_tz IS NULL THEN RETURN; END IF; -- unknown tenant \u2192 empty, not an error\n\n RETURN QUERY\n WITH day_schedules AS (\n SELECT sc.assignee_id, sc.starts_at AS win_start, sc.ends_at AS win_end\n FROM schedules sc\n WHERE sc.tenant_id = p_tenant_id\n AND sc.kind = 'working_hours'\n AND sc.is_active\n AND (p_assignee_id IS NULL OR sc.assignee_id = p_assignee_id)\n AND (\n sc.specific_date = p_date\n OR (sc.specific_date IS NULL AND sc.day_of_week = EXTRACT(dow FROM p_date)::smallint)\n )\n ),\n candidate AS (\n SELECT\n ds.assignee_id,\n gs AS s_start,\n gs + make_interval(mins => p_duration_minutes) AS s_end\n FROM day_schedules ds\n CROSS JOIN LATERAL generate_series(\n (p_date + ds.win_start) AT TIME ZONE v_tz,\n ((p_date + ds.win_end) AT TIME ZONE v_tz) - make_interval(mins => p_duration_minutes),\n make_interval(mins => p_slot_interval)\n ) AS gs\n )\n SELECT DISTINCT c.s_start, c.s_end\n FROM candidate c\n WHERE NOT EXISTS (\n SELECT 1 FROM appointments b\n WHERE b.tenant_id = p_tenant_id\n -- SMART SYNC: match this professional's own bookings OR any tenant-wide\n -- BLOCK (assignee_id IS NULL AND kind='block', e.g. imported from Google\n -- with a whole-tenant channel) \u2014 the latter busies the slot for every\n -- professional. Assignee-less rows of other kinds (an unassigned admin\n -- appointment) keep the 001 behaviour and do not block anyone.\n AND (b.assignee_id = c.assignee_id\n OR (b.assignee_id IS NULL AND b.kind = 'block'))\n AND b.status NOT IN ('cancelled', 'no_show')\n AND b.starts_at < c.s_end\n AND COALESCE(b.ends_at, b.starts_at) > c.s_start\n )\n ORDER BY 1;\nEND;\n$$;\n\nREVOKE ALL ON FUNCTION public.get_available_slots(uuid, date, integer, uuid, integer) FROM public;\nGRANT EXECUTE ON FUNCTION public.get_available_slots(uuid, date, integer, uuid, integer)\n TO anon, authenticated, service_role;\n\n-- ===========================================================================\n-- \u00A76 \u2014 Inbound schedule (optional) \u2014 per-channel incremental pull.\n-- Supersedes the single-calendar template in 001. Enable once per project after\n-- setting app.gcal_sync_url / app.gcal_service_key (see 001 header). The edge\n-- function fans out over active channels, using each channel's sync_token cursor:\n--\n-- SELECT cron.schedule('gcal-pull', '*/10 * * * *', $CRON$\n-- SELECT net.http_post(\n-- url := current_setting('app.gcal_sync_url'),\n-- headers := jsonb_build_object(\n-- 'Content-Type','application/json',\n-- 'Authorization','Bearer '||current_setting('app.gcal_service_key')),\n-- body := jsonb_build_object('action','pull_events'))\n-- $CRON$);\n--\n-- Or drive one row per channel from SQL (tighter scoping / per-channel cadence):\n--\n-- SELECT cron.schedule('gcal-pull-'||ch.id::text, '*/10 * * * *', format($CRON$\n-- SELECT net.http_post(\n-- url := current_setting('app.gcal_sync_url'),\n-- headers := jsonb_build_object(\n-- 'Content-Type','application/json',\n-- 'Authorization','Bearer '||current_setting('app.gcal_service_key')),\n-- body := jsonb_build_object('action','pull_events','channelId',%L))\n-- $CRON$, ch.id))\n-- FROM public.plg_calendar_channels ch\n-- WHERE ch.is_active AND ch.direction IN ('inbound','bidirectional');\n-- ===========================================================================\n";
3
+ export declare const MIGRATION_003_PLG_RENAME = "-- ---------------------------------------------------------------------------\n-- 003_plg_rename.sql \u2014 standardize the Google Calendar connector objects onto\n-- the platform `plg_<plugin>_` prefix (DATA-MODEL.md Ring 1).\n--\n-- calendar_integrations \u2192 plg_calendar_integrations\n-- calendar_channels \u2192 plg_calendar_channels\n-- calendar_sync_log \u2192 plg_calendar_sync_log\n--\n-- Legacy-pool remediation ONLY: 001/002 now create the prefixed names directly,\n-- so fresh installs never reach a rename branch. Fully idempotent and guarded \u2014\n-- every RENAME fires only when the old object exists and the new one does not,\n-- so re-running (or running on an already-prefixed pool) is a no-op.\n--\n-- A table RENAME preserves its data, RLS state, policies, grants, indexes,\n-- constraints and triggers (they follow the table), so \u00A71 only renames the\n-- table + the explicitly-named indexes. \u00A72 then re-points the SECURITY DEFINER\n-- gcal_* functions whose bodies hard-code the old table names.\n-- ---------------------------------------------------------------------------\n\n-- ===========================================================================\n-- \u00A71 \u2014 Guarded table + index renames.\n-- ===========================================================================\nDO $$\nBEGIN\n IF to_regclass('public.calendar_integrations') IS NOT NULL\n AND to_regclass('public.plg_calendar_integrations') IS NULL THEN\n ALTER TABLE public.calendar_integrations RENAME TO plg_calendar_integrations;\n END IF;\n\n IF to_regclass('public.calendar_channels') IS NOT NULL\n AND to_regclass('public.plg_calendar_channels') IS NULL THEN\n ALTER TABLE public.calendar_channels RENAME TO plg_calendar_channels;\n END IF;\n\n IF to_regclass('public.calendar_sync_log') IS NOT NULL\n AND to_regclass('public.plg_calendar_sync_log') IS NULL THEN\n ALTER TABLE public.calendar_sync_log RENAME TO plg_calendar_sync_log;\n END IF;\n\n -- Named indexes do not follow a table rename automatically.\n IF to_regclass('public.idx_calendar_integrations_tenant') IS NOT NULL\n AND to_regclass('public.idx_plg_calendar_integrations_tenant') IS NULL THEN\n ALTER INDEX public.idx_calendar_integrations_tenant RENAME TO idx_plg_calendar_integrations_tenant;\n END IF;\n\n IF to_regclass('public.idx_calendar_channels_tenant') IS NOT NULL\n AND to_regclass('public.idx_plg_calendar_channels_tenant') IS NULL THEN\n ALTER INDEX public.idx_calendar_channels_tenant RENAME TO idx_plg_calendar_channels_tenant;\n END IF;\n\n IF to_regclass('public.idx_calendar_channels_integration') IS NOT NULL\n AND to_regclass('public.idx_plg_calendar_channels_integration') IS NULL THEN\n ALTER INDEX public.idx_calendar_channels_integration RENAME TO idx_plg_calendar_channels_integration;\n END IF;\nEND $$;\n\n-- ===========================================================================\n-- \u00A72 \u2014 Re-point the inbound SECURITY DEFINER writers at the prefixed names.\n-- These bodies are byte-identical to 002_smart_sync.sql except the calendar_*\n-- table references, which now read plg_calendar_*. gcal_apply_event_patch and\n-- gcal_stamp_outbound only touch public.appointments (unaffected by the rename)\n-- and are re-asserted here for completeness so all four writers stay in step.\n-- ===========================================================================\n\nCREATE OR REPLACE FUNCTION public.gcal_import_event(\n p_tenant_id uuid,\n p_channel_id uuid,\n p_event_id text,\n p_etag text,\n p_summary text,\n p_starts_at timestamptz,\n p_ends_at timestamptz,\n p_all_day boolean,\n p_description text DEFAULT NULL\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path = public\nAS $$\nDECLARE\n v_ch record;\n v_kind text;\n v_assignee uuid;\n v_location uuid;\n v_ends timestamptz;\n v_meta jsonb;\n v_existing uuid;\n v_appt uuid;\nBEGIN\n IF p_tenant_id IS NULL OR p_event_id IS NULL OR p_starts_at IS NULL THEN\n RAISE EXCEPTION 'gcal_import_event: tenant, event id and start are required';\n END IF;\n\n PERFORM set_config('app.booking_origin', 'google', true);\n\n SELECT c.import_mode, c.target_kind, c.target_id\n INTO v_ch\n FROM plg_calendar_channels c\n WHERE c.id = p_channel_id AND c.tenant_id = p_tenant_id;\n IF NOT FOUND THEN RAISE EXCEPTION 'gcal_import_event: unknown channel for tenant'; END IF;\n\n v_kind := CASE WHEN v_ch.import_mode = 'appointment' THEN 'appointment' ELSE 'block' END;\n v_assignee := CASE WHEN v_ch.target_kind = 'assignee' THEN v_ch.target_id ELSE NULL END;\n v_location := CASE WHEN v_ch.target_kind = 'location' THEN v_ch.target_id ELSE NULL END;\n\n v_ends := COALESCE(\n p_ends_at,\n CASE WHEN COALESCE(p_all_day, false) THEN p_starts_at + interval '1 day' ELSE p_starts_at END\n );\n\n v_meta := jsonb_build_object(\n 'syncOrigin', 'google',\n 'googleCalendarEventId', p_event_id,\n 'gcalEtag', p_etag,\n 'gcalChannelId', p_channel_id::text,\n 'allDay', COALESCE(p_all_day, false),\n 'title', p_summary,\n 'source', 'google_import'\n );\n\n SELECT b.id INTO v_existing\n FROM appointments b\n WHERE b.tenant_id = p_tenant_id\n AND b.metadata->>'googleCalendarEventId' = p_event_id\n LIMIT 1;\n\n IF v_existing IS NOT NULL THEN\n UPDATE appointments b\n SET kind = v_kind,\n assignee_id = v_assignee,\n location_id = v_location,\n starts_at = p_starts_at,\n ends_at = v_ends,\n status = 'scheduled',\n notes = COALESCE(p_description, p_summary),\n metadata = COALESCE(b.metadata, '{}'::jsonb) || v_meta,\n updated_at = now()\n WHERE b.id = v_existing\n RETURNING b.id INTO v_appt;\n ELSE\n INSERT INTO appointments (tenant_id, kind, assignee_id, location_id,\n starts_at, ends_at, status, notes, metadata)\n VALUES (p_tenant_id, v_kind, v_assignee, v_location,\n p_starts_at, v_ends, 'scheduled', COALESCE(p_description, p_summary), v_meta)\n RETURNING id INTO v_appt;\n END IF;\n\n RETURN v_appt;\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.gcal_apply_event_patch(\n p_tenant_id uuid,\n p_event_id text,\n p_etag text,\n p_starts_at timestamptz,\n p_ends_at timestamptz,\n p_summary text,\n p_cancelled boolean\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path = public\nAS $$\nDECLARE\n v_appt uuid;\n v_curr_etag text;\nBEGIN\n IF p_tenant_id IS NULL OR p_event_id IS NULL THEN\n RAISE EXCEPTION 'gcal_apply_event_patch: tenant and event id are required';\n END IF;\n\n PERFORM set_config('app.booking_origin', 'google', true);\n\n SELECT b.id, b.metadata->>'gcalEtag'\n INTO v_appt, v_curr_etag\n FROM appointments b\n WHERE b.tenant_id = p_tenant_id\n AND b.metadata->>'googleCalendarEventId' = p_event_id\n LIMIT 1;\n\n IF v_appt IS NULL THEN RETURN NULL; END IF;\n IF p_etag IS NOT NULL AND p_etag = v_curr_etag THEN\n RETURN NULL;\n END IF;\n\n IF COALESCE(p_cancelled, false) THEN\n UPDATE appointments b\n SET status = 'cancelled',\n metadata = COALESCE(b.metadata, '{}'::jsonb)\n || jsonb_build_object('syncOrigin', 'google', 'gcalEtag', p_etag),\n updated_at = now()\n WHERE b.id = v_appt;\n ELSE\n UPDATE appointments b\n SET starts_at = COALESCE(p_starts_at, b.starts_at),\n ends_at = COALESCE(p_ends_at, b.ends_at),\n notes = COALESCE(p_summary, b.notes),\n metadata = COALESCE(b.metadata, '{}'::jsonb)\n || jsonb_build_object('syncOrigin', 'google',\n 'gcalEtag', p_etag,\n 'title', COALESCE(p_summary, b.metadata->>'title')),\n updated_at = now()\n WHERE b.id = v_appt;\n END IF;\n\n RETURN v_appt;\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.gcal_stamp_outbound(\n p_tenant_id uuid,\n p_booking_id uuid,\n p_event_id text,\n p_etag text DEFAULT NULL,\n p_calendar_id text DEFAULT NULL\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path = public\nAS $$\nDECLARE\n v_id uuid;\nBEGIN\n IF p_tenant_id IS NULL OR p_booking_id IS NULL OR p_event_id IS NULL THEN\n RAISE EXCEPTION 'gcal_stamp_outbound: tenant, booking and event id are required';\n END IF;\n\n PERFORM set_config('app.booking_origin', 'google', true);\n\n UPDATE appointments b\n SET metadata = COALESCE(b.metadata, '{}'::jsonb)\n || jsonb_build_object('googleCalendarEventId', p_event_id,\n 'gcalEtag', p_etag,\n 'gcalSyncedAt', now())\n || CASE WHEN p_calendar_id IS NULL THEN '{}'::jsonb\n ELSE jsonb_build_object('gcalCalendarId', p_calendar_id) END,\n updated_at = now()\n WHERE b.id = p_booking_id AND b.tenant_id = p_tenant_id\n RETURNING b.id INTO v_id;\n RETURN v_id;\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.gcal_unlink_channel(\n p_channel_id uuid\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path = public\nAS $$\nDECLARE\n v_id uuid;\nBEGIN\n UPDATE plg_calendar_channels c\n SET sync_token = NULL,\n channel_id = NULL,\n resource_id = NULL,\n channel_expires_at = NULL,\n updated_at = now()\n WHERE c.id = p_channel_id\n RETURNING c.id INTO v_id;\n RETURN v_id;\nEND $$;\n\n-- Re-assert the service_role-only lock (idempotent; matches 002 \u00A74).\nDO $$\nDECLARE fn text;\nBEGIN\n FOR fn IN SELECT unnest(ARRAY[\n 'public.gcal_import_event(uuid, uuid, text, text, text, timestamptz, timestamptz, boolean, text)',\n 'public.gcal_apply_event_patch(uuid, text, text, timestamptz, timestamptz, text, boolean)',\n 'public.gcal_stamp_outbound(uuid, uuid, text, text, text)',\n 'public.gcal_unlink_channel(uuid)'\n ])\n LOOP\n EXECUTE format('REVOKE ALL ON FUNCTION %s FROM public, anon, authenticated', fn);\n EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', fn);\n END LOOP;\nEND $$;\n";
4
+ export declare const MIGRATION_004_DROP_OAUTH_TOKENS = "-- ============================================================================\n-- Google Calendar 004: the credential leaves the tenant database, and the\n-- outbound trigger stops being able to break a booking write.\n--\n-- \u00A71 credential drop oauth_refresh_token + oauth_access_token\n-- \u00A72 outbound anti-loop, safe no-op, delete context, correlation id\n-- \u00A73 realtime publish public.appointments so the calendar repaints\n--\n-- Additive and idempotent: no table is created, no policy is rewritten, and the\n-- tenant RLS from 001 already covers everything touched here.\n-- ============================================================================\n\n-- \u00A71 \u2500\u2500 credential \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- THE EXPOSURE THIS CLOSES: 001 stored `oauth_refresh_token text` and\n-- `oauth_access_token text` on public.plg_calendar_integrations. That table is\n-- read from the browser with the publishable key under tenant RLS \u2014 and the\n-- connector read it with `select('*')` \u2014 so a Google refresh token was readable\n-- by ANY authenticated member of the tenant, reception included. A refresh token\n-- is not a session: it is durable access to the owner's entire calendar, mail-\n-- adjacent metadata and every calendar shared with them. And a pool is one\n-- Supabase project serving N tenants, so anything running there with\n-- service_role read every clinic's Google grant at once.\n--\n-- This is the third connector to make the same move, and the worst of the three.\n-- RankLayer had nothing to replace its key with (it pushes IN and authenticates\n-- against the platform); PlugBank's credential is meant to move to the platform\n-- (FAY-1360 \u2014 as of 2026-08-12 its api_token is still a plaintext pool column).\n-- Google Calendar is PlugBank's case: the grant still has to exist, it just must\n-- not exist HERE. The destination is the platform's ConnectorConnection store\n-- (encrypted; exists, unused), with the sync function redeeming per call \u2014 that\n-- is FAY-1359, and it has NOT happened.\n--\n-- \u26A0 REPO AHEAD OF POOL (verified 2026-08-12, FAY-1379): this file has NOT been\n-- applied to the live salon pool. plg_calendar_integrations there still carries\n-- both token columns with a real clinic's grant, and the DEPLOYED\n-- google-calendar-sync (v16) still reads and writes them. Applying this drop\n-- before FAY-1359's ordered rollout (function reads both homes \u2192 data migrated\n-- \u2192 function reads only the new home \u2192 drop) severs the clinic's sync. Do not\n-- `db apply` this to a pool outside that sequence.\n--\n-- `token_expires_at` deliberately stays. It is a timestamp, not a credential,\n-- and it is the only non-secret signal that a grant exists \u2014 which is what the\n-- browser needs in order to draw a connected/disconnected badge without ever\n-- being handed the thing it is reporting on. See ../credentials.ts.\n--\n-- ROTATE any token a pool already stored. Nothing is live on this connector yet,\n-- but DROP COLUMN is a catalog change: the old values stay in the heap until the\n-- next table rewrite, so a grant that ever reached a pool must be revoked at\n-- Google rather than assumed gone.\n\nDO $$\nBEGIN\n IF to_regclass('public.plg_calendar_integrations') IS NOT NULL THEN\n ALTER TABLE public.plg_calendar_integrations DROP COLUMN IF EXISTS oauth_refresh_token;\n ALTER TABLE public.plg_calendar_integrations DROP COLUMN IF EXISTS oauth_access_token;\n END IF;\n -- Pools converted before 003 renamed the tables carry the legacy name.\n IF to_regclass('public.calendar_integrations') IS NOT NULL THEN\n ALTER TABLE public.calendar_integrations DROP COLUMN IF EXISTS oauth_refresh_token;\n ALTER TABLE public.calendar_integrations DROP COLUMN IF EXISTS oauth_access_token;\n END IF;\nEND $$;\n\n-- \u00A72 \u2500\u2500 outbound trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- Three defects in 001's fn_gcal_push_booking, in order of how badly they hurt:\n--\n-- 1. NO ANTI-LOOP. An inbound Google event is written to public.appointments by\n-- the sync function, which fires this trigger, which pushes the same change\n-- back to Google, which changes the event, which the next pull reads as new.\n-- The symptom a customer reports is an appointment that will not stop moving.\n-- The sync function sets `app.booking_origin = 'google'` for the duration of\n-- its transaction; a write carrying that marker is never echoed back.\n--\n-- 2. A MISSING PREREQUISITE COULD BREAK THE BOOKING. 001 returned early on an\n-- empty URL but not on an empty key, so a half-configured pool sent an\n-- unauthenticated pg_net request; and any error raised in here aborts the\n-- INSERT that fired it. An integration that is not set up must degrade to\n-- \"Google is not updated\", never to \"the appointment could not be saved\".\n-- Every exit path below returns the row.\n--\n-- 3. A DELETE ARRIVED WITH NOTHING TO DELETE. AFTER DELETE can still read OLD,\n-- but pg_net delivers asynchronously \u2014 by the time the function runs, the row\n-- is gone and it cannot look up which Google event to remove. The linked ids\n-- now travel in the payload.\n--\n-- The early returns are not enough on their own: on a pool where the settings\n-- are present but pg_net is not installed, the PERFORM itself raises `schema\n-- \"net\" does not exist` and takes the INSERT down with it. So the delivery is\n-- wrapped too. Swallowing a delivery error is safe here because pg_net only\n-- ENQUEUES \u2014 anything it could tell us is already visible as \"nothing has\n-- synced\" in plg_calendar_sync_log and in the connector's derived health.\n--\n-- The Supabase service-role key still reaches this trigger through the\n-- `app.gcal_service_key` database setting that 001 introduced. That is a lesser\n-- exposure than the one closed in \u00A71 (a GUC is not reachable through PostgREST,\n-- so no browser can read it) but it is still a pool-wide credential in the\n-- database, and it is NOT fixed here. FAY-1372 shipped the RLS half (a\n-- tenant-scoped claim narrows user_tenant_ids); nothing mints such a token for\n-- this trigger yet, and the function still runs on service_role (FAY-1378).\n-- Left as a named debt rather than moved to a third hiding place.\n\nCREATE OR REPLACE FUNCTION public.fn_gcal_push_booking() RETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = public, extensions, net\nAS $$\nDECLARE\n url text := current_setting('app.gcal_sync_url', true);\n key text := current_setting('app.gcal_service_key', true);\n rec record := COALESCE(NEW, OLD);\n op text := lower(TG_OP);\n corr text := NULLIF(current_setting('app.booking_correlation_id', true), '');\nBEGIN\n -- (1) Inbound Google writes mark themselves. Never echo them back.\n IF current_setting('app.booking_origin', true) = 'google' THEN\n RETURN COALESCE(NEW, OLD);\n END IF;\n\n -- (2) Not configured is not an error. Diagnostics report the connector as\n -- incomplete; the appointment is saved either way.\n IF NULLIF(url, '') IS NULL OR NULLIF(key, '') IS NULL THEN\n RETURN COALESCE(NEW, OLD);\n END IF;\n\n BEGIN\n PERFORM net.http_post(\n url := url,\n headers := jsonb_build_object(\n 'Content-Type', 'application/json',\n 'Authorization', 'Bearer ' || key\n ),\n body := jsonb_build_object(\n 'action', 'push_event',\n 'op', op,\n 'bookingId', rec.id,\n 'tenantId', rec.tenant_id,\n -- (3) Carried so a delete can still be honoured after the row is gone.\n 'googleEventId', rec.metadata->>'googleCalendarEventId',\n 'calendarId', rec.metadata->>'gcalCalendarId',\n 'correlationId', COALESCE(corr, gen_random_uuid()::text)\n )\n );\n EXCEPTION WHEN OTHERS THEN\n -- Missing pg_net, a bad URL, a full queue: the appointment is still saved.\n RAISE WARNING 'gcal push skipped for booking %: %', rec.id, SQLERRM;\n END;\n\n RETURN COALESCE(NEW, OLD);\nEND $$;\n\n-- \u00A73 \u2500\u2500 realtime \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- The calendar screen subscribes to public.appointments so a booking written by\n-- the sync function repaints the grid the manager is looking at, instead of\n-- waiting for them to reload the tab. Additive and guarded: a pool without the\n-- supabase_realtime publication is left alone, and re-running adds nothing.\nDO $$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_publication WHERE pubname = 'supabase_realtime')\n AND NOT EXISTS (\n SELECT 1 FROM pg_publication_tables\n WHERE pubname = 'supabase_realtime' AND schemaname = 'public' AND tablename = 'appointments'\n )\n THEN\n ALTER PUBLICATION supabase_realtime ADD TABLE public.appointments;\n END IF;\nEND $$;\n";
5
+ export declare const MIGRATION_005_OUTBOUND_VAULT_DELIVERY = "-- ============================================================================\n-- Google Calendar 005: the outbound credential moves to Vault, and stops being\n-- the service-role key. The Authorization header stays.\n--\n-- \u00A71 trigger fn_gcal_push_booking reads Vault, falls back to the old GUCs\n-- \u00A72 cleanup clear app.gcal_service_key ONLY where Vault already covers it\n--\n-- Additive and idempotent: a CREATE OR REPLACE plus a guarded, conditional\n-- catalog cleanup.\n--\n-- WRITTEN AGAINST THE DEPLOYED POOL, NOT AGAINST THIS FOLDER. The repo and\n-- cluster-salon-br-01 (gphxclpkbtbucoqclbco) had drifted: `004` here still reads\n-- `app.gcal_service_key`, while the pool has been on Vault since 2026-07-28 and\n-- the deployed function is four features ahead of `functions/`. Everything below\n-- was checked against the live database and the live function source, because\n-- the previous attempt at this file was derived from the repo alone and would\n-- have broken a real clinic. See FAY-1379 for the drift itself.\n-- ============================================================================\n\n-- \u00A71 \u2500\u2500 the credential \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- THE EXPOSURE THIS CLOSES, precisely: `001` told operators to run\n-- ALTER DATABASE postgres SET app.gcal_service_key = '<service-role-key>';\n-- and every version of this trigger in the repo since (`001`, `002 \u00A72`,\n-- `004 \u00A72`) reads it back with current_setting(). A pool is ONE Supabase project\n-- serving N tenants and service_role bypasses RLS by definition, so that setting\n-- is unrestricted read/write over every tenant in the cluster, in plain text in\n-- pg_db_role_setting and in every pg_dump taken since.\n--\n-- It is closed by making the credential BOTH narrow and encrypted, not by\n-- removing it: the header is the only thing authorising the push. The deployed\n-- function checks it (`isServiceRequest` compares the bearer against\n-- GCAL_OUTBOUND_SECRET, then against the service-role key) and `push_event` is a\n-- service action with no user path, so a request with no Authorization header is\n-- rejected with `400 Unknown action: push_event`. Because pg_net is\n-- fire-and-forget that rejection is INVISIBLE \u2014 the clinic would simply stop\n-- appearing in Google Calendar. Removing the header is therefore not a security\n-- improvement, it is a silent outage. It was tried and reverted.\n--\n-- So: `gcal_outbound_secret` in Vault. Encrypted at rest, outside pg_dump's\n-- plaintext, readable only by a definer function like this one, and \u2014 the part\n-- that matters \u2014 it authorises exactly one thing, calling this connector's edge\n-- function. Losing it does not hand anyone another tenant's data. This is the\n-- shape the pool has been running since 2026-07-28; `005` is the repo catching\n-- up, so a pool provisioned fresh from this folder is born with it instead of\n-- being told to paste a service-role key into a database setting.\n--\n-- THE FALLBACK IS DELIBERATE. A pool that followed `001` and has no Vault secret\n-- keeps working off the old GUCs, loudly. The alternative \u2014 read Vault only \u2014\n-- silently disarms the push on exactly the pools that did what the docs told\n-- them to, which is the failure this file exists to avoid.\n--\n-- WHAT THIS DOES NOT FIX: the edge function still builds its Supabase client\n-- with SUPABASE_SERVICE_ROLE_KEY, so the FUNCTION still reads every tenant in\n-- the pool. That is not reachable from SQL and does not close here: it closes\n-- when the platform mints a short-lived tenant-scoped token (FAY-1353) that\n-- packages/db/migrations/028_tenant_scoped_token.sql already teaches every\n-- policy to honour through user_tenant_ids(). See docs/platform/DECISIONS.md,\n-- 2026-08-11 \u2014 \"a connector's privilege comes from its token\".\n--\n-- ROTATE the service-role key of any pool that ever ran `001`'s setup line: it\n-- was plain text at rest and in every backup taken since, so \u00A72 clearing it is\n-- not the same as it never having been there.\n\nCREATE OR REPLACE FUNCTION public.fn_gcal_push_booking() RETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = public, extensions, net, vault\nAS $$\nDECLARE\n url text;\n key text;\n legacy boolean := false;\n rec record := COALESCE(NEW, OLD);\n op text := lower(TG_OP);\n corr text := NULLIF(current_setting('app.booking_correlation_id', true), '');\nBEGIN\n -- Unchanged from 004: inbound Google writes mark themselves, and a write\n -- carrying that marker is never echoed back.\n IF current_setting('app.booking_origin', true) = 'google' THEN\n RETURN COALESCE(NEW, OLD);\n END IF;\n\n -- Vault first. Wrapped because a pool without the vault extension must\n -- degrade to the fallback, not abort the booking write.\n BEGIN\n SELECT decrypted_secret INTO url\n FROM vault.decrypted_secrets WHERE name = 'gcal_sync_url'\n ORDER BY created_at DESC LIMIT 1;\n SELECT decrypted_secret INTO key\n FROM vault.decrypted_secrets WHERE name = 'gcal_outbound_secret'\n ORDER BY created_at DESC LIMIT 1;\n EXCEPTION WHEN OTHERS THEN\n url := NULL;\n key := NULL;\n END;\n\n -- Legacy pools provisioned per 001's header keep working, and say so.\n IF NULLIF(url, '') IS NULL OR NULLIF(key, '') IS NULL THEN\n url := COALESCE(NULLIF(url, ''), current_setting('app.gcal_sync_url', true));\n key := COALESCE(NULLIF(key, ''), current_setting('app.gcal_service_key', true));\n legacy := NULLIF(key, '') IS NOT NULL;\n END IF;\n\n -- Not configured is not an error: an unconfigured pool degrades to \"Google is\n -- not updated\", never to \"the appointment could not be saved\".\n IF NULLIF(url, '') IS NULL OR NULLIF(key, '') IS NULL THEN\n RETURN COALESCE(NEW, OLD);\n END IF;\n\n IF legacy THEN\n RAISE WARNING 'gcal outbound is still using app.gcal_service_key; move it to the vault secret gcal_outbound_secret and rotate the service-role key';\n END IF;\n\n BEGIN\n PERFORM net.http_post(\n url := url,\n headers := jsonb_build_object(\n 'Content-Type', 'application/json',\n -- REQUIRED. The deployed function authorises push_event on this header\n -- alone; without it every push 400s, silently, forever.\n 'Authorization', 'Bearer ' || key\n ),\n body := jsonb_build_object(\n 'action', 'push_event',\n 'op', op,\n 'bookingId', rec.id,\n 'tenantId', rec.tenant_id,\n -- Carried so a delete can still be honoured after the row is gone.\n 'googleEventId', rec.metadata->>'googleCalendarEventId',\n 'calendarId', rec.metadata->>'gcalCalendarId',\n 'correlationId', COALESCE(corr, gen_random_uuid()::text)\n )\n );\n EXCEPTION WHEN OTHERS THEN\n -- Missing pg_net, a bad URL, a full queue: the appointment is still saved.\n -- The deployed trigger lacks this wrapper; 004 added it here after a pool\n -- with the settings present but the extension missing took an INSERT down.\n RAISE WARNING 'gcal push skipped for booking %: %', rec.id, SQLERRM;\n END;\n\n RETURN COALESCE(NEW, OLD);\nEND $$;\n\n-- \u00A72 \u2500\u2500 cleanup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- Redefining the function stops the key being PREFERRED; it does not stop it\n-- being STORED. Settings live in pg_db_role_setting per (database, role), so the\n-- sweep is driven off the catalog rather than off the one ALTER DATABASE form\n-- `001` happened to show.\n--\n-- CONDITIONAL ON PURPOSE: the setting is cleared only where Vault already\n-- supplies `gcal_outbound_secret`, i.e. only where \u00A71 provably never needs the\n-- fallback. Anywhere else it is left in place and reported, because clearing it\n-- there would disarm the push with no error anyone can see. On the live pool\n-- this loop finds nothing \u2014 the setting is not present on any role or database \u2014\n-- and that is the expected result, not a failure.\nDO $$\nDECLARE\n r record;\n has_vault boolean := false;\nBEGIN\n BEGIN\n SELECT EXISTS (\n SELECT 1 FROM vault.decrypted_secrets\n WHERE name = 'gcal_outbound_secret' AND NULLIF(decrypted_secret, '') IS NOT NULL\n ) INTO has_vault;\n EXCEPTION WHEN OTHERS THEN\n has_vault := false;\n END;\n\n FOR r IN\n SELECT d.datname, a.rolname\n FROM pg_db_role_setting s\n LEFT JOIN pg_database d ON d.oid = s.setdatabase\n LEFT JOIN pg_roles a ON a.oid = s.setrole\n WHERE EXISTS (\n SELECT 1 FROM unnest(s.setconfig) AS c WHERE c LIKE 'app.gcal_service_key=%'\n )\n LOOP\n IF NOT has_vault THEN\n RAISE WARNING 'app.gcal_service_key is still set (database %, role %) and is the ONLY outbound credential \u2014 create the vault secret gcal_outbound_secret first, then re-run this migration',\n r.datname, r.rolname;\n CONTINUE;\n END IF;\n BEGIN\n IF r.rolname IS NULL THEN\n EXECUTE format('ALTER DATABASE %I RESET app.gcal_service_key', r.datname);\n ELSIF r.datname IS NULL THEN\n EXECUTE format('ALTER ROLE %I RESET app.gcal_service_key', r.rolname);\n ELSE\n EXECUTE format('ALTER ROLE %I IN DATABASE %I RESET app.gcal_service_key', r.rolname, r.datname);\n END IF;\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'could not clear app.gcal_service_key (database %, role %): % \u2014 clear it by hand, and rotate the key either way',\n r.datname, r.rolname, SQLERRM;\n END;\n END LOOP;\nEND $$;\n";
6
+ export declare const MIGRATION_006_WEBHOOK_DELIVERIES_OUTBOX = "-- ---------------------------------------------------------------------------\n-- Google Calendar WEBHOOK INTAKE \u2014 delivery ledger + automation outbox\n-- ---------------------------------------------------------------------------\n-- RECONSTRUCTED FROM THE LIVE SCHEMA on 2026-08-12 (FAY-1379). These objects\n-- existed in pool gphxclpkbtbucoqclbco and in NO migration in any repo: they\n-- were applied by hand alongside the google-calendar-webhook deployment. A pool\n-- provisioned from this repo could not run that function \u2014 the SELECT of\n-- plg_calendar_channels.webhook_token_hash failed before any gate ran, and both\n-- INSERT targets were missing. This file closes that hole.\n--\n-- Written to be applied to a pool that ALREADY has them: every statement is\n-- IF NOT EXISTS / DROP-then-CREATE, and re-running it must be a no-op.\n--\n-- \u00A71 plg_calendar_channels \u2014 the three webhook columns\n-- \u00A72 plg_calendar_webhook_deliveries \u2014 one row per Google notification\n-- \u00A73 plg_calendar_automation_outbox \u2014 domain events for the automations lane\n--\n-- Read by: functions/google-calendar-webhook/index.ts (\u00A71 gate, \u00A72 idempotency,\n-- \u00A73 emit) and functions/google-calendar-sync/index.ts (\u00A71 watch bookkeeping,\n-- \u00A73 emit on inbound import/patch).\n-- ---------------------------------------------------------------------------\n\n-- ===========================================================================\n-- \u00A71 \u2014 Watch-channel authentication state on plg_calendar_channels.\n-- Added by ALTER in the pool (they sit after created_at/updated_at), so they\n-- are added the same way here rather than folded into 002's CREATE TABLE \u2014\n-- 002 has already run everywhere and its checksum must not move.\n--\n-- webhook_token_hash: SHA-256 of the token handed to Google at watch_start. The\n-- token itself is never stored, so a dump of this table does not let anyone\n-- forge a notification. The webhook re-hashes what arrives and compares in\n-- constant time.\n-- last_webhook_message_number: high-water mark of Google's monotonic per-channel\n-- counter \u2014 the replay guard. bigint because it outgrows int4.\n-- ===========================================================================\nALTER TABLE public.plg_calendar_channels\n ADD COLUMN IF NOT EXISTS webhook_token_hash text;\nALTER TABLE public.plg_calendar_channels\n ADD COLUMN IF NOT EXISTS last_webhook_message_number bigint;\nALTER TABLE public.plg_calendar_channels\n ADD COLUMN IF NOT EXISTS last_webhook_at timestamptz;\n\n-- ===========================================================================\n-- \u00A72 \u2014 plg_calendar_webhook_deliveries: one row per accepted notification.\n--\n-- The row IS the idempotency mechanism, not a log of one: the webhook inserts\n-- before doing any work and reads 23505 as \"already handled\", returning 204.\n-- Hence two unique keys, both present in the pool and both reproduced \u2014\n-- (channel_ref, message_number) is the constraint the FK cascade protects, and\n-- (google_channel_id, message_number) catches a replay arriving after the\n-- channel row was recreated with the same Google channel id.\n-- ===========================================================================\nCREATE TABLE IF NOT EXISTS public.plg_calendar_webhook_deliveries (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL,\n channel_ref uuid NOT NULL REFERENCES public.plg_calendar_channels(id) ON DELETE CASCADE,\n google_channel_id text NOT NULL,\n resource_id text,\n message_number bigint NOT NULL,\n resource_state text,\n status text NOT NULL DEFAULT 'pending'\n CHECK (status IN ('pending','processing','processed','failed','ignored')),\n error text,\n received_at timestamptz NOT NULL DEFAULT now(),\n processed_at timestamptz,\n created_at timestamptz DEFAULT now(),\n UNIQUE (channel_ref, message_number)\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS uq_plg_calendar_webhook_delivery_message\n ON public.plg_calendar_webhook_deliveries (google_channel_id, message_number);\n\nCREATE INDEX IF NOT EXISTS idx_plg_calendar_webhook_deliveries_tenant_created\n ON public.plg_calendar_webhook_deliveries (tenant_id, created_at DESC);\n\n-- Two partial indexes over the same predicate on different sort columns. Both\n-- are in the pool (evidence of two overlapping hand-applied scripts) and both\n-- are reproduced so this file matches the live schema exactly; consolidating\n-- them is a separate change with its own reasoning.\nCREATE INDEX IF NOT EXISTS idx_plg_calendar_webhook_pending\n ON public.plg_calendar_webhook_deliveries (status, received_at)\n WHERE status IN ('pending','failed');\nCREATE INDEX IF NOT EXISTS idx_plg_calendar_webhook_deliveries_pending\n ON public.plg_calendar_webhook_deliveries (status, created_at)\n WHERE status IN ('pending','failed');\n\nALTER TABLE public.plg_calendar_webhook_deliveries ENABLE ROW LEVEL SECURITY;\n\n-- Read-only for the app. Every write comes from the webhook function running as\n-- service_role, which bypasses RLS \u2014 there is no authenticated INSERT/UPDATE\n-- grant, so a browser cannot forge or retire a delivery.\nGRANT SELECT ON public.plg_calendar_webhook_deliveries TO authenticated;\nGRANT ALL ON public.plg_calendar_webhook_deliveries TO service_role;\n\nDROP POLICY IF EXISTS plg_calendar_webhook_deliveries_read ON public.plg_calendar_webhook_deliveries;\nCREATE POLICY plg_calendar_webhook_deliveries_read ON public.plg_calendar_webhook_deliveries\n FOR SELECT TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- ===========================================================================\n-- \u00A73 \u2014 plg_calendar_automation_outbox: calendar facts, for the automations lane.\n--\n-- Transactional outbox: the sync/webhook functions record WHAT happened and\n-- return; a consumer drains it. idempotency_key is unique per tenant so a\n-- replayed Google delivery or a re-run pull upserts onto the same row instead\n-- of firing an automation twice \u2014 that key is what the functions pass as\n-- onConflict, so it is load-bearing and not merely a hygiene constraint.\n-- ===========================================================================\nCREATE TABLE IF NOT EXISTS public.plg_calendar_automation_outbox (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL,\n source text NOT NULL DEFAULT 'google_calendar',\n event_type text NOT NULL,\n aggregate_type text NOT NULL,\n aggregate_id text,\n idempotency_key text NOT NULL,\n payload jsonb NOT NULL DEFAULT '{}'::jsonb,\n status text NOT NULL DEFAULT 'pending'\n CHECK (status IN ('pending','processing','processed','failed','ignored')),\n attempts integer NOT NULL DEFAULT 0,\n last_error text,\n occurred_at timestamptz NOT NULL DEFAULT now(),\n -- available_at is the retry clock: a failed row is re-eligible once it passes.\n available_at timestamptz NOT NULL DEFAULT now(),\n processed_at timestamptz,\n error text,\n created_at timestamptz DEFAULT now(),\n UNIQUE (tenant_id, idempotency_key)\n);\n\n-- Redundant with the UNIQUE constraint's own index, and present in the pool for\n-- the same overlapping-scripts reason as \u00A72. Reproduced, not tidied.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_plg_calendar_automation_outbox_idempotency\n ON public.plg_calendar_automation_outbox (tenant_id, idempotency_key);\n\nCREATE INDEX IF NOT EXISTS idx_plg_calendar_automation_pending\n ON public.plg_calendar_automation_outbox (status, available_at)\n WHERE status IN ('pending','failed');\nCREATE INDEX IF NOT EXISTS idx_plg_calendar_automation_outbox_pending\n ON public.plg_calendar_automation_outbox (status, available_at, created_at)\n WHERE status IN ('pending','failed');\n\nALTER TABLE public.plg_calendar_automation_outbox ENABLE ROW LEVEL SECURITY;\n\nGRANT SELECT ON public.plg_calendar_automation_outbox TO authenticated;\nGRANT ALL ON public.plg_calendar_automation_outbox TO service_role;\n\nDROP POLICY IF EXISTS plg_calendar_automation_outbox_read ON public.plg_calendar_automation_outbox;\nCREATE POLICY plg_calendar_automation_outbox_read ON public.plg_calendar_automation_outbox\n FOR SELECT TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\n";
7
+ export declare const MIGRATION_007_CREDENTIAL_FREE_INSTALL = "-- ---------------------------------------------------------------------------\n-- Google Calendar \u2014 a credential-free install, from zero\n-- ---------------------------------------------------------------------------\n-- FAY-1359/1360. On 2026-08-12 the five plg_calendar_* tables were DROPPED from\n-- the beauty pool (gphxclpkbtbucoqclbco) together with both edge functions,\n-- because plg_calendar_integrations held the tenant's Google refresh token in an\n-- open column. A pool is one database for N clinics, so that column was one\n-- backup, one dump or one service_role key away from being every clinic's\n-- calendar.\n--\n-- This file is what a pool runs to have the connector back. It is deliberately\n-- SELF-CONTAINED rather than \"001 through 006 again\":\n--\n-- * 001 CREATEs the credential columns and 004 drops them. A pool restored by\n-- replaying the chain therefore holds a refresh-token column for the length\n-- of that replay, and holds it forever if the replay stops early \u2014 which is\n-- exactly what a half-applied ledger does. There is no ordering of those\n-- files in which the column never exists; there is this file, in which it\n-- never exists.\n-- * The beauty pool's ledger and its schema had already parted company (it\n-- recorded shop/0006b while tables up to 0043 were applied by hand). A\n-- recovery that depends on the ledger being right is not a recovery.\n--\n-- Re-runnable, and a near no-op on a pool that ran 001\u2013006: every statement is\n-- IF NOT EXISTS / DROP-then-CREATE, and \u00A75 drops the credential columns if it\n-- finds them rather than assuming 004 ran.\n--\n-- \u00A71 plg_calendar_integrations \u2014 per-tenant sync state. NO credential.\n-- \u00A72 plg_calendar_channels \u2014 the de-para, incl. the webhook columns\n-- \u00A73 plg_calendar_sync_log \u2014 run history\n-- \u00A74 plg_calendar_webhook_deliveries + plg_calendar_automation_outbox\n-- \u00A75 the gate: this schema must not be able to hold a Google grant\n--\n-- WHAT OWNS WHAT, after this file (FAY-1388):\n-- the PLATFORM owns the credential \u2014 which Google account, authorized by\n-- whom, and the refresh token, in the vault, reachable only by redemption;\n-- plg_connections owns \"is this connector on for this tenant\", written by the\n-- app when consent comes back;\n-- these tables own the SYNC STATE \u2014 which calendars map to what, how far the\n-- cursor got, what Google last told us. Nothing here is a secret, and\n-- nothing here can be traded for access to anyone's calendar.\n-- ---------------------------------------------------------------------------\n\n-- ===========================================================================\n-- \u00A71 \u2014 plg_calendar_integrations: one row per tenant, sync state only.\n--\n-- Same shape as 001 MINUS oauth_refresh_token, oauth_access_token and\n-- token_expires_at. There is no column here to put a token in, which is the\n-- property this migration exists to establish \u2014 see \u00A75, which enforces it.\n--\n-- No FK to plg_connections on purpose. That table is the connector framework's\n-- and this one is the calendar's; a FK would make the connector's row a\n-- prerequisite for reading a cursor, and the two planes are meant to be able to\n-- fail independently.\n-- ===========================================================================\nCREATE TABLE IF NOT EXISTS public.plg_calendar_integrations (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n provider text NOT NULL DEFAULT 'google',\n calendar_id text NOT NULL DEFAULT 'primary',\n -- Incremental pull cursor + legacy single-calendar watch bookkeeping.\n sync_token text,\n channel_id text,\n channel_resource_id text,\n channel_expires_at timestamptz,\n active boolean NOT NULL DEFAULT true,\n last_sync_at timestamptz,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (tenant_id, provider)\n);\nALTER TABLE public.plg_calendar_integrations ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_calendar_integrations_tenant\n ON public.plg_calendar_integrations(tenant_id);\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_calendar_integrations TO authenticated;\nGRANT ALL ON public.plg_calendar_integrations TO service_role;\n\n-- ===========================================================================\n-- \u00A72 \u2014 plg_calendar_channels: the de-para, with 006's webhook columns folded in.\n-- ===========================================================================\nCREATE TABLE IF NOT EXISTS public.plg_calendar_channels (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n integration_id uuid NOT NULL REFERENCES public.plg_calendar_integrations(id) ON DELETE CASCADE,\n tenant_id uuid NOT NULL,\n google_calendar_id text NOT NULL,\n summary text,\n color text,\n direction text NOT NULL DEFAULT 'bidirectional'\n CHECK (direction IN ('inbound','outbound','bidirectional','off')),\n target_kind text CHECK (target_kind IN ('assignee','service','location')),\n target_id uuid,\n import_mode text NOT NULL DEFAULT 'block'\n CHECK (import_mode IN ('block','appointment')),\n sync_token text,\n channel_id text,\n resource_id text,\n channel_expires_at timestamptz,\n is_active boolean NOT NULL DEFAULT true,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (integration_id, google_calendar_id)\n);\n\n-- The webhook's authentication state. `webhook_token_hash` is a SHA-256 of the\n-- token handed to Google \u2014 the token itself is never stored, so reading this\n-- table does not let anyone forge a notification. Added by ALTER as well as in\n-- the CREATE above, for the pool that already has the table from 002.\nALTER TABLE public.plg_calendar_channels\n ADD COLUMN IF NOT EXISTS webhook_token_hash text;\nALTER TABLE public.plg_calendar_channels\n ADD COLUMN IF NOT EXISTS last_webhook_message_number bigint;\nALTER TABLE public.plg_calendar_channels\n ADD COLUMN IF NOT EXISTS last_webhook_at timestamptz;\n\nCREATE INDEX IF NOT EXISTS idx_plg_calendar_channels_tenant\n ON public.plg_calendar_channels(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_plg_calendar_channels_integration\n ON public.plg_calendar_channels(integration_id);\n\nALTER TABLE public.plg_calendar_channels ENABLE ROW LEVEL SECURITY;\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_calendar_channels TO authenticated;\nGRANT ALL ON public.plg_calendar_channels TO service_role;\n\nDROP TRIGGER IF EXISTS plg_calendar_channels_updated_at ON public.plg_calendar_channels;\nCREATE TRIGGER plg_calendar_channels_updated_at BEFORE UPDATE ON public.plg_calendar_channels\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\n-- ===========================================================================\n-- \u00A73 \u2014 plg_calendar_sync_log: what already happened, per run.\n-- ===========================================================================\nCREATE TABLE IF NOT EXISTS public.plg_calendar_sync_log (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n direction text NOT NULL, -- inbound | outbound\n trigger text, -- on-write | scheduled | webhook | manual\n status text NOT NULL DEFAULT 'success', -- success | partial | error\n fetched integer NOT NULL DEFAULT 0,\n written integer NOT NULL DEFAULT 0,\n error text,\n correlation_id uuid,\n channel_id_ref uuid,\n created_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_calendar_sync_log ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_calendar_sync_log ADD COLUMN IF NOT EXISTS correlation_id uuid;\nALTER TABLE public.plg_calendar_sync_log ADD COLUMN IF NOT EXISTS channel_id_ref uuid;\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_calendar_sync_log TO authenticated;\nGRANT ALL ON public.plg_calendar_sync_log TO service_role;\n\n-- RLS: tenant isolation for the three tables the app reads and writes.\nDO $$\nDECLARE t text;\nBEGIN\n FOR t IN SELECT unnest(ARRAY[\n 'plg_calendar_integrations',\n 'plg_calendar_channels',\n 'plg_calendar_sync_log'\n ])\n LOOP\n IF NOT EXISTS (\n SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_rls'\n ) THEN\n EXECUTE format(\n 'CREATE POLICY %I ON public.%I FOR ALL TO authenticated '\n 'USING (tenant_id IN (SELECT public.user_tenant_ids())) '\n 'WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()))',\n t||'_rls', t);\n END IF;\n IF NOT EXISTS (\n SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_service'\n ) THEN\n EXECUTE format(\n 'CREATE POLICY %I ON public.%I FOR ALL TO service_role USING (true) WITH CHECK (true)',\n t||'_service', t);\n END IF;\n END LOOP;\nEND $$;\n\n-- ===========================================================================\n-- \u00A74 \u2014 Webhook intake: the delivery ledger and the automation outbox.\n--\n-- The delivery row IS the idempotency mechanism: the webhook inserts before\n-- doing any work and reads a 23505 as \"already handled\". Google retries hard,\n-- so this is what stops one notification becoming two appointments.\n-- ===========================================================================\nCREATE TABLE IF NOT EXISTS public.plg_calendar_webhook_deliveries (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL,\n channel_ref uuid NOT NULL REFERENCES public.plg_calendar_channels(id) ON DELETE CASCADE,\n google_channel_id text NOT NULL,\n resource_id text,\n message_number bigint NOT NULL,\n resource_state text,\n status text NOT NULL DEFAULT 'pending'\n CHECK (status IN ('pending','processing','processed','failed','ignored')),\n error text,\n received_at timestamptz NOT NULL DEFAULT now(),\n processed_at timestamptz,\n created_at timestamptz DEFAULT now(),\n UNIQUE (channel_ref, message_number)\n);\n\n-- Catches a replay that arrives after the channel row was recreated with the\n-- same Google channel id \u2014 the FK-scoped constraint above cannot see that.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_plg_calendar_webhook_delivery_message\n ON public.plg_calendar_webhook_deliveries (google_channel_id, message_number);\nCREATE INDEX IF NOT EXISTS idx_plg_calendar_webhook_deliveries_tenant_created\n ON public.plg_calendar_webhook_deliveries (tenant_id, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_plg_calendar_webhook_deliveries_pending\n ON public.plg_calendar_webhook_deliveries (status, created_at)\n WHERE status IN ('pending','failed');\n\nALTER TABLE public.plg_calendar_webhook_deliveries ENABLE ROW LEVEL SECURITY;\n-- Read-only for the app: every write comes from the webhook function as\n-- service_role, so a browser cannot forge or retire a delivery.\nGRANT SELECT ON public.plg_calendar_webhook_deliveries TO authenticated;\nGRANT ALL ON public.plg_calendar_webhook_deliveries TO service_role;\n\nDROP POLICY IF EXISTS plg_calendar_webhook_deliveries_read ON public.plg_calendar_webhook_deliveries;\nCREATE POLICY plg_calendar_webhook_deliveries_read ON public.plg_calendar_webhook_deliveries\n FOR SELECT TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\nCREATE TABLE IF NOT EXISTS public.plg_calendar_automation_outbox (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL,\n source text NOT NULL DEFAULT 'google_calendar',\n event_type text NOT NULL,\n aggregate_type text NOT NULL,\n aggregate_id text,\n idempotency_key text NOT NULL,\n payload jsonb NOT NULL DEFAULT '{}'::jsonb,\n status text NOT NULL DEFAULT 'pending'\n CHECK (status IN ('pending','processing','processed','failed','ignored')),\n attempts integer NOT NULL DEFAULT 0,\n last_error text,\n occurred_at timestamptz NOT NULL DEFAULT now(),\n -- available_at is the retry clock: a failed row is re-eligible once it passes.\n available_at timestamptz NOT NULL DEFAULT now(),\n processed_at timestamptz,\n error text,\n created_at timestamptz DEFAULT now(),\n UNIQUE (tenant_id, idempotency_key)\n);\n\nCREATE INDEX IF NOT EXISTS idx_plg_calendar_automation_outbox_pending\n ON public.plg_calendar_automation_outbox (status, available_at, created_at)\n WHERE status IN ('pending','failed');\n\nALTER TABLE public.plg_calendar_automation_outbox ENABLE ROW LEVEL SECURITY;\nGRANT SELECT ON public.plg_calendar_automation_outbox TO authenticated;\nGRANT ALL ON public.plg_calendar_automation_outbox TO service_role;\n\nDROP POLICY IF EXISTS plg_calendar_automation_outbox_read ON public.plg_calendar_automation_outbox;\nCREATE POLICY plg_calendar_automation_outbox_read ON public.plg_calendar_automation_outbox\n FOR SELECT TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- ===========================================================================\n-- \u00A75 \u2014 The gate.\n--\n-- Everything above is a CREATE, and a CREATE says nothing about a pool that\n-- already has the old shape. This block is the part that actually removes the\n-- hole: it drops the three credential columns wherever it finds them \u2014 the\n-- pool 004 never reached, the pool restored from an old backup, the pool\n-- someone re-provisioned from 001 \u2014 and then refuses to finish if any survived.\n--\n-- The RAISE matters more than the DROP. A migration that silently tolerated a\n-- token column is how this schema kept one for months.\n-- ===========================================================================\nDO $$\nDECLARE\n leftover text;\nBEGIN\n ALTER TABLE IF EXISTS public.plg_calendar_integrations DROP COLUMN IF EXISTS oauth_refresh_token;\n ALTER TABLE IF EXISTS public.plg_calendar_integrations DROP COLUMN IF EXISTS oauth_access_token;\n ALTER TABLE IF EXISTS public.plg_calendar_integrations DROP COLUMN IF EXISTS token_expires_at;\n\n SELECT string_agg(table_name || '.' || column_name, ', ')\n INTO leftover\n FROM information_schema.columns\n WHERE table_schema = 'public'\n AND table_name LIKE 'plg_calendar%'\n AND (\n column_name LIKE '%refresh_token%'\n OR column_name LIKE '%access_token%'\n OR column_name IN ('oauth_token', 'client_secret', 'api_key')\n );\n\n IF leftover IS NOT NULL THEN\n RAISE EXCEPTION\n 'Google Calendar schema still holds credential columns: %. The grant belongs in the platform vault (FAY-1359); redeem it at run time instead.',\n leftover;\n END IF;\nEND $$;\n\n-- Dropping the columns does not erase what was in them: Postgres leaves the old\n-- values in the heap until the next table rewrite. Any grant that ever reached a\n-- pool must be REVOKED AT GOOGLE, not assumed gone \u2014 which is also why every\n-- tenant re-consents after this file runs.\n";
8
+ export declare const MIGRATIONS: Array<{
9
+ id: string;
10
+ sql: string;
11
+ }>;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/integrations/google-calendar/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,6BAA6B,i9JAiGzC,CAAA;AAED,eAAO,MAAM,wBAAwB,6zuBAqgBpC,CAAA;AAED,eAAO,MAAM,wBAAwB,klUAiRpC,CAAA;AAED,eAAO,MAAM,+BAA+B,+jTAsK3C,CAAA;AAED,eAAO,MAAM,qCAAqC,k/TAqMjD,CAAA;AAED,eAAO,MAAM,uCAAuC,osQAuJnD,CAAA;AAED,eAAO,MAAM,qCAAqC,2zdA4SjD,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAQzD,CAAA"}
@@ -0,0 +1,26 @@
1
+ import type { GoogleCalendarHealth } from './health';
2
+ import type { CalendarChannel, CalendarIntegration, CalendarSyncLogEntry } from './types';
3
+ /** The narrow slice of the provider this needs — a fake is three functions. */
4
+ export interface GoogleCalendarPanelSource {
5
+ getIntegration(): Promise<CalendarIntegration | null>;
6
+ getSyncLog(): Promise<CalendarSyncLogEntry[]>;
7
+ listChannels(): Promise<CalendarChannel[]>;
8
+ }
9
+ export interface GoogleCalendarPanelData {
10
+ integration: CalendarIntegration | null;
11
+ logs: CalendarSyncLogEntry[];
12
+ channels: CalendarChannel[];
13
+ health: GoogleCalendarHealth;
14
+ }
15
+ /** Derive health from rows the caller already holds. Pure — it never fetches. */
16
+ export declare function googleCalendarHealthOf(input: {
17
+ integration: CalendarIntegration | null;
18
+ logs: CalendarSyncLogEntry[];
19
+ channels: CalendarChannel[];
20
+ }): GoogleCalendarHealth;
21
+ /**
22
+ * One read per table. Tolerant: a partial read still reports, and a
23
+ * disconnected integration costs exactly one request.
24
+ */
25
+ export declare function readGoogleCalendarPanel(provider: GoogleCalendarPanelSource): Promise<GoogleCalendarPanelData>;
26
+ //# sourceMappingURL=panel-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panel-data.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/panel-data.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAEzF,+EAA+E;AAC/E,MAAM,WAAW,yBAAyB;IACxC,cAAc,IAAI,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAA;IACrD,UAAU,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAA;IAC7C,YAAY,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAAA;CAC3C;AAED,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,mBAAmB,GAAG,IAAI,CAAA;IACvC,IAAI,EAAE,oBAAoB,EAAE,CAAA;IAC5B,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,MAAM,EAAE,oBAAoB,CAAA;CAC7B;AAQD,iFAAiF;AACjF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,WAAW,EAAE,mBAAmB,GAAG,IAAI,CAAA;IACvC,IAAI,EAAE,oBAAoB,EAAE,CAAA;IAC5B,QAAQ,EAAE,eAAe,EAAE,CAAA;CAC5B,GAAG,oBAAoB,CASvB;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,yBAAyB,GAClC,OAAO,CAAC,uBAAuB,CAAC,CAUlC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=panel-data.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panel-data.test.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/panel-data.test.ts"],"names":[],"mappings":""}
@@ -34,6 +34,8 @@ export interface GCalEvent {
34
34
  timeZone?: string;
35
35
  };
36
36
  status?: string;
37
+ /** RFC3339 stamp of Google's last change — the remote side of the conflict rule. */
38
+ updated?: string;
37
39
  /** Set on instances expanded from a recurring series (singleEvents=true). */
38
40
  recurringEventId?: string;
39
41
  }
@@ -114,6 +116,21 @@ export interface AppointmentRow {
114
116
  notes?: string | null;
115
117
  metadata?: Record<string, any> | null;
116
118
  }
119
+ /**
120
+ * Raw shape of a public.plg_calendar_integrations row (snake_case DB columns),
121
+ * as the browser is allowed to see it. No credential appears here — see
122
+ * ./credentials.ts and migrations/004_drop_oauth_tokens.sql.
123
+ */
124
+ export interface CalendarIntegrationRow {
125
+ id: string;
126
+ tenant_id?: string;
127
+ provider: string;
128
+ calendar_id?: string | null;
129
+ active?: boolean | null;
130
+ /** A timestamp, not a token — the non-secret signal that a grant exists. */
131
+ token_expires_at?: string | null;
132
+ last_sync_at?: string | null;
133
+ }
117
134
  /** Raw shape of a public.plg_calendar_channels row (snake_case DB columns). */
118
135
  export interface CalendarChannelRow {
119
136
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { CalendarBooking } from '../types';
2
+ /** O evento específico de uma transição de status, se ela tiver um. */
3
+ export declare function bookingStatusEventKey(status: string): string | undefined;
4
+ export declare function emitBookingEvent(key: string, booking: CalendarBooking): void;
5
+ //# sourceMappingURL=booking-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-events.d.ts","sourceRoot":"","sources":["../../src/lib/booking-events.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAW/C,uEAAuE;AACvE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE;AAeD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAK5E"}
@@ -0,0 +1,8 @@
1
+ import type { EventInput } from '@fullcalendar/core';
2
+ import type { CalendarBooking } from '../types';
3
+ /**
4
+ * Booking → FullCalendar event. Extracted from CalendarView so the resource-lane
5
+ * rule below is testable without mounting the grid.
6
+ */
7
+ export declare function bookingToEvent(booking: CalendarBooking, statusColors: Record<string, string>, visibleResourceIds?: string[]): EventInput;
8
+ //# sourceMappingURL=calendar-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar-events.d.ts","sourceRoot":"","sources":["../../src/lib/calendar-events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAE/C;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,eAAe,EACxB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,kBAAkB,GAAE,MAAM,EAAO,GAChC,UAAU,CAqBZ"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=calendar-events.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar-events.test.d.ts","sourceRoot":"","sources":["../../src/lib/calendar-events.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA0NrC,CAAA"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAyNrC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA0NvC,CAAA"}
1
+ {"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAyNvC,CAAA"}
@@ -1,5 +1,5 @@
1
- import { createMockAgendaProvider, createSupabasePublicBookingProvider } from '../chunk-76ZOHRBM.js';
2
- export { createSupabasePublicBookingProvider } from '../chunk-76ZOHRBM.js';
1
+ import { createMockAgendaProvider, createSupabasePublicBookingProvider } from '../chunk-ISLJXP7O.js';
2
+ export { createSupabasePublicBookingProvider } from '../chunk-ISLJXP7O.js';
3
3
  import { createContext, useState, useEffect, useMemo, useContext, useRef, createElement } from 'react';
4
4
  import { ChevronLeft, ShieldCheck, Clock, ChevronRight, User, Check, Calendar, ChevronDown, Loader2, CreditCard, Plus, QrCode, CalendarCheck, Copy, Video } from 'lucide-react';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CalendarView.d.ts","sourceRoot":"","sources":["../../src/views/CalendarView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAA;AAwRhF,wBAAgB,YAAY,sBA+zB3B"}
1
+ {"version":3,"file":"CalendarView.d.ts","sourceRoot":"","sources":["../../src/views/CalendarView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAA;AA2QhF,wBAAgB,YAAY,sBA22B3B"}