@jhizzard/termdeck 0.18.0 → 1.0.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.
@@ -0,0 +1,117 @@
1
+ -- Mnestra v0.4.1 — `mnestra doctor` SECURITY DEFINER probe wrappers
2
+ --
3
+ -- Sprint 51.5 T2 (TermDeck). Adds five SECURITY DEFINER helper functions
4
+ -- so the `mnestra doctor` subcommand (running under the supabase
5
+ -- service_role) can probe `cron.job_run_details`, `cron.job`, `vault.secrets`,
6
+ -- `information_schema.columns`, and `pg_proc` without granting raw schema
7
+ -- access to service_role.
8
+ --
9
+ -- Why SECURITY DEFINER: by default `cron.*` and `vault.*` are owned by
10
+ -- the `postgres` role and unreadable from service_role. The classical
11
+ -- alternative is to `grant usage on schema cron to service_role; grant
12
+ -- select on cron.job_run_details to service_role; …` — broader privilege
13
+ -- expansion than this lane needs. SECURITY DEFINER lets us read exactly
14
+ -- what the doctor needs without exposing the entire cron/vault surface.
15
+ --
16
+ -- Idempotent: every function uses CREATE OR REPLACE; every GRANT is
17
+ -- safe to re-run. No data mutation. Safe to re-apply on every install.
18
+
19
+ -- ── 1. cron.job_run_details lookup ───────────────────────────────────────
20
+ --
21
+ -- Returns the most recent N runs of a named cron job, projecting only
22
+ -- the columns the doctor needs (status, start/end timestamps, return
23
+ -- message). The doctor parses `return_message` for the rumen-tick /
24
+ -- graph-inference all-zeros pattern.
25
+
26
+ create or replace function mnestra_doctor_cron_runs(
27
+ p_jobname text,
28
+ p_limit int default 10
29
+ )
30
+ returns table (
31
+ jobname text,
32
+ status text,
33
+ start_time timestamptz,
34
+ end_time timestamptz,
35
+ return_message text
36
+ )
37
+ language sql
38
+ security definer
39
+ set search_path = cron, public
40
+ as $$
41
+ select j.jobname, d.status, d.start_time, d.end_time, d.return_message
42
+ from cron.job_run_details d
43
+ join cron.job j on j.jobid = d.jobid
44
+ where j.jobname = p_jobname
45
+ order by d.start_time desc
46
+ limit greatest(coalesce(p_limit, 10), 1);
47
+ $$;
48
+
49
+ -- ── 2. column existence probe (public schema only) ───────────────────────
50
+
51
+ create or replace function mnestra_doctor_column_exists(
52
+ p_table text,
53
+ p_column text
54
+ )
55
+ returns boolean
56
+ language sql
57
+ security definer
58
+ set search_path = public
59
+ as $$
60
+ select exists (
61
+ select 1
62
+ from information_schema.columns
63
+ where table_schema = 'public'
64
+ and table_name = p_table
65
+ and column_name = p_column
66
+ );
67
+ $$;
68
+
69
+ -- ── 3. RPC / function existence probe ────────────────────────────────────
70
+
71
+ create or replace function mnestra_doctor_rpc_exists(p_name text)
72
+ returns boolean
73
+ language sql
74
+ security definer
75
+ set search_path = public
76
+ as $$
77
+ select exists (
78
+ select 1
79
+ from pg_proc p
80
+ join pg_namespace n on n.oid = p.pronamespace
81
+ where p.proname = p_name
82
+ and n.nspname = 'public'
83
+ );
84
+ $$;
85
+
86
+ -- ── 4. cron.job existence probe (does the named job exist at all) ───────
87
+
88
+ create or replace function mnestra_doctor_cron_job_exists(p_jobname text)
89
+ returns boolean
90
+ language sql
91
+ security definer
92
+ set search_path = cron, public
93
+ as $$
94
+ select exists (select 1 from cron.job where jobname = p_jobname);
95
+ $$;
96
+
97
+ -- ── 5. vault.secrets existence probe (no value disclosure) ──────────────
98
+ --
99
+ -- Existence-only — never returns the secret value. The doctor only needs
100
+ -- to know whether the named vault entry was created during stack install.
101
+
102
+ create or replace function mnestra_doctor_vault_secret_exists(p_name text)
103
+ returns boolean
104
+ language sql
105
+ security definer
106
+ set search_path = vault, public
107
+ as $$
108
+ select exists (select 1 from vault.secrets where name = p_name);
109
+ $$;
110
+
111
+ -- ── 6. Grants ────────────────────────────────────────────────────────────
112
+
113
+ grant execute on function mnestra_doctor_cron_runs(text, int) to service_role;
114
+ grant execute on function mnestra_doctor_column_exists(text, text) to service_role;
115
+ grant execute on function mnestra_doctor_rpc_exists(text) to service_role;
116
+ grant execute on function mnestra_doctor_cron_job_exists(text) to service_role;
117
+ grant execute on function mnestra_doctor_vault_secret_exists(text) to service_role;
@@ -52,6 +52,23 @@ function extensionsDashboardUrl(secrets) {
52
52
  return `https://supabase.com/dashboard/project/${parsed.projectRef}/database/extensions`;
53
53
  }
54
54
 
55
+ // Sprint 51.5 T3: Build a SQL-Editor deeplink that pre-fills a
56
+ // vault.create_secret() call. Used when the audit's vault probe finds the
57
+ // secret missing and the wizard's auto-apply step (init-rumen.js
58
+ // `ensureVaultSecrets`) was unable to create it via pgRunner. The Vault
59
+ // dashboard panel was quietly removed/relocated in current Supabase UIs
60
+ // (Brad 2026-05-03 takeaway #2; INSTALLER-PITFALLS.md Class B), so SQL
61
+ // Editor is the working manual surface.
62
+ function vaultSqlEditorUrl(secrets, secretName, secretValue) {
63
+ if (!secrets || !secrets.SUPABASE_URL) return null;
64
+ const parsed = supabaseUrlHelper.parseProjectUrl(secrets.SUPABASE_URL);
65
+ if (!parsed.ok) return null;
66
+ const value = String(secretValue == null ? '' : secretValue).replace(/'/g, "''");
67
+ const name = String(secretName == null ? '' : secretName).replace(/'/g, "''");
68
+ const sql = `select vault.create_secret('${value}', '${name}');`;
69
+ return `https://supabase.com/dashboard/project/${parsed.projectRef}/sql/new?content=${encodeURIComponent(sql)}`;
70
+ }
71
+
55
72
  // Render a single gap into 2-3 lines of CLI output (one indented hint per
56
73
  // non-empty `hint` line). Format aligned with the rest of the wizard's
57
74
  // step lines.
@@ -206,14 +223,28 @@ async function auditRumenPreconditions({ secrets, env, _pgClient } = {}) {
206
223
  'If permission is denied, the Vault is not accessible to this connection — double-check secrets.env.'
207
224
  });
208
225
  } else if (!vault.ok) {
226
+ // Sprint 51.5 T3: the Supabase Vault dashboard panel was quietly removed
227
+ // / relocated in current Supabase UIs (Brad 2026-05-03 takeaway #2;
228
+ // INSTALLER-PITFALLS.md Class B). The wizard's `ensureVaultSecrets`
229
+ // step (init-rumen.js) auto-creates this key via pgRunner when the
230
+ // user's connection has vault.create_secret privileges; this hint only
231
+ // fires when auto-apply also failed, in which case the SQL Editor is
232
+ // the working manual surface. Build a project-specific deeplink when
233
+ // we can derive the project ref so the user gets one click instead of
234
+ // a "find the SQL Editor yourself" instruction.
235
+ const sqlEditorUrl = vaultSqlEditorUrl(secrets, 'rumen_service_role_key',
236
+ '<paste your service_role JWT from Project Settings → API>');
237
+ const sqlEditorLine = sqlEditorUrl
238
+ ? ` Open: ${sqlEditorUrl}\n Replace the placeholder with your service_role key from Project Settings → API, then click Run.`
239
+ : ' Open the Supabase SQL Editor and run:\n' +
240
+ " select vault.create_secret('<service_role JWT>', 'rumen_service_role_key');";
209
241
  gaps.push({
210
242
  key: 'rumen_service_role_key',
211
243
  message: 'Vault secret "rumen_service_role_key" is missing',
212
244
  hint:
213
- 'Create it in the Supabase dashboard:\n' +
214
- ' Project Settings Vault New secret\n' +
215
- ' Name: rumen_service_role_key (exact, case-sensitive)\n' +
216
- ' Value: your service_role key from Project Settings → API\n' +
245
+ "The wizard tries to auto-create this via vault.create_secret() and only surfaces this hint when auto-apply also failed.\n" +
246
+ 'Create it manually via the SQL Editor (the Vault dashboard panel was removed in current Supabase UIs):\n' +
247
+ sqlEditorLine + '\n' +
217
248
  '(The pg_cron schedule calls the Edge Function with this key as the bearer token.)'
218
249
  });
219
250
  }
