Skip to content

feat(safegres): stack + posture presets and four more exposure adapters - #1601

Merged
pyramation merged 1 commit into
mainfrom
feat/safegres-presets
Aug 2, 2026
Merged

feat(safegres): stack + posture presets and four more exposure adapters#1601
pyramation merged 1 commit into
mainfrom
feat/safegres-presets

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Summary

Packages the plane/adapter machinery from #1599 into presets you can name, split along the axis that actually varies: a stack preset knows how your framework declares exposure and what its role names mean, a posture preset says how harshly to read what it finds. They compose, because they are just partial configs:

{ "extends": ["safegres:supabase", "safegres:multi-tenant"] }
Stack Resolves exposure from Untrusted
constructive routing_public.apisapi_schemas anonymous
postgrest pgrst.db_schemas in pg_db_role_setting anon
supabase same (Supabase is PostgREST) anon, authenticated
hasura tracked tables in hdb_catalog anonymous, public
graphile the graphile-starter layout visitor
Posture Behavior
multi-tenant A1/A2, L1–L3, L5 escalated — in a shared-table database an RLS gap is a cross-tenant read; one critical floors the grade at D
oltp X2–X4/X9 escalated — the policy shapes that turn an index scan into a per-row function call; failOn: perfGrade C

Nothing is switched off. Per your note: a rule that doesn't apply to a stack is demoted to info — zero weight, so the score is byte-identical to disabling it — instead of deleted, so it stays in the report and stays re-tunable. That also flips the existing constructive preset's A3: 'off''info' (score unchanged; the finding becomes visible). Supabase's and Hasura's platform-managed schemas are scoped the same way, by demotion rather than exclusion. minimal is the one deliberate exception — being a smoke check is its whole job. A test enforces the invariant across every preset, and another asserts every string adapter a preset names exists in BUILTIN_ADAPTERS.

Adapters read catalog state, not schema names. PostgREST publishes its own surface as a GUC, so postgrestAdapter reads pg_db_role_setting rather than guessing — which also means it works from an audit connection that isn't the API's:

SELECT unnest(setconfig) FROM pg_db_role_setting   -- 'pgrst.db_schemas=app,api'

It emits the API plane plus a direct:<authenticator> role plane, because a role that can SET ROLE is a separate question from what the API serves. Hasura reads tracked tables out of hdb_catalog.hdb_metadata (v2 JSON, with the v1 hdb_table fallback) — untracked tables are not served, so they are not the surface.

graphileAdapter is the deliberate exception and is documented as one: PostGraphile's schema list is a process argument that leaves no trace in the database, so the adapter resolves the graphile-starter layout (app_public + app_hidden served, app_private its own internal plane). That is naming-as-intent, which safegres otherwise refuses; the difference is consent — naming the adapter is the declaration. When the convention doesn't hold, exposure.schemas still wins.

Also: dropped the hand-written MIT © Constructive line from the README. Every package here gets the shared FOOTER.md appended at build time (credits, license, disclaimer), so safegres was the only one ending in a duplicate, half-size footer.

Verification

pnpm lint, pnpm build, pnpm test in packages/safegres — 23 suites / 243 tests, 10 of them new. The adapter tests run against a fixture that reproduces each stack's one catalog signal (__tests__/fixtures/stacks.sql), and include a negative: the constructive adapter must stay quiet in a database that isn't Constructive. Confirmed dist/README.md renders with the shared footer and no duplicate.

Link to Devin session: https://app.devin.ai/sessions/b7874ecee0c7471ea271e6e7193869dc
Requested by: @pyramation

…aphile adapters

Presets configure, they never delete: a rule that does not apply to a stack is
demoted to info (zero weight, so the score is unchanged) rather than switched
off, so it stays in the report and stays re-tunable. constructive's A3 goes
off -> info under the same rule.

- stack presets (constructive, postgrest, supabase, hasura, graphile) each
  resolve exposure from a real catalog signal and supply that stack's untrusted
  role vocabulary to R1/R2/R3/L5
- posture presets (multi-tenant, oltp) carry no exposure of their own and
  compose: extends: ["safegres:supabase", "safegres:multi-tenant"]
- postgrestAdapter reads pgrst.db_schemas / pgrst.db_anon_role from
  pg_db_role_setting; hasuraAdapter reads tracked tables from hdb_catalog;
  graphileAdapter resolves the graphile-starter layout
- drop the hand-written "MIT (c) Constructive" line: the shared FOOTER.md is
  appended at build and already carries credits, license and disclaimer
@pyramation pyramation self-assigned this Aug 2, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@pyramation
pyramation merged commit 015a50e into main Aug 2, 2026
16 checks passed
@pyramation
pyramation deleted the feat/safegres-presets branch August 2, 2026 02:46
devin-ai-integration Bot pushed a commit that referenced this pull request Aug 2, 2026
graphileAdapter answers which schemas a graphile-starter DB serves;
postgraphileAdapter answers what the served schemas expose. The former
delegates reach to the latter rather than duplicating the behavior read.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant