@fayz-ai/plugin-agenda 0.26.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-AKALKGB2.js +96 -0
- package/dist/chunk-AKALKGB2.js.map +1 -0
- package/dist/index.js +63 -59
- package/dist/index.js.map +1 -1
- package/dist/integrations/google-calendar/index.d.ts.map +1 -1
- package/dist/integrations/google-calendar/migrations/index.d.ts +1 -0
- package/dist/integrations/google-calendar/migrations/index.d.ts.map +1 -1
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +3 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +11 -7
|
@@ -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;
|
|
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;AAcnD,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,YAAY,EACV,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,SAAS,EACT,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,SAAS,CAAA;AAKhB,wBAAgB,0BAA0B,IAAI,cAAc,CA8I3D"}
|
|
@@ -6,6 +6,7 @@ export declare const MIGRATION_005_OUTBOUND_VAULT_DELIVERY = "-- ===============
|
|
|
6
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
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
8
|
export declare const MIGRATION_008_THE_OUTBOUND_HALF = "-- ---------------------------------------------------------------------------\n-- Google Calendar \u2014 the outbound half that 007 does not install\n-- ---------------------------------------------------------------------------\n-- 007 is the credential-free install: it creates the five plg_calendar_* tables\n-- from zero, with no column a Google grant could ever sit in. What it does NOT\n-- create is the trigger on public.appointments that pushes a booking OUT to\n-- Google, because that trigger never lived in those tables.\n--\n-- Only 001 and 002 create `trg_gcal_push`, and 001 is the file whose credential\n-- columns 007 exists to prevent. So there is no ordering of the existing files\n-- that gives a fresh pool the outbound direction without also giving it, for a\n-- while, a plaintext refresh-token column.\n--\n-- The result on a pool installed by 007 alone: Google \u2192 Fayz works, Fayz \u2192\n-- Google is silent. Not an error, not a warning \u2014 appointments simply never\n-- reach the calendar. Discovered on cluster-squash-test after 007 came up green\n-- and the tables were correct.\n--\n-- This file is the missing half, and nothing else:\n--\n-- \u00A71 public.fn_gcal_push_booking \u2014 verbatim the definition 005 settled on:\n-- vault first (`gcal_sync_url` + `gcal_outbound_secret`), the legacy GUC\n-- only as a fallback that WARNs, and a no-op when neither is configured.\n-- Reproduced rather than referenced because 005 cannot run here: it\n-- replaces a function 001 created, and 001 is the file we are avoiding.\n-- \u00A72 trg_gcal_push on public.appointments \u2014 the shape 002 re-asserts.\n--\n-- A pool that already ran 001\u2192006 gets an identical function and an identical\n-- trigger, which is why this is safe to apply anywhere: every statement is\n-- CREATE OR REPLACE / DROP-then-CREATE.\n--\n-- WITHOUT THE VAULT SECRETS THIS IS INERT ON PURPOSE. The function reads\n-- `gcal_sync_url` and `gcal_outbound_secret` from the vault and returns without\n-- pushing when they are absent, so installing the trigger before the secrets\n-- exist cannot break an INSERT into appointments \u2014 the most load-bearing table\n-- the agenda has.\n--\n-- Idempotent and replay-safe.\n-- ---------------------------------------------------------------------------\n\n-- ===========================================================================\n-- \u00A71 \u2014 the push, as 005 defined it\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 -- Does THIS tenant have a calendar at all?\n --\n -- Added here, not inherited from 005. The vault secrets are per POOL and the\n -- integration is per TENANT, so without this every appointment write in the\n -- pool posted to the sync function \u2014 including for tenants that never\n -- connected Google. On the cluster this was installed against that is 354.989\n -- appointments across several tenants, all of them paying for an HTTP round\n -- trip whose only possible answer is \"no integration\".\n --\n -- The function would have answered correctly; the cost is the asking. Indexed\n -- by tenant (idx_plg_calendar_integrations_tenant), so the guard is a lookup,\n -- not a scan.\n --\n -- A tenant who DISCONNECTS stops pushing from that moment, which means a\n -- booking deleted afterwards leaves its Google event behind. That is the right\n -- trade: the alternative is pushing on behalf of someone who revoked us.\n IF NOT EXISTS (\n SELECT 1 FROM public.plg_calendar_integrations i\n WHERE i.tenant_id = rec.tenant_id AND i.active\n ) 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 -- 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\n-- ===========================================================================\n-- \u00A72 \u2014 the trigger 007 left out\n--\n-- AFTER INSERT OR UPDATE OR DELETE: a booking that is moved or cancelled has to\n-- reach Google too, and a trigger that only fired on INSERT would leave the\n-- calendar showing an appointment the CRM no longer has.\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\nCOMMENT ON FUNCTION public.fn_gcal_push_booking() IS\n 'Pushes a booking to Google Calendar over pg_net, authorised by the vault secret gcal_outbound_secret. Inert while the vault secrets are absent (008).';\n";
|
|
9
|
+
export declare const MIGRATION_009_THE_SERVICE_RUNS_THE_SYNC = "-- ============================================================================\n-- google-calendar 009_the_service_runs_the_sync \u2014 the Google Calendar data\n-- plane moves to the integration service (ADR 0003 phase 2, wave 3).\n--\n-- User actions run AS the verified caller and machine actions (a booking push,\n-- a webhook pull, watch renewal) AS the store's owner, both under RLS: the\n-- integrations, channels and sync log already admit a member. What only a\n-- machine writes gets the service role, table by table, with an explicit\n-- policy: the webhook delivery log and the automation outbox. And the four\n-- server-only RPCs the sync calls (002/003: service_role only) get EXECUTE for\n-- the service role, where they exist on the cluster.\n--\n-- Guarded: the role is an operator step, and a cluster may not have the RPCs.\n-- Replay-safe.\n-- ============================================================================\n\nDO $$\nDECLARE\n t text;\n f text;\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') THEN\n RETURN;\n END IF;\n\n FOREACH t IN ARRAY ARRAY['plg_calendar_webhook_deliveries', 'plg_calendar_automation_outbox'] LOOP\n IF to_regclass('public.' || t) IS NULL THEN CONTINUE; END IF;\n EXECUTE format('GRANT SELECT, INSERT, UPDATE ON public.%I TO fayz_integration', t);\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_service', t);\n EXECUTE format('CREATE POLICY %I ON public.%I FOR ALL TO fayz_integration USING (true) WITH CHECK (true)', t || '_service', t);\n END LOOP;\n\n -- The webhook resolves a channel by its Google channel id before it knows the\n -- tenant: a read of the channels table, and nothing else on it.\n IF to_regclass('public.plg_calendar_channels') IS NOT NULL THEN\n GRANT SELECT ON public.plg_calendar_channels TO fayz_integration;\n DROP POLICY IF EXISTS plg_calendar_channels_service_read ON public.plg_calendar_channels;\n CREATE POLICY plg_calendar_channels_service_read ON public.plg_calendar_channels FOR SELECT TO fayz_integration USING (true);\n END IF;\n\n FOREACH f IN ARRAY 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 ] LOOP\n IF to_regprocedure(f) IS NOT NULL THEN\n EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO fayz_integration', f);\n END IF;\n END LOOP;\nEND $$;\n";
|
|
9
10
|
export declare const MIGRATIONS: Array<{
|
|
10
11
|
id: string;
|
|
11
12
|
sql: string;
|
|
@@ -1 +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,+BAA+B,4nUAuN3C,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,
|
|
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,+BAA+B,4nUAuN3C,CAAA;AAED,eAAO,MAAM,uCAAuC,2jFAmDnD,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAUzD,CAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin's server-side code, for the integration service (ADR 0003 phase
|
|
3
|
+
* 2): the pure Google Calendar mapping the sync runs, and the table names.
|
|
4
|
+
* Its own entry so the service never pulls React through this package.
|
|
5
|
+
*/
|
|
6
|
+
export { appointmentRowToEvent, channelRowToChannel, eventTimes, resolveTargetChannel, } from '../integrations/google-calendar/mapping';
|
|
7
|
+
export { GCAL_TABLES } from '../integrations/google-calendar/data/tables';
|
|
8
|
+
export type { ChannelDirection } from '../integrations/google-calendar/types';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,UAAU,EACV,oBAAoB,GACrB,MAAM,yCAAyC,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,6CAA6C,CAAA;AACzE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"status": "beta",
|
|
5
5
|
"dependencies": []
|
|
6
6
|
},
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.27.0",
|
|
8
8
|
"description": "Fayz SDK — plugin-agenda plugin",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"sideEffects": [
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
"./public": {
|
|
22
22
|
"types": "./dist/public/index.d.ts",
|
|
23
23
|
"import": "./dist/public/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./server": {
|
|
26
|
+
"types": "./dist/server/index.d.ts",
|
|
27
|
+
"import": "./dist/server/index.js"
|
|
24
28
|
}
|
|
25
29
|
},
|
|
26
30
|
"files": [
|
|
@@ -42,12 +46,12 @@
|
|
|
42
46
|
"@fullcalendar/resource-timegrid": "^6.1.0",
|
|
43
47
|
"@fullcalendar/timegrid": "^6.1.20",
|
|
44
48
|
"@tanstack/react-table": "^8.20.0",
|
|
45
|
-
"@fayz-ai/admin": "^0.
|
|
46
|
-
"@fayz-ai/core": "^0.
|
|
47
|
-
"@fayz-ai/financial": "^0.1.
|
|
48
|
-
"@fayz-ai/forms": "^0.1.
|
|
49
|
-
"@fayz-ai/plugin-forms": "^0.15.
|
|
50
|
-
"@fayz-ai/ui": "^0.
|
|
49
|
+
"@fayz-ai/admin": "^0.51.0",
|
|
50
|
+
"@fayz-ai/core": "^0.50.0",
|
|
51
|
+
"@fayz-ai/financial": "^0.1.2",
|
|
52
|
+
"@fayz-ai/forms": "^0.1.2",
|
|
53
|
+
"@fayz-ai/plugin-forms": "^0.15.3",
|
|
54
|
+
"@fayz-ai/ui": "^0.50.0"
|
|
51
55
|
},
|
|
52
56
|
"devDependencies": {
|
|
53
57
|
"@types/react": "^18.3.0",
|