Skip to content

fix(express-context): key tenant-DB module discovery by database_id, ship the auth-surface and identity-provider loaders - #1644

Merged
pyramation merged 1 commit into
mainfrom
feat/express-context-auth-loaders
Aug 3, 2026
Merged

fix(express-context): key tenant-DB module discovery by database_id, ship the auth-surface and identity-provider loaders#1644
pyramation merged 1 commit into
mainfrom
feat/express-context-auth-loaders

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Summary

constructive-planning#1403 reported one unfiltered discovery query. Auditing the rest found a second, and the reason both survived review is that neither fails:

   FROM metaschema_modules_public.sessions_module sm
   JOIN metaschema_public.schema s ON s.id = sm.schema_id
+  WHERE sm.database_id = $1
   LIMIT 1

In a schema-per-tenant serving database that LIMIT 1 returns whichever tenant the planner reaches first, step 2 then reads that tenant's auth_settings — cookie domain, SameSite, captcha policy — and createModuleLoader caches the result under the requesting tenant's id for five minutes. Wrong config, no error, sticky. agentChat had the identical shape (agent_chat_module, no WHERE).

A context with no databaseId now throws instead of silently running unkeyed:

requireDatabaseId(databaseId, 'authSettings'); // asserts databaseId is string

The audit is now a test, not a promise__tests__/loaders/tenant-keying.test.ts extracts every FROM metaschema_modules_public.<module> … span out of the loader sources and asserts each is filtered by database_id = $1, plus that any file containing $1 passes a values array (pg accepts query(text) with no params happily, so that one is a runtime error rather than a type error). It also asserts it matched ≥6 queries, so a regex that quietly stops matching can't make the whole suite vacuously green. That test is what found agentChat.

Two loaders shipped (constructive-planning#1414), lifted from the SSO proof where they were hand-written:

  • authSurfaceLoader — the tenant's auth schemas + physical identifier relation names, one round trip. Registered in the default registry. The companion joins are on database_id too, since joining connected_accounts_module by schema id alone can pair one tenant's providers with another's accounts and still return a plausible row.
  • identityProvidersLoader — provider config keyed by slug, with the tenant's own <secrets_table>_get() inlined so a rotated secret is picked up with no coordination. Not in the default registry: three round trips and it decrypts secrets, so it's opt-in for services actually serving an auth flow.

The provider shape is the point of shipping it. Deriving it per integration is how one of them ends up not checking nonce, so issuerUrl, jwks, acceptableClientIds, skipNonceCheck and pkceEnabled are fixed in IdentityProviderConfig, and the two policy flags default to the safe side (skipNonceCheck ?? false, pkceEnabled ?? true) rather than to whatever the row omitted.

Failure modes follow the package contract — module row absent → undefined — with two deliberate exceptions that are configuration faults rather than absence: a provider with no client_id, and a tenant that provisions identity_providers_module but no internal_secrets_module (returning secret-less providers there would present a confidential client as a public one).

Not in this PR

  • feat(pgpm-core): rebundleWorkspace initScaffold — optional pgpm init boilerplate #1414's acceptance is "both proofs delete their local copies and keep passing". Those proofs (constructive-db#2725, #2724) are unmerged branches in the other repo consuming the published @constructive-io/express-context@0.24.x, so they can't drop their copies until this ships. The loaders here are their code, moved, not a reinterpretation of it.
  • graphile/graphile-llm/src/plugins/agent-discovery-plugin.ts carries a third copy of the unkeyed agent_chat_module query (DISCOVERY_SQL, also cached per database). Same bug, different package and different cache — flagged rather than fixed here to keep this diff to the loaders.

packages/express-context: 27 tests passing, lint and build clean.

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

…ship auth loaders

authSettings and agentChat discovered their module row with an unfiltered
LIMIT 1 against the tenant database. In a schema-per-tenant serving database
that does not fail — it returns a neighbouring tenant's row, and the loader
cache then serves that answer for its whole TTL.

Adds the two loaders every auth consumer was hand-writing (auth surface,
identity providers), and a structural test asserting every
metaschema_modules_public discovery query is filtered by database_id.

constructive-planning#1403, constructive-planning#1414
@pyramation pyramation self-assigned this Aug 3, 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 c944043 into main Aug 3, 2026
16 checks passed
@pyramation
pyramation deleted the feat/express-context-auth-loaders branch August 3, 2026 23:13
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