@fayz-ai/db 0.10.0 → 0.12.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.
Files changed (34) hide show
  1. package/dist/index.cjs +39 -0
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +39 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/schema/spine.d.ts +553 -0
  8. package/dist/schema/spine.d.ts.map +1 -1
  9. package/migrations/025_created_by.sql +75 -0
  10. package/migrations/026_audit_trail.sql +73 -0
  11. package/migrations/027_domain_events.sql +266 -0
  12. package/migrations/028_tenant_scoped_token.sql +127 -0
  13. package/migrations/029_connections.sql +186 -0
  14. package/migrations/030_effect_idempotency.sql +159 -0
  15. package/migrations/031_sync_run_message.sql +39 -0
  16. package/migrations/032_connection_secrets.sql +227 -0
  17. package/migrations/033_sync_schedule.sql +651 -0
  18. package/migrations/034_custom_fields.sql +55 -0
  19. package/migrations/035_field_registry.sql +148 -0
  20. package/migrations/036_analytics_run_batch.sql +84 -0
  21. package/migrations/037_sync_tick_one_at_a_time.sql +256 -0
  22. package/migrations/038_onboarding_responses.sql +103 -0
  23. package/migrations/039_unit_tree.sql +270 -0
  24. package/migrations/040_resource_grants.sql +474 -0
  25. package/migrations/041_scoped_columns.sql +192 -0
  26. package/migrations/042_unit_scope_policies.sql +145 -0
  27. package/migrations/043_view_invoker.sql +81 -0
  28. package/migrations/044_unit_member_facts.sql +47 -0
  29. package/migrations/045_unit_entry.sql +236 -0
  30. package/migrations/046_membership_visible_to_members.sql +85 -0
  31. package/migrations/047_tasks.sql +266 -0
  32. package/migrations/048_every_login_is_a_person.sql +190 -0
  33. package/migrations/049_bookable_people.sql +126 -0
  34. package/package.json +7 -4
@@ -0,0 +1,159 @@
1
+ -- ============================================================================
2
+ -- 030_effect_idempotency.sql — the run row IS the claim.
3
+ --
4
+ -- 029 drew the line the request plane needs: a QUOTE is not a run (no fetched,
5
+ -- no written, no cursor, and an `anon` shopper with no tenant-scoped write path
6
+ -- to file one with), while the EFFECTS a quote leads to — capture the payment,
7
+ -- buy the label — are ordinary rows of plg_sync_runs.
8
+ --
9
+ -- What 029 could not express is the half of an effect that the sync plane never
10
+ -- needed: nobody double-clicks a cron. A gateway that re-delivers its webhook,
11
+ -- and a buyer who clicks "pagar" twice, both arrive as a second call to capture
12
+ -- the same order for the same amount. The sync plane's answer — "the run is
13
+ -- filed after the work" — leaves exactly the window in which the second call
14
+ -- charges again.
15
+ --
16
+ -- So the claim and the run become one statement. `plg_claim_effect` inserts the
17
+ -- run row with the buyer-derived key and lets the unique index decide the race:
18
+ -- the caller that inserted owns the effect, the caller that conflicted is handed
19
+ -- the FIRST run's id and told not to call the provider. Two connectors need this
20
+ -- (AppMax capture, Melhor Envio label), which is why it is here and not in
21
+ -- either of them.
22
+ --
23
+ -- WHY A COLUMN AND NOT A TABLE. A second table would have to be kept in
24
+ -- agreement with plg_sync_runs about which effects happened — two sources of
25
+ -- truth for one fact, and the failure mode is a claim with no run behind it (or
26
+ -- the reverse) that nobody notices until a customer is charged twice. The claim
27
+ -- and the record of the effect are the same row on purpose.
28
+ --
29
+ -- The column is NULLABLE and the index PARTIAL: an ordinary sync run carries no
30
+ -- key and is completely unaffected. Nothing about the sync plane changes here.
31
+ -- ============================================================================
32
+
33
+ ALTER TABLE public.plg_sync_runs
34
+ ADD COLUMN IF NOT EXISTS idempotency_key text;
35
+
36
+ -- Scoped to (tenant, connector) rather than to the connection: an order is
37
+ -- captured once per gateway for a tenant, whichever instance of that gateway
38
+ -- answered. Including tenant_id is not decoration — a key is derived from an
39
+ -- order id, and two tenants of one pool must never be able to make each other's
40
+ -- capture look like a duplicate that was already done.
41
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_sync_runs_effect_key
42
+ ON public.plg_sync_runs (tenant_id, connector_id, idempotency_key)
43
+ WHERE idempotency_key IS NOT NULL;
44
+
45
+ COMMENT ON COLUMN public.plg_sync_runs.idempotency_key IS
46
+ 'Buyer-derived key for an effect on the request plane (capture, label). NULL '
47
+ 'for every sync run and for every quote — a quote is not a run at all. Unique '
48
+ 'per (tenant, connector) so a re-delivered webhook claims the first run '
49
+ 'instead of performing the effect a second time.';
50
+
51
+ -- ── Claim ───────────────────────────────────────────────────────────────────
52
+ -- Returns (run_id, claimed). `claimed = false` means the key already ran: hand
53
+ -- back the first run and DO NOT call the provider.
54
+ --
55
+ -- The tenant is resolved from the connection row and never taken from the
56
+ -- caller — the same discipline plugbank_import_movements follows, and the reason
57
+ -- a compromised data-plane call cannot file an effect against a tenant it does
58
+ -- not hold a connection for.
59
+ CREATE OR REPLACE FUNCTION public.plg_claim_effect(
60
+ p_connection_id uuid,
61
+ p_idempotency_key text,
62
+ p_direction text,
63
+ p_trigger_kind text,
64
+ p_stream text DEFAULT NULL
65
+ )
66
+ RETURNS TABLE (run_id uuid, claimed boolean)
67
+ LANGUAGE plpgsql
68
+ SECURITY DEFINER
69
+ SET search_path = public
70
+ AS $fn$
71
+ DECLARE
72
+ v_tenant_id uuid;
73
+ v_connector_id text;
74
+ v_id uuid;
75
+ BEGIN
76
+ IF p_idempotency_key IS NULL OR btrim(p_idempotency_key) = '' THEN
77
+ -- An effect nobody can name twice is an effect that runs twice. There is no
78
+ -- "just this once" path, because that path is the one a retry takes.
79
+ RAISE EXCEPTION 'plg_claim_effect requires an idempotency key';
80
+ END IF;
81
+
82
+ SELECT c.tenant_id, c.connector_id INTO v_tenant_id, v_connector_id
83
+ FROM public.plg_connections c
84
+ WHERE c.id = p_connection_id;
85
+
86
+ IF v_tenant_id IS NULL THEN
87
+ RAISE EXCEPTION 'plg_claim_effect: no connection %', p_connection_id;
88
+ END IF;
89
+
90
+ INSERT INTO public.plg_sync_runs
91
+ (tenant_id, connection_id, connector_id, direction, trigger_kind, status, stream, idempotency_key)
92
+ VALUES
93
+ (v_tenant_id, p_connection_id, v_connector_id, p_direction, p_trigger_kind, 'running', p_stream, p_idempotency_key)
94
+ ON CONFLICT (tenant_id, connector_id, idempotency_key) WHERE idempotency_key IS NOT NULL
95
+ DO NOTHING
96
+ RETURNING id INTO v_id;
97
+
98
+ IF v_id IS NOT NULL THEN
99
+ RETURN QUERY SELECT v_id, true;
100
+ RETURN;
101
+ END IF;
102
+
103
+ SELECT r.id INTO v_id
104
+ FROM public.plg_sync_runs r
105
+ WHERE r.tenant_id = v_tenant_id
106
+ AND r.connector_id = v_connector_id
107
+ AND r.idempotency_key = p_idempotency_key;
108
+
109
+ RETURN QUERY SELECT v_id, false;
110
+ END;
111
+ $fn$;
112
+
113
+ -- ── Settle ─────────────────────────────────────────────────────────────────
114
+ -- Closes the run the claim opened. `finished_at IS NULL` in the WHERE is the
115
+ -- guard that matters: a replayed webhook cannot rewrite an effect that already
116
+ -- has a verdict, so "captured" never becomes "error" because the gateway
117
+ -- re-sent an old event.
118
+ CREATE OR REPLACE FUNCTION public.plg_settle_effect(
119
+ p_run_id uuid,
120
+ p_status text,
121
+ p_stats jsonb DEFAULT '{}'::jsonb,
122
+ p_error text DEFAULT NULL
123
+ )
124
+ RETURNS boolean
125
+ LANGUAGE plpgsql
126
+ SECURITY DEFINER
127
+ SET search_path = public
128
+ AS $fn$
129
+ DECLARE
130
+ v_rows integer;
131
+ BEGIN
132
+ UPDATE public.plg_sync_runs
133
+ SET status = p_status,
134
+ stats = COALESCE(p_stats, '{}'::jsonb),
135
+ error = p_error,
136
+ finished_at = now()
137
+ WHERE id = p_run_id
138
+ AND finished_at IS NULL;
139
+ GET DIAGNOSTICS v_rows = ROW_COUNT;
140
+ RETURN v_rows > 0;
141
+ END;
142
+ $fn$;
143
+
144
+ -- Server-side only, exactly like the runs table itself: 029 grants
145
+ -- `authenticated` SELECT and nothing else, and a SECURITY DEFINER function that
146
+ -- inserts runs would hand back the INSERT that grant deliberately withholds.
147
+ -- The shopper is `anon` and reaches neither.
148
+ REVOKE ALL ON FUNCTION public.plg_claim_effect(uuid, text, text, text, text) FROM PUBLIC;
149
+ REVOKE ALL ON FUNCTION public.plg_claim_effect(uuid, text, text, text, text) FROM anon, authenticated;
150
+ GRANT EXECUTE ON FUNCTION public.plg_claim_effect(uuid, text, text, text, text) TO service_role;
151
+
152
+ REVOKE ALL ON FUNCTION public.plg_settle_effect(uuid, text, jsonb, text) FROM PUBLIC;
153
+ REVOKE ALL ON FUNCTION public.plg_settle_effect(uuid, text, jsonb, text) FROM anon, authenticated;
154
+ GRANT EXECUTE ON FUNCTION public.plg_settle_effect(uuid, text, jsonb, text) TO service_role;
155
+
156
+ COMMENT ON FUNCTION public.plg_claim_effect(uuid, text, text, text, text) IS
157
+ 'Claim an effect on the request plane. The run row is the claim: the caller '
158
+ 'that inserts owns it, the caller that conflicts gets the first run back with '
159
+ 'claimed=false and must not call the provider. Quotes never come here.';
@@ -0,0 +1,39 @@
1
+ -- ============================================================================
2
+ -- 031_sync_run_message.sql — the one thing the generic shape split in two.
3
+ --
4
+ -- Four connectors brought a bespoke sync log and every one of them had a single
5
+ -- `message`/`error` column carrying whatever the run had to say. 029 kept only
6
+ -- the failure half (`error`), on the reasonable-sounding theory that a
7
+ -- successful run says everything in `fetched`/`written`.
8
+ --
9
+ -- It does not. "Importado do extrato de 01/08 a 12/08, 3 duplicados ignorados"
10
+ -- is a successful run with something to say, and RankLayer's panel had to
11
+ -- reconstruct it as `run.error ?? run.stats.message` — an ad-hoc key inside a
12
+ -- free-form jsonb, which is precisely the convention the generic tables exist
13
+ -- to stop the sixth connector from reinventing.
14
+ --
15
+ -- So: one nullable text column, and the rule that goes with it —
16
+ -- error is what went WRONG. Null on a successful run, always.
17
+ -- message is what the run has to SAY. Set on any status, including 'error',
18
+ -- where it is the human sentence next to the machine one.
19
+ --
20
+ -- Additive and idempotent: every pool has already applied 029, and `fayz db
21
+ -- apply` re-runs a file whose checksum moved, so this must be safe twice.
22
+ -- ============================================================================
23
+
24
+ ALTER TABLE public.plg_sync_runs
25
+ ADD COLUMN IF NOT EXISTS message text;
26
+
27
+ COMMENT ON COLUMN public.plg_sync_runs.message IS
28
+ 'What the run has to say, on any status. `error` stays reserved for what went '
29
+ 'wrong, so a successful run with something to report no longer has to smuggle '
30
+ 'it through stats.';
31
+
32
+ -- No grant statement here, deliberately, and the omission is load-bearing.
33
+ --
34
+ -- A defensive `REVOKE INSERT, UPDATE, DELETE … FROM authenticated` looks like
35
+ -- belt and braces. It is the opposite: 029 is the file that owns the write ban,
36
+ -- and re-asserting it here means the bench's negative control (which deletes
37
+ -- 029's REVOKE and demands that C3 go red) passes with the ban gone. A second
38
+ -- REVOKE does not add a guarantee, it hides the loss of the first one.
39
+ -- The property is asserted instead — connections.regression.sql C10c.
@@ -0,0 +1,227 @@
1
+ -- ============================================================================
2
+ -- 032_connection_secrets.sql — the credential holder 029 promised and nobody built.
3
+ --
4
+ -- 029 wrote, on the table itself: "secrets belong to the platform credential
5
+ -- store, never to a row a connector's own browser code can write." That was the
6
+ -- right call and it left a hole, because the platform credential store does not
7
+ -- exist (FAY-1381). The consequence is not theoretical:
8
+ --
9
+ -- * Bling declares authKind 'oauth' and its Connect button is disabled,
10
+ -- because there is nowhere to put the refresh token the consent returns.
11
+ -- * AppMax and Melhor Envio declare an api-key field, the hub routes it to
12
+ -- ConnectorCredentialSink, and no app installs a sink — so the honest
13
+ -- outcome is "cannot save", which is where they are now.
14
+ -- * google-calendar still cannot migrate off its bespoke table for exactly
15
+ -- this reason; 029 said so in its own header.
16
+ --
17
+ -- Three connectors, one missing thing. This file is that thing, scoped to the
18
+ -- pool rather than the platform: Supabase Vault is installed here
19
+ -- (`supabase_vault`, encrypted at rest, keyed outside the table), so the pool
20
+ -- CAN hold a tenant credential safely today. When the platform broker ships,
21
+ -- the data plane keeps calling the same three functions and the storage moves
22
+ -- underneath — which is the point of putting the functions in front of it.
23
+ --
24
+ -- ── Why a table of ids and not a column on plg_connections ──────────────────
25
+ -- Because 029's argument still holds in full. A credential column is a column
26
+ -- some later `select *` returns, some later RLS policy exposes, and some later
27
+ -- connector writes from the browser. Nothing here changes that: plg_connections
28
+ -- keeps no credential, and the bench's C1 keeps proving it.
29
+ --
30
+ -- What this table holds is a POINTER — `(connection, key) → vault secret id`.
31
+ -- The secret itself lives in vault.secrets, encrypted, reachable only through a
32
+ -- SECURITY DEFINER function that no client role may execute.
33
+ --
34
+ -- ── Why the mapping table at all, when the vault name is deterministic ──────
35
+ -- `plg:<connection_id>:<key>` could be recomputed instead of stored. The table
36
+ -- earns its place three times over: ON DELETE CASCADE means disconnecting a
37
+ -- connection takes its credentials with it without anyone remembering to; the
38
+ -- keys of a connection can be enumerated without granting a single privilege on
39
+ -- the vault schema; and the drop path deletes what it actually stored rather
40
+ -- than what it guesses the name should have been.
41
+ --
42
+ -- ── Why the table is granted to nobody ──────────────────────────────────────
43
+ -- It holds ids, not secrets. It is still shut to `anon` and `authenticated`,
44
+ -- because an id is a handle, and the whole design rests on the handle being
45
+ -- unreachable from the browser. A tenant that can read the handle is one
46
+ -- "GRANT SELECT ON vault.decrypted_secrets TO authenticated for debugging"
47
+ -- away from reading the credential — and that grant is a one-liner somebody
48
+ -- will be tempted by at 2am. Take the handle away and the temptation has
49
+ -- nothing to act on.
50
+ --
51
+ -- Idempotent, because `fayz db apply` re-runs a file whose checksum moved.
52
+ -- ============================================================================
53
+
54
+ -- ── The pointer ─────────────────────────────────────────────────────────────
55
+ -- One row per (connection, key). `key` is the connector's own field name —
56
+ -- 'access_token' / 'refresh_token' for an OAuth connector, the declared field
57
+ -- key ('apiKey', 'token') for an api-key one. Deliberately free text: the set
58
+ -- of keys belongs to the ConnectorDefinition, and a CHECK here would mean every
59
+ -- new connector needs a migration.
60
+ CREATE TABLE IF NOT EXISTS public.plg_connection_secrets (
61
+ connection_id uuid NOT NULL REFERENCES public.plg_connections(id) ON DELETE CASCADE,
62
+ key text NOT NULL,
63
+ vault_secret_id uuid NOT NULL,
64
+ created_at timestamptz NOT NULL DEFAULT now(),
65
+ updated_at timestamptz NOT NULL DEFAULT now(),
66
+ PRIMARY KEY (connection_id, key)
67
+ );
68
+
69
+ COMMENT ON TABLE public.plg_connection_secrets IS
70
+ 'Pointer table: (connection, key) -> vault secret id. Holds NO secret value '
71
+ 'and is granted to NO client role. The three plg_*_connection_secret(s) '
72
+ 'functions are the only way in or out, and only service_role may call them.';
73
+
74
+ -- ── Grants: none, and the RLS has to agree ─────────────────────────────────
75
+ -- 011 shut `anon` out of every new table in this schema and left `authenticated`
76
+ -- born with the full set — the same trap 029 documented. Both are stripped, and
77
+ -- nothing is granted back. RLS is enabled with NO policy, which for a
78
+ -- non-owner role means deny-all; the REVOKE and the empty policy set have to
79
+ -- agree, or the next person to "fix the grant" reopens it silently.
80
+ REVOKE ALL ON public.plg_connection_secrets FROM PUBLIC;
81
+ REVOKE ALL ON public.plg_connection_secrets FROM anon, authenticated;
82
+
83
+ ALTER TABLE public.plg_connection_secrets ENABLE ROW LEVEL SECURITY;
84
+
85
+ -- ── Store ───────────────────────────────────────────────────────────────────
86
+ -- Upsert semantics, and they are load-bearing: Bling's refresh token is
87
+ -- replaced on every renewal, and a store that appended would leave the pool
88
+ -- holding a growing pile of dead credentials — each one still decryptable.
89
+ -- One connection+key is one vault secret, forever.
90
+ --
91
+ -- The vault name is derived, never supplied, so a caller cannot aim this at
92
+ -- another connection's secret or at an unrelated vault entry.
93
+ CREATE OR REPLACE FUNCTION public.plg_store_connection_secret(
94
+ p_connection_id uuid,
95
+ p_key text,
96
+ p_secret text
97
+ )
98
+ RETURNS uuid
99
+ LANGUAGE plpgsql
100
+ SECURITY DEFINER
101
+ SET search_path = public
102
+ AS $fn$
103
+ DECLARE
104
+ v_name text;
105
+ v_id uuid;
106
+ BEGIN
107
+ IF p_connection_id IS NULL OR coalesce(p_key, '') = '' THEN
108
+ RAISE EXCEPTION 'connection id and key are required';
109
+ END IF;
110
+ -- The key becomes part of a vault name; keep it to an identifier so it cannot
111
+ -- carry a colon and collide with another connection's namespace.
112
+ IF p_key !~ '^[a-zA-Z0-9_.-]{1,64}$' THEN
113
+ RAISE EXCEPTION 'invalid secret key';
114
+ END IF;
115
+ IF p_secret IS NULL OR length(p_secret) = 0 THEN
116
+ RAISE EXCEPTION 'refusing to store an empty secret';
117
+ END IF;
118
+
119
+ v_name := 'plg:' || p_connection_id::text || ':' || p_key;
120
+
121
+ SELECT vault_secret_id INTO v_id
122
+ FROM public.plg_connection_secrets
123
+ WHERE connection_id = p_connection_id AND key = p_key;
124
+
125
+ -- A vault entry may outlive its pointer (a manual DELETE, a half-applied
126
+ -- earlier run). Adopt it rather than create a second one under a name the
127
+ -- vault's unique index would reject anyway.
128
+ IF v_id IS NULL THEN
129
+ SELECT id INTO v_id FROM vault.secrets WHERE name = v_name;
130
+ END IF;
131
+
132
+ IF v_id IS NULL THEN
133
+ v_id := vault.create_secret(p_secret, v_name, 'fayz connector credential');
134
+ ELSE
135
+ PERFORM vault.update_secret(v_id, p_secret);
136
+ END IF;
137
+
138
+ INSERT INTO public.plg_connection_secrets (connection_id, key, vault_secret_id)
139
+ VALUES (p_connection_id, p_key, v_id)
140
+ ON CONFLICT (connection_id, key)
141
+ DO UPDATE SET vault_secret_id = EXCLUDED.vault_secret_id, updated_at = now();
142
+
143
+ RETURN v_id;
144
+ END;
145
+ $fn$;
146
+
147
+ -- ── Read ────────────────────────────────────────────────────────────────────
148
+ -- What the data plane calls at run time, and the only decrypt path that exists.
149
+ -- Returns NULL rather than raising when there is nothing stored: "not connected"
150
+ -- is an ordinary answer for a connector, not an exception.
151
+ CREATE OR REPLACE FUNCTION public.plg_read_connection_secret(
152
+ p_connection_id uuid,
153
+ p_key text
154
+ )
155
+ RETURNS text
156
+ LANGUAGE plpgsql
157
+ SECURITY DEFINER
158
+ SET search_path = public
159
+ AS $fn$
160
+ DECLARE
161
+ v_secret text;
162
+ BEGIN
163
+ SELECT d.decrypted_secret INTO v_secret
164
+ FROM public.plg_connection_secrets s
165
+ JOIN vault.decrypted_secrets d ON d.id = s.vault_secret_id
166
+ WHERE s.connection_id = p_connection_id AND s.key = p_key;
167
+ RETURN v_secret;
168
+ END;
169
+ $fn$;
170
+
171
+ -- ── Drop ────────────────────────────────────────────────────────────────────
172
+ -- Disconnect. 029's C9g rule stands — a disconnect FREEZES the mirror and keeps
173
+ -- the connection row and its history — but the credential is exactly the thing
174
+ -- that must not survive it, so this deletes the vault entries as well as the
175
+ -- pointers. Returns the count so the caller can log a real number.
176
+ --
177
+ -- The CASCADE on the table only removes pointers when the whole connection is
178
+ -- deleted; the vault rows would be orphaned. That is why this function exists
179
+ -- rather than a `DELETE FROM plg_connection_secrets`.
180
+ CREATE OR REPLACE FUNCTION public.plg_drop_connection_secrets(p_connection_id uuid)
181
+ RETURNS integer
182
+ LANGUAGE plpgsql
183
+ SECURITY DEFINER
184
+ SET search_path = public
185
+ AS $fn$
186
+ DECLARE
187
+ v_ids uuid[];
188
+ v_rows integer;
189
+ BEGIN
190
+ SELECT array_agg(vault_secret_id) INTO v_ids
191
+ FROM public.plg_connection_secrets WHERE connection_id = p_connection_id;
192
+
193
+ DELETE FROM public.plg_connection_secrets WHERE connection_id = p_connection_id;
194
+ GET DIAGNOSTICS v_rows = ROW_COUNT;
195
+
196
+ IF v_ids IS NOT NULL THEN
197
+ DELETE FROM vault.secrets WHERE id = ANY (v_ids);
198
+ END IF;
199
+
200
+ RETURN v_rows;
201
+ END;
202
+ $fn$;
203
+
204
+ -- ── The grant discipline, three times ──────────────────────────────────────
205
+ -- PUBLIC first: EXECUTE on a new function is granted to PUBLIC by default, and
206
+ -- revoking from anon/authenticated alone leaves it reachable through that.
207
+ REVOKE ALL ON FUNCTION public.plg_store_connection_secret(uuid, text, text) FROM PUBLIC;
208
+ REVOKE ALL ON FUNCTION public.plg_store_connection_secret(uuid, text, text) FROM anon, authenticated;
209
+ GRANT EXECUTE ON FUNCTION public.plg_store_connection_secret(uuid, text, text) TO service_role;
210
+
211
+ REVOKE ALL ON FUNCTION public.plg_read_connection_secret(uuid, text) FROM PUBLIC;
212
+ REVOKE ALL ON FUNCTION public.plg_read_connection_secret(uuid, text) FROM anon, authenticated;
213
+ GRANT EXECUTE ON FUNCTION public.plg_read_connection_secret(uuid, text) TO service_role;
214
+
215
+ REVOKE ALL ON FUNCTION public.plg_drop_connection_secrets(uuid) FROM PUBLIC;
216
+ REVOKE ALL ON FUNCTION public.plg_drop_connection_secrets(uuid) FROM anon, authenticated;
217
+ GRANT EXECUTE ON FUNCTION public.plg_drop_connection_secrets(uuid) TO service_role;
218
+
219
+ COMMENT ON FUNCTION public.plg_store_connection_secret(uuid, text, text) IS
220
+ 'Place or replace one credential for a connection. Vault name is derived from '
221
+ 'the connection id and key, so a refresh replaces rather than accumulates. '
222
+ 'service_role only.';
223
+ COMMENT ON FUNCTION public.plg_read_connection_secret(uuid, text) IS
224
+ 'The only decrypt path. Called by the data plane at run time. service_role only.';
225
+ COMMENT ON FUNCTION public.plg_drop_connection_secrets(uuid) IS
226
+ 'Disconnect: removes the pointers AND the vault entries behind them. '
227
+ 'service_role only.';