@fayz-ai/plugin-agenda 0.16.1 → 0.17.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/index.js
CHANGED
|
@@ -10585,6 +10585,222 @@ END $$;
|
|
|
10585
10585
|
-- pool must be REVOKED AT GOOGLE, not assumed gone \u2014 which is also why every
|
|
10586
10586
|
-- tenant re-consents after this file runs.
|
|
10587
10587
|
`;
|
|
10588
|
+
var MIGRATION_008_THE_OUTBOUND_HALF = `-- ---------------------------------------------------------------------------
|
|
10589
|
+
-- Google Calendar \u2014 the outbound half that 007 does not install
|
|
10590
|
+
-- ---------------------------------------------------------------------------
|
|
10591
|
+
-- 007 is the credential-free install: it creates the five plg_calendar_* tables
|
|
10592
|
+
-- from zero, with no column a Google grant could ever sit in. What it does NOT
|
|
10593
|
+
-- create is the trigger on public.appointments that pushes a booking OUT to
|
|
10594
|
+
-- Google, because that trigger never lived in those tables.
|
|
10595
|
+
--
|
|
10596
|
+
-- Only 001 and 002 create \`trg_gcal_push\`, and 001 is the file whose credential
|
|
10597
|
+
-- columns 007 exists to prevent. So there is no ordering of the existing files
|
|
10598
|
+
-- that gives a fresh pool the outbound direction without also giving it, for a
|
|
10599
|
+
-- while, a plaintext refresh-token column.
|
|
10600
|
+
--
|
|
10601
|
+
-- The result on a pool installed by 007 alone: Google \u2192 Fayz works, Fayz \u2192
|
|
10602
|
+
-- Google is silent. Not an error, not a warning \u2014 appointments simply never
|
|
10603
|
+
-- reach the calendar. Discovered on cluster-squash-test after 007 came up green
|
|
10604
|
+
-- and the tables were correct.
|
|
10605
|
+
--
|
|
10606
|
+
-- This file is the missing half, and nothing else:
|
|
10607
|
+
--
|
|
10608
|
+
-- \xA71 public.fn_gcal_push_booking \u2014 verbatim the definition 005 settled on:
|
|
10609
|
+
-- vault first (\`gcal_sync_url\` + \`gcal_outbound_secret\`), the legacy GUC
|
|
10610
|
+
-- only as a fallback that WARNs, and a no-op when neither is configured.
|
|
10611
|
+
-- Reproduced rather than referenced because 005 cannot run here: it
|
|
10612
|
+
-- replaces a function 001 created, and 001 is the file we are avoiding.
|
|
10613
|
+
-- \xA72 trg_gcal_push on public.appointments \u2014 the shape 002 re-asserts.
|
|
10614
|
+
--
|
|
10615
|
+
-- A pool that already ran 001\u2192006 gets an identical function and an identical
|
|
10616
|
+
-- trigger, which is why this is safe to apply anywhere: every statement is
|
|
10617
|
+
-- CREATE OR REPLACE / DROP-then-CREATE.
|
|
10618
|
+
--
|
|
10619
|
+
-- WITHOUT THE VAULT SECRETS THIS IS INERT ON PURPOSE. The function reads
|
|
10620
|
+
-- \`gcal_sync_url\` and \`gcal_outbound_secret\` from the vault and returns without
|
|
10621
|
+
-- pushing when they are absent, so installing the trigger before the secrets
|
|
10622
|
+
-- exist cannot break an INSERT into appointments \u2014 the most load-bearing table
|
|
10623
|
+
-- the agenda has.
|
|
10624
|
+
--
|
|
10625
|
+
-- Idempotent and replay-safe.
|
|
10626
|
+
-- ---------------------------------------------------------------------------
|
|
10627
|
+
|
|
10628
|
+
-- ===========================================================================
|
|
10629
|
+
-- \xA71 \u2014 the push, as 005 defined it
|
|
10630
|
+
-- ===========================================================================
|
|
10631
|
+
CREATE OR REPLACE FUNCTION public.fn_gcal_push_booking() RETURNS trigger
|
|
10632
|
+
LANGUAGE plpgsql
|
|
10633
|
+
SECURITY DEFINER
|
|
10634
|
+
SET search_path = public, extensions, net, vault
|
|
10635
|
+
AS $$
|
|
10636
|
+
DECLARE
|
|
10637
|
+
url text;
|
|
10638
|
+
key text;
|
|
10639
|
+
legacy boolean := false;
|
|
10640
|
+
rec record := COALESCE(NEW, OLD);
|
|
10641
|
+
op text := lower(TG_OP);
|
|
10642
|
+
corr text := NULLIF(current_setting('app.booking_correlation_id', true), '');
|
|
10643
|
+
BEGIN
|
|
10644
|
+
-- Unchanged from 004: inbound Google writes mark themselves, and a write
|
|
10645
|
+
-- carrying that marker is never echoed back.
|
|
10646
|
+
IF current_setting('app.booking_origin', true) = 'google' THEN
|
|
10647
|
+
RETURN COALESCE(NEW, OLD);
|
|
10648
|
+
END IF;
|
|
10649
|
+
|
|
10650
|
+
-- Vault first. Wrapped because a pool without the vault extension must
|
|
10651
|
+
-- degrade to the fallback, not abort the booking write.
|
|
10652
|
+
BEGIN
|
|
10653
|
+
SELECT decrypted_secret INTO url
|
|
10654
|
+
FROM vault.decrypted_secrets WHERE name = 'gcal_sync_url'
|
|
10655
|
+
ORDER BY created_at DESC LIMIT 1;
|
|
10656
|
+
SELECT decrypted_secret INTO key
|
|
10657
|
+
FROM vault.decrypted_secrets WHERE name = 'gcal_outbound_secret'
|
|
10658
|
+
ORDER BY created_at DESC LIMIT 1;
|
|
10659
|
+
EXCEPTION WHEN OTHERS THEN
|
|
10660
|
+
url := NULL;
|
|
10661
|
+
key := NULL;
|
|
10662
|
+
END;
|
|
10663
|
+
|
|
10664
|
+
-- Legacy pools provisioned per 001's header keep working, and say so.
|
|
10665
|
+
IF NULLIF(url, '') IS NULL OR NULLIF(key, '') IS NULL THEN
|
|
10666
|
+
url := COALESCE(NULLIF(url, ''), current_setting('app.gcal_sync_url', true));
|
|
10667
|
+
key := COALESCE(NULLIF(key, ''), current_setting('app.gcal_service_key', true));
|
|
10668
|
+
legacy := NULLIF(key, '') IS NOT NULL;
|
|
10669
|
+
END IF;
|
|
10670
|
+
|
|
10671
|
+
-- Not configured is not an error: an unconfigured pool degrades to "Google is
|
|
10672
|
+
-- not updated", never to "the appointment could not be saved".
|
|
10673
|
+
IF NULLIF(url, '') IS NULL OR NULLIF(key, '') IS NULL THEN
|
|
10674
|
+
RETURN COALESCE(NEW, OLD);
|
|
10675
|
+
END IF;
|
|
10676
|
+
|
|
10677
|
+
IF legacy THEN
|
|
10678
|
+
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';
|
|
10679
|
+
END IF;
|
|
10680
|
+
|
|
10681
|
+
-- Does THIS tenant have a calendar at all?
|
|
10682
|
+
--
|
|
10683
|
+
-- Added here, not inherited from 005. The vault secrets are per POOL and the
|
|
10684
|
+
-- integration is per TENANT, so without this every appointment write in the
|
|
10685
|
+
-- pool posted to the sync function \u2014 including for tenants that never
|
|
10686
|
+
-- connected Google. On the cluster this was installed against that is 354.989
|
|
10687
|
+
-- appointments across several tenants, all of them paying for an HTTP round
|
|
10688
|
+
-- trip whose only possible answer is "no integration".
|
|
10689
|
+
--
|
|
10690
|
+
-- The function would have answered correctly; the cost is the asking. Indexed
|
|
10691
|
+
-- by tenant (idx_plg_calendar_integrations_tenant), so the guard is a lookup,
|
|
10692
|
+
-- not a scan.
|
|
10693
|
+
--
|
|
10694
|
+
-- A tenant who DISCONNECTS stops pushing from that moment, which means a
|
|
10695
|
+
-- booking deleted afterwards leaves its Google event behind. That is the right
|
|
10696
|
+
-- trade: the alternative is pushing on behalf of someone who revoked us.
|
|
10697
|
+
IF NOT EXISTS (
|
|
10698
|
+
SELECT 1 FROM public.plg_calendar_integrations i
|
|
10699
|
+
WHERE i.tenant_id = rec.tenant_id AND i.active
|
|
10700
|
+
) THEN
|
|
10701
|
+
RETURN COALESCE(NEW, OLD);
|
|
10702
|
+
END IF;
|
|
10703
|
+
|
|
10704
|
+
BEGIN
|
|
10705
|
+
PERFORM net.http_post(
|
|
10706
|
+
url := url,
|
|
10707
|
+
headers := jsonb_build_object(
|
|
10708
|
+
'Content-Type', 'application/json',
|
|
10709
|
+
-- REQUIRED. The deployed function authorises push_event on this header
|
|
10710
|
+
-- alone; without it every push 400s, silently, forever.
|
|
10711
|
+
'Authorization', 'Bearer ' || key
|
|
10712
|
+
),
|
|
10713
|
+
body := jsonb_build_object(
|
|
10714
|
+
'action', 'push_event',
|
|
10715
|
+
'op', op,
|
|
10716
|
+
'bookingId', rec.id,
|
|
10717
|
+
'tenantId', rec.tenant_id,
|
|
10718
|
+
-- Carried so a delete can still be honoured after the row is gone.
|
|
10719
|
+
'googleEventId', rec.metadata->>'googleCalendarEventId',
|
|
10720
|
+
'calendarId', rec.metadata->>'gcalCalendarId',
|
|
10721
|
+
'correlationId', COALESCE(corr, gen_random_uuid()::text)
|
|
10722
|
+
)
|
|
10723
|
+
);
|
|
10724
|
+
EXCEPTION WHEN OTHERS THEN
|
|
10725
|
+
-- Missing pg_net, a bad URL, a full queue: the appointment is still saved.
|
|
10726
|
+
-- The deployed trigger lacks this wrapper; 004 added it here after a pool
|
|
10727
|
+
-- with the settings present but the extension missing took an INSERT down.
|
|
10728
|
+
RAISE WARNING 'gcal push skipped for booking %: %', rec.id, SQLERRM;
|
|
10729
|
+
END;
|
|
10730
|
+
|
|
10731
|
+
RETURN COALESCE(NEW, OLD);
|
|
10732
|
+
END $$;
|
|
10733
|
+
|
|
10734
|
+
-- \xA72 \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
|
|
10735
|
+
-- Redefining the function stops the key being PREFERRED; it does not stop it
|
|
10736
|
+
-- being STORED. Settings live in pg_db_role_setting per (database, role), so the
|
|
10737
|
+
-- sweep is driven off the catalog rather than off the one ALTER DATABASE form
|
|
10738
|
+
-- \`001\` happened to show.
|
|
10739
|
+
--
|
|
10740
|
+
-- CONDITIONAL ON PURPOSE: the setting is cleared only where Vault already
|
|
10741
|
+
-- supplies \`gcal_outbound_secret\`, i.e. only where \xA71 provably never needs the
|
|
10742
|
+
-- fallback. Anywhere else it is left in place and reported, because clearing it
|
|
10743
|
+
-- there would disarm the push with no error anyone can see. On the live pool
|
|
10744
|
+
-- this loop finds nothing \u2014 the setting is not present on any role or database \u2014
|
|
10745
|
+
-- and that is the expected result, not a failure.
|
|
10746
|
+
DO $$
|
|
10747
|
+
DECLARE
|
|
10748
|
+
r record;
|
|
10749
|
+
has_vault boolean := false;
|
|
10750
|
+
BEGIN
|
|
10751
|
+
BEGIN
|
|
10752
|
+
SELECT EXISTS (
|
|
10753
|
+
SELECT 1 FROM vault.decrypted_secrets
|
|
10754
|
+
WHERE name = 'gcal_outbound_secret' AND NULLIF(decrypted_secret, '') IS NOT NULL
|
|
10755
|
+
) INTO has_vault;
|
|
10756
|
+
EXCEPTION WHEN OTHERS THEN
|
|
10757
|
+
has_vault := false;
|
|
10758
|
+
END;
|
|
10759
|
+
|
|
10760
|
+
FOR r IN
|
|
10761
|
+
SELECT d.datname, a.rolname
|
|
10762
|
+
FROM pg_db_role_setting s
|
|
10763
|
+
LEFT JOIN pg_database d ON d.oid = s.setdatabase
|
|
10764
|
+
LEFT JOIN pg_roles a ON a.oid = s.setrole
|
|
10765
|
+
WHERE EXISTS (
|
|
10766
|
+
SELECT 1 FROM unnest(s.setconfig) AS c WHERE c LIKE 'app.gcal_service_key=%'
|
|
10767
|
+
)
|
|
10768
|
+
LOOP
|
|
10769
|
+
IF NOT has_vault THEN
|
|
10770
|
+
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',
|
|
10771
|
+
r.datname, r.rolname;
|
|
10772
|
+
CONTINUE;
|
|
10773
|
+
END IF;
|
|
10774
|
+
BEGIN
|
|
10775
|
+
IF r.rolname IS NULL THEN
|
|
10776
|
+
EXECUTE format('ALTER DATABASE %I RESET app.gcal_service_key', r.datname);
|
|
10777
|
+
ELSIF r.datname IS NULL THEN
|
|
10778
|
+
EXECUTE format('ALTER ROLE %I RESET app.gcal_service_key', r.rolname);
|
|
10779
|
+
ELSE
|
|
10780
|
+
EXECUTE format('ALTER ROLE %I IN DATABASE %I RESET app.gcal_service_key', r.rolname, r.datname);
|
|
10781
|
+
END IF;
|
|
10782
|
+
EXCEPTION WHEN OTHERS THEN
|
|
10783
|
+
RAISE WARNING 'could not clear app.gcal_service_key (database %, role %): % \u2014 clear it by hand, and rotate the key either way',
|
|
10784
|
+
r.datname, r.rolname, SQLERRM;
|
|
10785
|
+
END;
|
|
10786
|
+
END LOOP;
|
|
10787
|
+
END $$;
|
|
10788
|
+
|
|
10789
|
+
-- ===========================================================================
|
|
10790
|
+
-- \xA72 \u2014 the trigger 007 left out
|
|
10791
|
+
--
|
|
10792
|
+
-- AFTER INSERT OR UPDATE OR DELETE: a booking that is moved or cancelled has to
|
|
10793
|
+
-- reach Google too, and a trigger that only fired on INSERT would leave the
|
|
10794
|
+
-- calendar showing an appointment the CRM no longer has.
|
|
10795
|
+
-- ===========================================================================
|
|
10796
|
+
DROP TRIGGER IF EXISTS trg_gcal_push ON public.appointments;
|
|
10797
|
+
CREATE TRIGGER trg_gcal_push
|
|
10798
|
+
AFTER INSERT OR UPDATE OR DELETE ON public.appointments
|
|
10799
|
+
FOR EACH ROW EXECUTE FUNCTION public.fn_gcal_push_booking();
|
|
10800
|
+
|
|
10801
|
+
COMMENT ON FUNCTION public.fn_gcal_push_booking() IS
|
|
10802
|
+
'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).';
|
|
10803
|
+
`;
|
|
10588
10804
|
|
|
10589
10805
|
// src/integrations/google-calendar/connector.ts
|
|
10590
10806
|
var googleCalendarConnector = {
|
|
@@ -10723,6 +10939,12 @@ function createGoogleCalendarPlugin() {
|
|
|
10723
10939
|
version: "0.9.3",
|
|
10724
10940
|
sql: MIGRATION_007_CREDENTIAL_FREE_INSTALL,
|
|
10725
10941
|
description: "Install the whole connector schema in its post-FAY-1359 shape, from zero, with no credential column at any point \u2014 the five tables the beauty pool had dropped, plus a gate that drops a token column wherever it is still found and refuses to finish if one survives"
|
|
10942
|
+
},
|
|
10943
|
+
{
|
|
10944
|
+
id: "google-calendar-008_the_outbound_half",
|
|
10945
|
+
version: "0.16.2",
|
|
10946
|
+
sql: MIGRATION_008_THE_OUTBOUND_HALF,
|
|
10947
|
+
description: "Install trg_gcal_push and the 005 push function on a pool that came up through 007. 007 creates the tables but not the trigger, and the only files that create it (001, 002) also create the credential columns 007 exists to prevent \u2014 so a fresh pool silently had inbound only. Inert until the vault secrets exist"
|
|
10726
10948
|
}
|
|
10727
10949
|
]
|
|
10728
10950
|
};
|