@@ -384,6 +415,7 @@ module.exports = {
384
415
  printAuditReport,
385
416
  printVerifyReport,
386
417
  extensionsDashboardUrl,
418
+ vaultSqlEditorUrl,
387
419
  // Test surface
388
420
  _probeSupabaseAuth: probeSupabaseAuth,
389
421
  _safeQuery: safeQuery
@@ -343,9 +343,13 @@ export async function runGraphInference(sql: Sql): Promise<InferenceSummary> {
343
343
  }
344
344
 
345
345
  serve(async (_req: Request) => {
346
- const url = Deno.env.get('DATABASE_URL');
346
+ // Supabase Edge Runtime auto-injects SUPABASE_DB_URL as a built-in env var.
347
+ // Falling back to it removes one whole category of "where do I get the DB
348
+ // connection string" from the install wizard. Brad surfaced this 2026-05-03
349
+ // after hand-patching all four of his deployed copies.
350
+ const url = Deno.env.get('DATABASE_URL') ?? Deno.env.get('SUPABASE_DB_URL');
347
351
  if (!url) {
348
- console.error('[graph-inference] DATABASE_URL not set in Edge Function secrets');
352
+ console.error('[graph-inference] DATABASE_URL / SUPABASE_DB_URL not set in Edge Function secrets');
349
353
  return new Response(
350
354
  JSON.stringify({ ok: false, error: 'DATABASE_URL not set' }),
351
355
  { status: 500, headers: { 'Content-Type': 'application/json' } },
@@ -31,9 +31,13 @@ import { runRumenJob, createPoolFromUrl } from 'npm:@jhizzard/rumen@__RUMEN_VERS
31
31
  declare const Deno: { env: { get: (k: string) => string | undefined } };
32
32
 
33
33
  serve(async (_req: Request) => {
34
- const url = Deno.env.get('DATABASE_URL');
34
+ // Supabase Edge Runtime auto-injects SUPABASE_DB_URL as a built-in env var.
35
+ // Falling back to it removes one whole category of "where do I get the DB
36
+ // connection string" from the install wizard. Brad surfaced this 2026-05-03
37
+ // after hand-patching all four of his deployed copies.
38
+ const url = Deno.env.get('DATABASE_URL') ?? Deno.env.get('SUPABASE_DB_URL');
35
39
  if (!url) {
36
- console.error('[rumen] DATABASE_URL not set in Edge Function secrets');
40
+ console.error('[rumen] DATABASE_URL / SUPABASE_DB_URL not set in Edge Function secrets');
37
41
  return new Response(
38
42
  JSON.stringify({
39
43
  ok: false,