@everystack/server 0.4.8 → 0.4.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/server",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Server runtime primitives for Lambda — event adapters, routing, SSR, image processing",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -43,6 +43,10 @@
43
43
  "types": "./src/migrate.ts",
44
44
  "default": "./src/migrate.ts"
45
45
  },
46
+ "./db-doctor": {
47
+ "types": "./src/db-doctor.ts",
48
+ "default": "./src/db-doctor.ts"
49
+ },
46
50
  "./worker": {
47
51
  "types": "./src/worker.ts",
48
52
  "default": "./src/worker.ts"
@@ -155,7 +159,7 @@
155
159
  "sst": "4.13.1",
156
160
  "ts-jest": "29.4.9",
157
161
  "typescript": "5.9.3",
158
- "@everystack/cli": "0.4.22",
162
+ "@everystack/cli": "0.4.23",
159
163
  "@everystack/auth": "0.4.0",
160
164
  "@everystack/pg-tools": "0.4.0"
161
165
  },
package/src/db-doctor.ts CHANGED
@@ -593,6 +593,88 @@ export function buildDoctorReport(probe: DoctorProbe, options: DoctorReportOptio
593
593
  };
594
594
  }
595
595
 
596
+ /** A query runner for ONE connection — returns plain result rows. Venue-agnostic: the
597
+ * dbPlugin wraps drizzle (`db.execute(sql.raw(q))`), the CLI direct venue wraps a
598
+ * postgres.js client. Its errors propagate so the ambient probe can catch a fail-closed. */
599
+ export type DoctorRunner = (query: string) => Promise<any[]>;
600
+
601
+ /** The two connections db:doctor compares: the api credential under scrutiny and the
602
+ * operator credential that carries the catalog introspection. */
603
+ export interface DoctorProbeRunners {
604
+ /** The api (least-privilege / DATABASE_URL) connection. */
605
+ api: DoctorRunner;
606
+ /** The operator (ADMIN_DATABASE_URL) connection — sees the whole catalog. */
607
+ ops: DoctorRunner;
608
+ }
609
+
610
+ export interface RunDoctorProbeOptions extends DoctorReportOptions {
611
+ /** Names the api credential source in the connect-failure message (e.g. 'DATABASE_URL'). */
612
+ apiSourceLabel?: string;
613
+ /** The backups lens (pg_dump layer) probe — Lambda-only. Omitted on the direct venue,
614
+ * where there is no layer concept, so the lens is simply not shown. */
615
+ backup?: () => Promise<DoctorProbe['backup']>;
616
+ }
617
+
618
+ const quoteDoctorIdent = (s: string): string => '"' + String(s).replace(/"/g, '""') + '"';
619
+
620
+ /**
621
+ * Gather the full doctor probe over two connection runners and classify it. This is the
622
+ * single orchestration BOTH venues share:
623
+ *
624
+ * - the dbPlugin `db:doctor` action — drizzle runners, inside the VPC, with the pg_dump
625
+ * layer backups lens and the app's dbPlugin `forceRlsCarveouts` / `functionSchemas`;
626
+ * - the CLI `--database-url` / `--direct` path — postgres.js runners from the operator's
627
+ * shell, no layer lens (there is none locally).
628
+ *
629
+ * The api connection is probed for its identity (role, superuser, BYPASSRLS) and its
630
+ * ambient read; the operator connection carries every catalog query. A failure to even
631
+ * identify the api connection throws (the venue reports it) — every other probe error
632
+ * surfaces as a red check via buildDoctorReport.
633
+ */
634
+ export async function runDoctorProbe(
635
+ runners: DoctorProbeRunners,
636
+ options: RunDoctorProbeOptions = {},
637
+ ): Promise<DoctorReport> {
638
+ const { api: apiRun, ops: opsRun } = runners;
639
+ const sourceLabel = options.apiSourceLabel ?? 'DATABASE_URL';
640
+
641
+ let api: ConnInfo;
642
+ try {
643
+ api = rowToConnInfo((await apiRun(CONN_SQL))[0]);
644
+ } catch (err: any) {
645
+ throw new Error(`api connection probe failed (${sourceLabel}): ${err?.message || String(err)}`);
646
+ }
647
+
648
+ const admin = rowToConnInfo((await opsRun(CONN_SQL))[0]);
649
+ const forceRls = (await opsRun(FORCE_RLS_SQL)).map(rowToForceRls);
650
+ const appGrantedRls = (await opsRun(appGrantsRlsSql())).map(rowToAppGranted);
651
+ const secdefFunctions = (await opsRun(SECDEF_FUNCTIONS_SQL)).map(rowToSecdefFn);
652
+ const publicExecutableFunctions = (await opsRun(PUBLIC_EXECUTABLE_FUNCTIONS_SQL)).map(rowToPublicExecutableFn);
653
+ const darkRelations = (await opsRun(DARK_RELATIONS_SQL)).map(rowToDarkRelation);
654
+
655
+ // Ambient-access probe: can the api connection read an RLS table WITHOUT SET ROLE? A
656
+ // least-privilege role must fail closed here; a role that returns rows carries ambient
657
+ // privilege (INHERIT + membership in a USING(true) role like the master).
658
+ let apiAmbientRead: { table: string; accessible: boolean } | undefined;
659
+ if (forceRls.length > 0) {
660
+ const t = forceRls[0];
661
+ const ref = `${t.schema}.${t.table}`;
662
+ try {
663
+ await apiRun(`SELECT 1 FROM ${quoteDoctorIdent(t.schema)}.${quoteDoctorIdent(t.table)} LIMIT 1`);
664
+ apiAmbientRead = { table: ref, accessible: true };
665
+ } catch {
666
+ apiAmbientRead = { table: ref, accessible: false };
667
+ }
668
+ }
669
+
670
+ const backup = options.backup ? await options.backup() : undefined;
671
+
672
+ return buildDoctorReport(
673
+ { api, admin, forceRls, appGrantedRls, apiAmbientRead, secdefFunctions, publicExecutableFunctions, darkRelations, backup },
674
+ { forceRlsCarveouts: options.forceRlsCarveouts, functionSchemas: options.functionSchemas },
675
+ );
676
+ }
677
+
596
678
  /** Map a PUBLIC_EXECUTABLE_FUNCTIONS_SQL row to PublicExecutableFn. */
597
679
  export function rowToPublicExecutableFn(row: any): PublicExecutableFn {
598
680
  return {
package/src/plugin.ts CHANGED
@@ -694,82 +694,63 @@ export function dbPlugin(options: DbPluginOptions): Plugin {
694
694
  // (api still on the master), BYPASSRLS, and un-FORCEd RLS. See db-doctor.ts.
695
695
  actions['db:doctor'] = async (_payload, ctx) => {
696
696
  const { sql } = await import('drizzle-orm');
697
- const {
698
- buildDoctorReport, rowToConnInfo, rowToForceRls, rowToAppGranted, rowToSecdefFn,
699
- rowToPublicExecutableFn, rowToDarkRelation,
700
- CONN_SQL, FORCE_RLS_SQL, appGrantsRlsSql, SECDEF_FUNCTIONS_SQL,
701
- PUBLIC_EXECUTABLE_FUNCTIONS_SQL, DARK_RELATIONS_SQL,
702
- } = await import('./db-doctor');
697
+ const { runDoctorProbe } = await import('./db-doctor');
703
698
  const rowsOf = (result: any): any[] => (Array.isArray(result) ? result : result?.rows || []);
704
699
 
705
- const quoteIdent = (s: string) => '"' + String(s).replace(/"/g, '""') + '"';
706
-
700
+ // Two drizzle-backed runners for the shared orchestrator: the api credential under
701
+ // scrutiny (createDb / DATABASE_URL) and the operator connection that carries the
702
+ // catalog introspection (opsDb / ADMIN_DATABASE_URL).
707
703
  let apiDb: any;
708
- let api;
709
704
  try {
710
705
  const { createDb } = await import('./db');
711
706
  apiDb = createDb(ctx.schema).db;
712
- api = rowToConnInfo(rowsOf(await apiDb.execute(sql.raw(CONN_SQL)))[0]);
713
707
  } catch (err: any) {
714
708
  return { error: `api connection probe failed (DATABASE_URL): ${err?.message || String(err)}` };
715
709
  }
710
+ const apiRun = async (q: string): Promise<any[]> => rowsOf(await apiDb.execute(sql.raw(q)));
711
+ const opsRun = async (q: string): Promise<any[]> => rowsOf(await opsDb.execute(sql.raw(q)));
716
712
 
717
- const admin = rowToConnInfo(rowsOf(await opsDb.execute(sql.raw(CONN_SQL)))[0]);
718
- const forceRls = rowsOf(await opsDb.execute(sql.raw(FORCE_RLS_SQL))).map(rowToForceRls);
719
- // The "we require RLS" set — every app-reachable table + its RLS posture, so a table
720
- // granted to a request role with RLS off (the PostgREST fail-open shape) is caught.
721
- const appGrantedRls = rowsOf(await opsDb.execute(sql.raw(appGrantsRlsSql()))).map(rowToAppGranted);
722
- // SECDEF functions (schema, name, search_path pinned?) — the `functions` teeth read these
723
- // to assert every SECDEF function in a functions-written schema pins its search_path.
724
- const secdefFunctions = rowsOf(await opsDb.execute(sql.raw(SECDEF_FUNCTIONS_SQL))).map(rowToSecdefFn);
725
- // B4 — the derived layer's runtime echoes: PUBLIC-executable functions (NULL proacl
726
- // included via acldefault) and superuser-only views/matviews.
727
- const publicExecutableFunctions = rowsOf(await opsDb.execute(sql.raw(PUBLIC_EXECUTABLE_FUNCTIONS_SQL))).map(rowToPublicExecutableFn);
728
- const darkRelations = rowsOf(await opsDb.execute(sql.raw(DARK_RELATIONS_SQL))).map(rowToDarkRelation);
729
-
730
- // Ambient-access probe: can the api connection read an RLS table WITHOUT SET ROLE?
731
- let apiAmbientRead: { table: string; accessible: boolean } | undefined;
732
- if (forceRls.length > 0) {
733
- const t = forceRls[0];
734
- const ref = `${t.schema}.${t.table}`;
713
+ // Backups lens — is the pg_dump layer attached AND version-compatible? Lambda-only:
714
+ // a missing layer leaves pgDump null (the lens warns), never throws. Absent on the
715
+ // CLI direct venue, where there is no layer.
716
+ const backup = async (): Promise<import('./db-doctor').DoctorProbe['backup']> => {
735
717
  try {
736
- await apiDb.execute(sql.raw(`SELECT 1 FROM ${quoteIdent(t.schema)}.${quoteIdent(t.table)} LIMIT 1`));
737
- apiAmbientRead = { table: ref, accessible: true };
718
+ const { pgClientVersion, parsePgDumpMajor, serverMajorFromNum, isDumpCompatible } = await import('./backup');
719
+ let pgDump: string | null = null;
720
+ let pgDumpMajor: number | null = null;
721
+ try {
722
+ pgDump = await pgClientVersion('pg_dump');
723
+ pgDumpMajor = parsePgDumpMajor(pgDump);
724
+ } catch {
725
+ // layer absent — pgDump stays null, the lens reports "not attached"
726
+ }
727
+ const svnRow = rowsOf(await opsDb.execute(sql.raw('SHOW server_version_num')))[0];
728
+ const svn = Number(svnRow?.server_version_num);
729
+ const serverMajor = Number.isFinite(svn) ? serverMajorFromNum(svn) : null;
730
+ return {
731
+ pgDump,
732
+ pgDumpMajor,
733
+ serverMajor,
734
+ compatible: pgDumpMajor != null && Number.isFinite(svn) ? isDumpCompatible(pgDumpMajor, svn) : false,
735
+ };
738
736
  } catch {
739
- apiAmbientRead = { table: ref, accessible: false };
737
+ return undefined; // leave backup out → the lens is skipped, like the other optional probes
740
738
  }
741
- }
739
+ };
742
740
 
743
- // Backups lens — is the pg_dump layer attached AND version-compatible? Reuse the
744
- // db:backup:probe internals; a missing layer leaves pgDump null (the lens warns), never throws.
745
- let backup: import('./db-doctor').DoctorProbe['backup'];
746
741
  try {
747
- const { pgClientVersion, parsePgDumpMajor, serverMajorFromNum, isDumpCompatible } = await import('./backup');
748
- let pgDump: string | null = null;
749
- let pgDumpMajor: number | null = null;
750
- try {
751
- pgDump = await pgClientVersion('pg_dump');
752
- pgDumpMajor = parsePgDumpMajor(pgDump);
753
- } catch {
754
- // layer absent — pgDump stays null, the lens reports "not attached"
755
- }
756
- const svnRow = rowsOf(await opsDb.execute(sql.raw('SHOW server_version_num')))[0];
757
- const svn = Number(svnRow?.server_version_num);
758
- const serverMajor = Number.isFinite(svn) ? serverMajorFromNum(svn) : null;
759
- backup = {
760
- pgDump,
761
- pgDumpMajor,
762
- serverMajor,
763
- compatible: pgDumpMajor != null && Number.isFinite(svn) ? isDumpCompatible(pgDumpMajor, svn) : false,
764
- };
765
- } catch {
766
- // leave backup undefined → the lens is skipped, exactly like the other optional probes
742
+ return await runDoctorProbe(
743
+ { api: apiRun, ops: opsRun },
744
+ {
745
+ apiSourceLabel: 'DATABASE_URL',
746
+ forceRlsCarveouts: options.forceRlsCarveouts,
747
+ functionSchemas: options.functionSchemas,
748
+ backup,
749
+ },
750
+ );
751
+ } catch (err: any) {
752
+ return { error: err?.message || String(err) };
767
753
  }
768
-
769
- return buildDoctorReport(
770
- { api, admin, forceRls, appGrantedRls, apiAmbientRead, secdefFunctions, publicExecutableFunctions, darkRelations, backup },
771
- { forceRlsCarveouts: options.forceRlsCarveouts, functionSchemas: options.functionSchemas },
772
- );
773
754
  };
774
755
 
775
756
  // --- db:provision — BOTH sides of the credential split, in the safe order ---
@@ -1032,6 +1013,27 @@ export function dbPlugin(options: DbPluginOptions): Plugin {
1032
1013
  }
1033
1014
  };
1034
1015
 
1016
+ // --- db:refresh — the credential-free DATA lane's refresh half (the reconcile-lane twin) ---
1017
+ // db:reconcile deploys the derived DEFINITION but no-ops on an unchanged def, so a pure
1018
+ // DATA-changed matview refresh had no credential-free path. This runs the refresh HERE on
1019
+ // the operator connection: the CLI ships the declared matview identities in dependency order,
1020
+ // this REFRESHes each. A least-privileged operator never holds a URL for the routine refresh.
1021
+ actions['db:refresh'] = async (payload, _ctx) => {
1022
+ const p = (payload ?? {}) as { matviews?: string[] };
1023
+ try {
1024
+ const { executeRefresh } = await import('@everystack/cli/refresh');
1025
+ const { sql } = await import('drizzle-orm');
1026
+ const runner = async (query: string): Promise<any[]> => {
1027
+ const result: any = await opsDb.execute(sql.raw(query));
1028
+ return Array.isArray(result) ? result : result?.rows ?? [];
1029
+ };
1030
+ const run = await executeRefresh(runner, { matviews: p.matviews ?? [] });
1031
+ return { refreshed: run.refreshed, statements: run.statements, failed: run.failed ?? null };
1032
+ } catch (err: any) {
1033
+ return { error: err?.message || String(err) };
1034
+ }
1035
+ };
1036
+
1035
1037
  // --- pipeline:run — the credential-free ingest reproduction (Phase 3, Brick 5) ---
1036
1038
  // The orchestrator runs HERE, in the ops Lambda, over the operator connection: a
1037
1039
  // deployed stage's small curated overlays reproduce via IAM, never a held URL. The