From 589b6adde6aaac36d78c847ff24347069ca78f30 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sat, 23 May 2026 06:54:11 +0000 Subject: [PATCH] feat: OAuth middleware with module loaders for identity/secrets/auth config Replace manual SQL queries and hardcoded schema assumptions in OAuth middleware with module loaders from express-context: New loaders: - encryptedSecretsLoader: resolves encrypted_secrets schema from metaschema_modules_public.encrypted_secrets_module - userAuthLoader: resolves user_auth_module for sign_in/sign_up function names and schema (no longer assumes privateSchema) - identityProvidersLoader: resolves identity_providers_module for provider config table location OAuth middleware changes: - Uses req.constructive.useModule() for all schema lookups - Uses req.constructive.withPgClient() for properly scoped RLS transactions (replaces manual set_config calls) - Removes express-rate-limit (DB already handles rate limiting) - Uses getNodeEnv() instead of process.env.NODE_ENV - Extracts email_verified from raw provider profile data - Passes loaders registry to createContextMiddleware in server.ts Addresses review comments from PR #1141. --- graphql/server/package.json | 1 + graphql/server/src/middleware/oauth.ts | 692 ++++++++++++++++++ graphql/server/src/server.ts | 9 +- packages/express-context/src/index.ts | 6 + .../src/loaders/encrypted-secrets.ts | 53 ++ .../src/loaders/identity-providers.ts | 65 ++ packages/express-context/src/loaders/index.ts | 12 + .../express-context/src/loaders/user-auth.ts | 79 ++ packages/express-context/src/types.ts | 26 + pnpm-lock.yaml | 226 +----- 10 files changed, 963 insertions(+), 206 deletions(-) create mode 100644 graphql/server/src/middleware/oauth.ts create mode 100644 packages/express-context/src/loaders/encrypted-secrets.ts create mode 100644 packages/express-context/src/loaders/identity-providers.ts create mode 100644 packages/express-context/src/loaders/user-auth.ts diff --git a/graphql/server/package.json b/graphql/server/package.json index be18bfea0..6e89520bb 100644 --- a/graphql/server/package.json +++ b/graphql/server/package.json @@ -46,6 +46,7 @@ "@constructive-io/express-context": "workspace:^", "@constructive-io/graphql-env": "workspace:^", "@constructive-io/graphql-types": "workspace:^", + "@constructive-io/oauth": "workspace:^", "@constructive-io/s3-utils": "workspace:^", "@constructive-io/upload-names": "workspace:^", "@constructive-io/url-domains": "workspace:^", diff --git a/graphql/server/src/middleware/oauth.ts b/graphql/server/src/middleware/oauth.ts new file mode 100644 index 000000000..e71942497 --- /dev/null +++ b/graphql/server/src/middleware/oauth.ts @@ -0,0 +1,692 @@ +/** + * OAuth / SSO Middleware + * + * Express router for OAuth2/OIDC identity-based sign-in. Uses module loaders + * from @constructive-io/express-context to discover schemas and config at + * runtime rather than hardcoding assumptions about where tables live. + * + * Resolves per-database: + * - identityProviders → schema where identity_providers table lives + * - encryptedSecrets → schema for decrypting client secrets + * - userAuth → schema + function names for sign_in_identity / sign_up_identity + * - authSettings → cookie, captcha, and session config + * - rlsModule → private/public schema references + * + * All DB queries run through `req.constructive.withPgClient()` which + * applies pgSettings (role, claims, request_id) via SET LOCAL, replacing + * the manual `set_config()` calls in the original implementation. + */ + +import crypto from 'crypto'; +import { Router, Request, Response } from 'express'; +import { OAuthClient, OAuthProfile } from '@constructive-io/oauth'; +import { Logger } from '@pgpmjs/logger'; +import { getNodeEnv } from '@pgpmjs/env'; +import type { ConstructiveOptions } from '@constructive-io/graphql-types'; +import type { + AuthSettings, + ConstructiveContext, + EncryptedSecretsConfig, + IdentityProvidersConfig, + UserAuthConfig, +} from '@constructive-io/express-context'; + +import { + DEVICE_TOKEN_COOKIE_NAME, + getSessionCookieConfig, + getDeviceTokenCookieConfig, + setSessionCookie, + setDeviceTokenCookie, + parseCookieValue, +} from './cookie'; + +const log = new Logger('oauth'); + +const OAUTH_STATE_COOKIE = 'oauth_state'; +const DEFAULT_OAUTH_STATE_MAX_AGE = 10 * 60 * 1000; // 10 minutes +const DEFAULT_ERROR_REDIRECT_PATH = '/auth/error'; + +// ============================================================================= +// Signed State Utilities +// ============================================================================= + +interface StatePayload { + redirect_uri: string; + provider: string; + nonce: string; + exp: number; +} + +function getStateSecret(): string { + const secret = process.env.OAUTH_SECRET; + if (!secret) { + throw new Error('OAUTH_SECRET environment variable is required'); + } + return secret; +} + +function createSignedState( + payload: { redirect_uri: string; provider: string }, + maxAge: number, +): string { + const data: StatePayload = { + ...payload, + nonce: crypto.randomBytes(16).toString('hex'), + exp: Date.now() + maxAge, + }; + const json = JSON.stringify(data); + const sig = crypto + .createHmac('sha256', getStateSecret()) + .update(json) + .digest('base64url'); + return Buffer.from(json).toString('base64url') + '.' + sig; +} + +function verifySignedState(state: string): StatePayload | null { + try { + const [payloadB64, sig] = state.split('.'); + if (!payloadB64 || !sig) return null; + + const json = Buffer.from(payloadB64, 'base64url').toString(); + const expectedSig = crypto + .createHmac('sha256', getStateSecret()) + .update(json) + .digest('base64url'); + + if ( + !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig)) + ) { + return null; + } + + const data = JSON.parse(json) as StatePayload; + if (data.exp < Date.now()) { + return null; + } + + return data; + } catch { + return null; + } +} + +// ============================================================================= +// Module Resolution Helpers +// ============================================================================= + +interface OAuthModules { + identityProviders: IdentityProvidersConfig; + encryptedSecrets: EncryptedSecretsConfig; + userAuth: UserAuthConfig; + authSettings: AuthSettings | undefined; +} + +async function resolveOAuthModules( + ctx: ConstructiveContext, +): Promise { + const [identityProviders, encryptedSecrets, userAuth, authSettings] = + await Promise.all([ + ctx.useModule('identityProviders'), + ctx.useModule('encryptedSecrets'), + ctx.useModule('userAuth'), + ctx.useModule('authSettings'), + ]); + + if (!identityProviders || !encryptedSecrets || !userAuth) { + return null; + } + + return { identityProviders, encryptedSecrets, userAuth, authSettings }; +} + +// ============================================================================= +// Identity Provider Database Functions +// ============================================================================= + +interface IdentityProviderConfig { + slug: string; + kind: 'oauth2' | 'oidc'; + display_name: string; + enabled: boolean; + client_id: string; + client_secret: string; + authorization_url: string | null; + token_url: string | null; + userinfo_url: string | null; + scopes: string[]; + pkce_enabled: boolean; +} + +async function getEnabledProviders( + ctx: ConstructiveContext, + modules: OAuthModules, +): Promise { + const { privateSchemaName, tableName } = modules.identityProviders; + const sql = ` + SELECT slug FROM "${privateSchemaName}"."${tableName}" + WHERE enabled = true AND client_id IS NOT NULL AND client_secret_id IS NOT NULL + `; + const result = await ctx.pool.query(sql); + return result.rows.map((row: { slug: string }) => row.slug); +} + +async function getIdentityProvider( + ctx: ConstructiveContext, + modules: OAuthModules, + providerSlug: string, +): Promise { + const { privateSchemaName, tableName } = modules.identityProviders; + const { schemaName: encryptedSchema } = modules.encryptedSecrets; + + const sql = ` + SELECT + ip.slug, + ip.kind, + ip.display_name, + ip.enabled, + ip.client_id, + "${encryptedSchema}".get(ip.client_secret_id, 'oauth_client_secret') as client_secret, + ip.authorization_url, + ip.token_url, + ip.userinfo_url, + ip.scopes, + ip.pkce_enabled + FROM "${privateSchemaName}"."${tableName}" ip + WHERE ip.slug = $1 AND ip.enabled = true + `; + + const result = await ctx.pool.query(sql, [providerSlug]); + if (result.rows.length === 0) { + return null; + } + + const row = result.rows[0]; + if (!row.client_id || !row.client_secret) { + return null; + } + + return { + slug: row.slug, + kind: row.kind, + display_name: row.display_name, + enabled: row.enabled, + client_id: row.client_id, + client_secret: row.client_secret, + authorization_url: row.authorization_url, + token_url: row.token_url, + userinfo_url: row.userinfo_url, + scopes: row.scopes || [], + pkce_enabled: row.pkce_enabled ?? true, + }; +} + +function createOAuthClientForProvider( + providerConfig: IdentityProviderConfig, + baseUrl: string, +): OAuthClient { + return new OAuthClient({ + providers: { + [providerConfig.slug]: { + clientId: providerConfig.client_id, + clientSecret: providerConfig.client_secret, + }, + }, + baseUrl, + callbackPath: '/auth/{provider}/callback', + }); +} + +// ============================================================================= +// Database Functions +// ============================================================================= + +interface SignInIdentityResult { + id?: string; + user_id?: string; + access_token?: string; + access_token_expires_at?: string; + is_verified?: boolean; + totp_enabled?: boolean; + mfa_required?: boolean; + mfa_challenge_token?: string; + out_device_token?: string; +} + +async function generateCrossOriginToken( + ctx: ConstructiveContext, + modules: OAuthModules, + accessToken: string, +): Promise { + const otToken = crypto.randomBytes(32).toString('base64url'); + const { schemaName } = modules.userAuth; + + const sql = ` + UPDATE "${schemaName}".session_credentials + SET ot_token = $1 + WHERE secret_hash = digest($2::text, 'sha256') + RETURNING id + `; + + const result = await ctx.pool.query(sql, [otToken, accessToken]); + if (result.rows.length === 0) { + throw new Error('Failed to set cross-origin token'); + } + + return otToken; +} + +// ============================================================================= +// OAuth Routes +// ============================================================================= + +function getBaseUrl(req: Request): string { + const protocol = req.protocol || 'http'; + const host = req.get('host') || 'localhost:3000'; + return `${protocol}://${host}`; +} + +/** + * Extract email_verified from the raw provider response. + * OAuthProfile.raw contains the original provider data which includes + * email_verified for OIDC providers (Google, etc.). + */ +function isEmailVerified(profile: OAuthProfile): boolean { + const raw = profile.raw as Record | null; + if (!raw) return false; + if (typeof raw.email_verified === 'boolean') return raw.email_verified; + if (typeof raw.verified_email === 'boolean') return raw.verified_email; + return false; +} + +function redirectToError( + res: Response, + baseUrl: string, + errorPath: string, + error: string, + provider: string, + errorDescription?: string, +): void { + const errorUrl = new URL(errorPath, baseUrl); + errorUrl.searchParams.set('error', error); + errorUrl.searchParams.set('provider', provider); + if (errorDescription) { + errorUrl.searchParams.set('error_description', errorDescription); + } + res.redirect(errorUrl.toString()); +} + +export function createOAuthRoutes(_opts: ConstructiveOptions): Router { + const router = Router(); + const isProduction = getNodeEnv() === 'production'; + + // GET /auth/providers - List available providers from database + router.get('/providers', async (req: Request, res: Response) => { + const ctx = req.constructive; + if (!ctx) { + return res.json({ providers: [] }); + } + + try { + const modules = await resolveOAuthModules(ctx); + if (!modules) { + return res.json({ providers: [] }); + } + const providers = await getEnabledProviders(ctx, modules); + res.json({ providers }); + } catch (error) { + log.error('[oauth] Failed to fetch providers:', error); + res.json({ providers: [] }); + } + }); + + // GET /auth/error - Pass to next middleware stack for frontend to handle + router.get('/error', (_req: Request, _res: Response, next) => { + next('router'); + }); + + // GET /auth/:provider - Initiate OAuth flow + router.get('/:provider', async (req: Request, res: Response) => { + const { provider } = req.params; + const redirectUri = (req.query.redirect_uri as string) || '/'; + const ctx = req.constructive; + const baseUrl = getBaseUrl(req); + + if (!ctx) { + log.error(`[oauth] No constructive context for ${provider} initiation`); + return redirectToError( + res, + baseUrl, + DEFAULT_ERROR_REDIRECT_PATH, + 'API_NOT_CONFIGURED', + provider, + ); + } + + try { + const modules = await resolveOAuthModules(ctx); + if (!modules) { + log.error(`[oauth] Required modules not provisioned for ${provider}`); + return redirectToError( + res, + baseUrl, + DEFAULT_ERROR_REDIRECT_PATH, + 'MODULES_NOT_CONFIGURED', + provider, + ); + } + + const providerConfig = await getIdentityProvider(ctx, modules, provider); + if (!providerConfig) { + log.warn(`[oauth] Provider ${provider} not found or not configured`); + return redirectToError( + res, + baseUrl, + DEFAULT_ERROR_REDIRECT_PATH, + 'PROVIDER_NOT_CONFIGURED', + provider, + ); + } + + const stateMaxAge = DEFAULT_OAUTH_STATE_MAX_AGE; + const state = createSignedState( + { redirect_uri: redirectUri, provider }, + stateMaxAge, + ); + + res.cookie(OAUTH_STATE_COOKIE, state, { + httpOnly: true, + secure: isProduction, + maxAge: stateMaxAge, + sameSite: 'lax', + }); + + const client = createOAuthClientForProvider(providerConfig, baseUrl); + const { url } = client.getAuthorizationUrl({ provider, state }); + log.info(`[oauth] Initiating OAuth flow for provider: ${provider}`); + res.redirect(url); + } catch (error) { + log.error(`[oauth] Failed to initiate OAuth for ${provider}:`, error); + redirectToError( + res, + baseUrl, + DEFAULT_ERROR_REDIRECT_PATH, + 'OAUTH_INIT_FAILED', + provider, + ); + } + }); + + // GET /auth/:provider/callback - Handle OAuth callback + router.get( + '/:provider/callback', + async (req: Request, res: Response) => { + const { provider } = req.params; + const { + code, + state, + error: oauthError, + error_description: errorDescription, + } = req.query; + const baseUrl = getBaseUrl(req); + + const storedState = parseCookieValue(req, OAUTH_STATE_COOKIE); + res.clearCookie(OAUTH_STATE_COOKIE); + + // Handle OAuth provider errors + if (oauthError) { + log.warn(`[oauth] Provider ${provider} returned error: ${oauthError}`); + return redirectToError( + res, + baseUrl, + DEFAULT_ERROR_REDIRECT_PATH, + oauthError as string, + provider, + errorDescription as string | undefined, + ); + } + + // Verify state + if (state !== storedState) { + log.warn(`[oauth] State mismatch for ${provider}`); + return redirectToError( + res, + baseUrl, + DEFAULT_ERROR_REDIRECT_PATH, + 'INVALID_STATE', + provider, + ); + } + + const statePayload = verifySignedState(storedState as string); + if (!statePayload) { + log.warn(`[oauth] Invalid or expired state for ${provider}`); + return redirectToError( + res, + baseUrl, + DEFAULT_ERROR_REDIRECT_PATH, + 'INVALID_STATE', + provider, + ); + } + + const { redirect_uri: redirectUri } = statePayload; + const ctx = req.constructive; + + if (!ctx) { + log.error( + `[oauth] No constructive context for ${provider} callback`, + ); + return redirectToError( + res, + baseUrl, + DEFAULT_ERROR_REDIRECT_PATH, + 'API_NOT_CONFIGURED', + provider, + ); + } + + try { + const modules = await resolveOAuthModules(ctx); + if (!modules) { + log.error( + `[oauth] Required modules not provisioned for ${provider}`, + ); + return redirectToError( + res, + baseUrl, + DEFAULT_ERROR_REDIRECT_PATH, + 'MODULES_NOT_CONFIGURED', + provider, + ); + } + + const providerConfig = await getIdentityProvider( + ctx, + modules, + provider, + ); + if (!providerConfig) { + log.error(`[oauth] Provider ${provider} not found in database`); + return redirectToError( + res, + baseUrl, + DEFAULT_ERROR_REDIRECT_PATH, + 'PROVIDER_NOT_CONFIGURED', + provider, + ); + } + + const client = createOAuthClientForProvider(providerConfig, baseUrl); + const profile = await client.handleCallback({ + provider, + code: code as string, + }); + log.info(`[oauth] Got profile for ${provider}: ${profile.email}`); + + const deviceToken = + parseCookieValue(req, DEVICE_TOKEN_COOKIE_NAME) ?? null; + + // Calculate target origin for cross-origin flow + const currentOrigin = baseUrl; + let targetOrigin: string; + try { + const redirectUrl = new URL(redirectUri, currentOrigin); + targetOrigin = redirectUrl.origin; + } catch { + targetOrigin = currentOrigin; + } + + const userAgent = req.get('user-agent') || ''; + const { userAuth } = modules; + + // Use withPgClient to run sign_in/sign_up within a properly scoped + // RLS transaction. pgSettings (role, claims, request_id) are applied + // automatically via SET LOCAL, replacing the manual set_config calls. + const result = await ctx.withPgClient( + async (client) => { + // Set OAuth-specific JWT claims on this transaction + await client.query( + `SELECT set_config('jwt.claims.user_agent', $1, true), + set_config('jwt.claims.origin', $2, true)`, + [userAgent, targetOrigin], + ); + + const emailVerified = isEmailVerified(profile); + const details = { + provider: profile.provider, + sub: profile.providerId, + email: profile.email, + email_verified: emailVerified, + name: profile.name, + picture: profile.picture, + raw_userinfo: profile.raw, + }; + + // sign_in_identity lives in the userAuth schema, NOT assumed to + // be in the RLS privateSchema. + const signInSql = ` + SELECT * FROM "${userAuth.schemaName}".sign_in_identity( + $1::text, $2::text, $3::jsonb, $4::text, 'access_token'::text, $5::boolean, $6::text + ) + `; + + try { + const signInResult = await client.query(signInSql, [ + profile.provider, + profile.providerId, + JSON.stringify(details), + profile.email, + true, + deviceToken, + ]); + return signInResult.rows[0] || {}; + } catch (err: any) { + const errorMessage = err.message || ''; + + if (!errorMessage.includes('IDENTITY_ACCOUNT_NOT_FOUND')) { + throw err; + } + + log.info( + `[oauth] Account not found for ${profile.email}, attempting signup`, + ); + + if (!emailVerified) { + log.warn( + `[oauth] Rejecting unverified email for signup: ${profile.email}`, + ); + return { _error: 'EMAIL_NOT_VERIFIED' } as any; + } + + // sign_up_identity also lives in the userAuth schema + const signUpSql = ` + SELECT * FROM "${userAuth.schemaName}".sign_up_identity( + $1::text, $2::text, $3::text, $4::jsonb, 'access_token'::text, $5::boolean, $6::text + ) + `; + + const signUpResult = await client.query(signUpSql, [ + profile.provider, + profile.providerId, + profile.email, + JSON.stringify(details), + true, + deviceToken, + ]); + return signUpResult.rows[0] || {}; + } + }, + ); + + // Handle error sentinels from within the transaction + if ((result as any)._error === 'EMAIL_NOT_VERIFIED') { + return redirectToError( + res, + baseUrl, + DEFAULT_ERROR_REDIRECT_PATH, + 'EMAIL_NOT_VERIFIED', + provider, + ); + } + + // Handle MFA required + if (result.mfa_required && result.mfa_challenge_token) { + log.info(`[oauth] MFA required for ${profile.email}`); + const mfaUrl = new URL('/auth/mfa', baseUrl); + mfaUrl.searchParams.set('token', result.mfa_challenge_token); + mfaUrl.searchParams.set('redirect_uri', redirectUri); + return res.redirect(mfaUrl.toString()); + } + + if (!result.access_token) { + throw new Error('No access token returned from sign_in_identity'); + } + + const isCrossOrigin = targetOrigin !== currentOrigin; + + if (isCrossOrigin) { + const otToken = await generateCrossOriginToken( + ctx, + modules, + result.access_token, + ); + const redirectUrl = new URL(redirectUri, currentOrigin); + redirectUrl.searchParams.set('token', otToken); + log.info( + `[oauth] OAuth success for ${profile.email}, cross-origin redirect`, + ); + return res.redirect(redirectUrl.toString()); + } else { + const sessionConfig = getSessionCookieConfig( + modules.authSettings, + true, + ); + setSessionCookie(res, result.access_token, sessionConfig); + + if (result.out_device_token) { + const deviceConfig = getDeviceTokenCookieConfig( + modules.authSettings, + ); + setDeviceTokenCookie(res, result.out_device_token, deviceConfig); + } + + log.info( + `[oauth] OAuth success for ${profile.email}, same-origin redirect`, + ); + return res.redirect(redirectUri); + } + } catch (error: any) { + log.error(`[oauth] Callback failed for ${provider}:`, error); + redirectToError( + res, + baseUrl, + DEFAULT_ERROR_REDIRECT_PATH, + 'CALLBACK_FAILED', + provider, + ); + } + }, + ); + + return router; +} diff --git a/graphql/server/src/server.ts b/graphql/server/src/server.ts index e2be61a07..8607c9871 100644 --- a/graphql/server/src/server.ts +++ b/graphql/server/src/server.ts @@ -39,7 +39,8 @@ import { createCaptchaMiddleware } from './middleware/captcha'; import { parseCookieValue, SESSION_COOKIE_NAME } from './middleware/cookie'; import { createUploadAuthenticateMiddleware, uploadRoute } from './middleware/upload'; import { createLlmApiRouter } from './middleware/llm-api'; -import { createContextMiddleware, requestIdMiddleware } from '@constructive-io/express-context'; +import { createOAuthRoutes } from './middleware/oauth'; +import { createContextMiddleware, createDefaultRegistry, requestIdMiddleware } from '@constructive-io/express-context'; import { startDebugSampler } from './diagnostics/debug-sampler'; const log = new Logger('server'); @@ -167,7 +168,7 @@ class Server { app.use(api); app.post('/upload', uploadAuthenticate, ...uploadRoute); app.use(authenticate); - app.use(createContextMiddleware({ pg: effectiveOpts.pg })); + app.use(createContextMiddleware({ pg: effectiveOpts.pg, loaders: createDefaultRegistry() })); app.use(createCaptchaMiddleware()); // CSRF protection for cookie-authenticated requests @@ -199,6 +200,10 @@ class Server { app.use(csrfSetToken); // Set CSRF token cookie on all requests app.use('/graphql', csrfProtect); // Enforce CSRF on GraphQL mutations + // OAuth / SSO routes — mounted before graphile so OAuth callbacks + // are handled without going through PostGraphile + app.use('/auth', createOAuthRoutes(effectiveOpts)); + // LLM Agent REST API — mounted before graphile so SSE streaming // routes are handled without going through PostGraphile app.use(createLlmApiRouter()); diff --git a/packages/express-context/src/index.ts b/packages/express-context/src/index.ts index f70907c6a..11f28ce27 100644 --- a/packages/express-context/src/index.ts +++ b/packages/express-context/src/index.ts @@ -54,6 +54,9 @@ export type { PublicKeyChallengeData, PubkeyChallengeSettings, RlsModule, + EncryptedSecretsConfig, + IdentityProvidersConfig, + UserAuthConfig, WebauthnSettings, WithPgClient, } from './types'; @@ -92,6 +95,9 @@ export { pubkeyLoader, rlsLoader, webauthnLoader, + encryptedSecretsLoader, + userAuthLoader, + identityProvidersLoader, } from './loaders'; // Side-effect: Express type augmentation diff --git a/packages/express-context/src/loaders/encrypted-secrets.ts b/packages/express-context/src/loaders/encrypted-secrets.ts new file mode 100644 index 000000000..5f79f77cb --- /dev/null +++ b/packages/express-context/src/loaders/encrypted-secrets.ts @@ -0,0 +1,53 @@ +/** + * Encrypted Secrets Module Loader + * + * Resolves the schema name for the encrypted_secrets table from + * metaschema_modules_public.encrypted_secrets_module. Used by OAuth + * and other modules that need to decrypt secrets stored in the tenant DB. + */ + +import type { LoaderContext, ModuleLoader } from './types'; +import { createModuleLoader } from './create-loader'; + +// ─── Types ────────────────────────────────────────────────────────────────── + +export interface EncryptedSecretsConfig { + schemaName: string; + tableName: string; +} + +// ─── SQL ──────────────────────────────────────────────────────────────────── + +const ENCRYPTED_SECRETS_MODULE_SQL = ` + SELECT + s.schema_name, + esm.table_name + FROM metaschema_modules_public.encrypted_secrets_module esm + JOIN metaschema_public.schema s ON s.id = esm.schema_id + WHERE esm.database_id = $1 + LIMIT 1 +`; + +// ─── Loader ───────────────────────────────────────────────────────────────── + +export const encryptedSecretsLoader: ModuleLoader = + createModuleLoader({ + name: 'encryptedSecrets', + ttlMs: 5 * 60_000, + async resolve(ctx: LoaderContext) { + const { tenantPool, databaseId } = ctx; + + const result = await tenantPool.query<{ + schema_name: string; + table_name: string; + }>(ENCRYPTED_SECRETS_MODULE_SQL, [databaseId]); + + const row = result.rows[0]; + if (!row) return undefined; + + return { + schemaName: row.schema_name, + tableName: row.table_name, + }; + }, + }); diff --git a/packages/express-context/src/loaders/identity-providers.ts b/packages/express-context/src/loaders/identity-providers.ts new file mode 100644 index 000000000..96d655aea --- /dev/null +++ b/packages/express-context/src/loaders/identity-providers.ts @@ -0,0 +1,65 @@ +/** + * Identity Providers Module Loader + * + * Resolves the identity_providers_module config from metaschema_modules_public. + * Provides schema names where the identity_providers table lives, used by + * OAuth/SSO middleware to look up provider definitions (client_id, encrypted + * client_secret, scopes, etc.). + */ + +import type { LoaderContext, ModuleLoader } from './types'; +import { createModuleLoader } from './create-loader'; + +// ─── Types ────────────────────────────────────────────────────────────────── + +export interface IdentityProvidersConfig { + schemaName: string; + privateSchemaName: string; + tableName: string; +} + +// ─── SQL ──────────────────────────────────────────────────────────────────── + +const IDENTITY_PROVIDERS_MODULE_SQL = ` + SELECT + s.schema_name, + ps.schema_name AS private_schema_name, + ipm.table_name + FROM metaschema_modules_public.identity_providers_module ipm + JOIN metaschema_public.schema s ON s.id = ipm.schema_id + JOIN metaschema_public.schema ps ON ps.id = ipm.private_schema_id + WHERE ipm.database_id = $1 + LIMIT 1 +`; + +// ─── Row Types ────────────────────────────────────────────────────────────── + +interface IdentityProvidersModuleRow { + schema_name: string; + private_schema_name: string; + table_name: string; +} + +// ─── Loader ───────────────────────────────────────────────────────────────── + +export const identityProvidersLoader: ModuleLoader = + createModuleLoader({ + name: 'identityProviders', + ttlMs: 5 * 60_000, + async resolve(ctx: LoaderContext) { + const { tenantPool, databaseId } = ctx; + + const result = await tenantPool.query( + IDENTITY_PROVIDERS_MODULE_SQL, + [databaseId], + ); + const row = result.rows[0]; + if (!row) return undefined; + + return { + schemaName: row.schema_name, + privateSchemaName: row.private_schema_name, + tableName: row.table_name, + }; + }, + }); diff --git a/packages/express-context/src/loaders/index.ts b/packages/express-context/src/loaders/index.ts index 70187df40..ef4eac3cf 100644 --- a/packages/express-context/src/loaders/index.ts +++ b/packages/express-context/src/loaders/index.ts @@ -12,6 +12,9 @@ * - pubkeyChallengeSettings (services_public.pubkey_settings) * - webauthnSettings(services_public.webauthn_settings) * - authSettings (metaschema_modules_public.sessions_module → tenant DB) + * - encryptedSecrets (metaschema_modules_public.encrypted_secrets_module) + * - userAuth (metaschema_modules_public.user_auth_module) + * - identityProviders (metaschema_modules_public.identity_providers_module) * * To add a new per-db lookup, implement a ModuleLoader and register it: * @@ -47,6 +50,9 @@ export { authSettingsLoader } from './auth-settings'; export { billingLoader } from './billing'; export { inferenceLogLoader } from './inference-log'; export { agentChatLoader } from './agent-chat'; +export { encryptedSecretsLoader } from './encrypted-secrets'; +export { userAuthLoader } from './user-auth'; +export { identityProvidersLoader } from './identity-providers'; /** * Convenience: create a registry pre-loaded with all built-in loaders. @@ -61,6 +67,9 @@ import { authSettingsLoader } from './auth-settings'; import { billingLoader } from './billing'; import { inferenceLogLoader } from './inference-log'; import { agentChatLoader } from './agent-chat'; +import { encryptedSecretsLoader } from './encrypted-secrets'; +import { userAuthLoader } from './user-auth'; +import { identityProvidersLoader } from './identity-providers'; export function createDefaultRegistry() { const registry = createLoaderRegistry(); @@ -73,5 +82,8 @@ export function createDefaultRegistry() { registry.register(billingLoader); registry.register(inferenceLogLoader); registry.register(agentChatLoader); + registry.register(encryptedSecretsLoader); + registry.register(userAuthLoader); + registry.register(identityProvidersLoader); return registry; } diff --git a/packages/express-context/src/loaders/user-auth.ts b/packages/express-context/src/loaders/user-auth.ts new file mode 100644 index 000000000..9bcda1fca --- /dev/null +++ b/packages/express-context/src/loaders/user-auth.ts @@ -0,0 +1,79 @@ +/** + * User Auth Module Loader + * + * Resolves the user_auth_module config from metaschema_modules_public. + * Provides schema name and function names for sign-in/sign-up operations + * including identity-based (OAuth/SSO) auth functions. + */ + +import type { LoaderContext, ModuleLoader } from './types'; +import { createModuleLoader } from './create-loader'; + +// ─── Types ────────────────────────────────────────────────────────────────── + +export interface UserAuthConfig { + schemaName: string; + signInFunction: string; + signUpFunction: string; + signOutFunction: string; + signInCrossOriginFunction: string | null; + requestCrossOriginTokenFunction: string | null; + extendTokenExpires: string; +} + +// ─── SQL ──────────────────────────────────────────────────────────────────── + +const USER_AUTH_MODULE_SQL = ` + SELECT + s.schema_name, + uam.sign_in_function, + uam.sign_up_function, + uam.sign_out_function, + uam.sign_in_cross_origin_function, + uam.request_cross_origin_token_function, + uam.extend_token_expires + FROM metaschema_modules_public.user_auth_module uam + JOIN metaschema_public.schema s ON s.id = uam.schema_id + WHERE uam.database_id = $1 + LIMIT 1 +`; + +// ─── Row Types ────────────────────────────────────────────────────────────── + +interface UserAuthModuleRow { + schema_name: string; + sign_in_function: string; + sign_up_function: string; + sign_out_function: string; + sign_in_cross_origin_function: string | null; + request_cross_origin_token_function: string | null; + extend_token_expires: string; +} + +// ─── Loader ───────────────────────────────────────────────────────────────── + +export const userAuthLoader: ModuleLoader = + createModuleLoader({ + name: 'userAuth', + ttlMs: 5 * 60_000, + async resolve(ctx: LoaderContext) { + const { tenantPool, databaseId } = ctx; + + const result = await tenantPool.query( + USER_AUTH_MODULE_SQL, + [databaseId], + ); + const row = result.rows[0]; + if (!row) return undefined; + + return { + schemaName: row.schema_name, + signInFunction: row.sign_in_function, + signUpFunction: row.sign_up_function, + signOutFunction: row.sign_out_function, + signInCrossOriginFunction: row.sign_in_cross_origin_function, + requestCrossOriginTokenFunction: row.request_cross_origin_token_function, + extendTokenExpires: row.extend_token_expires, + }; + }, + }); diff --git a/packages/express-context/src/types.ts b/packages/express-context/src/types.ts index b2fd2ff45..bd2f70c39 100644 --- a/packages/express-context/src/types.ts +++ b/packages/express-context/src/types.ts @@ -150,6 +150,29 @@ export interface AgentChatConfig { taskTableName: string | null; } +// ─── OAuth / Identity Types ───────────────────────────────────────────────── + +export interface EncryptedSecretsConfig { + schemaName: string; + tableName: string; +} + +export interface UserAuthConfig { + schemaName: string; + signInFunction: string; + signUpFunction: string; + signOutFunction: string; + signInCrossOriginFunction: string | null; + requestCrossOriginTokenFunction: string | null; + extendTokenExpires: string; +} + +export interface IdentityProvidersConfig { + schemaName: string; + privateSchemaName: string; + tableName: string; +} + // ─── Module Types Map ─────────────────────────────────────────────────────── /** @@ -170,6 +193,9 @@ export interface BuiltinModuleMap { billing: BillingConfig; inferenceLog: InferenceLogConfig; agentChat: AgentChatConfig; + encryptedSecrets: EncryptedSecretsConfig; + userAuth: UserAuthConfig; + identityProviders: IdentityProvidersConfig; } // ─── Constructive Context ─────────────────────────────────────────────────── diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 944af29b5..3bfe2fc53 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -260,7 +260,7 @@ importers: version: 5.2.1 grafserv: specifier: 1.0.0 - version: 1.0.0(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.20.1) + version: 1.0.0(@types/node@25.9.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.20.1) graphile-realtime-subscriptions: specifier: workspace:^ version: link:../graphile-realtime-subscriptions/dist @@ -272,7 +272,7 @@ importers: version: link:../../postgres/pg-cache/dist postgraphile: specifier: 5.0.3 - version: 5.0.3(4080119c6ab3f2725faab12a7cbc5738) + version: 5.0.3(ad3b1ddbbeba5ca9cd3b71263258e931) devDependencies: '@types/express': specifier: ^5.0.6 @@ -285,7 +285,7 @@ importers: version: 3.1.14 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.19.19)(typescript@5.9.3) + version: 10.9.2(@types/node@25.9.1)(typescript@5.9.3) publishDirectory: dist graphile/graphile-connection-filter: @@ -731,7 +731,7 @@ importers: version: 0.3.0 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.19.19)(typescript@5.9.3) + version: 10.9.2(@types/node@25.9.1)(typescript@5.9.3) publishDirectory: dist graphile/graphile-search: @@ -833,7 +833,7 @@ importers: version: 1.0.2(graphql@16.13.0) grafserv: specifier: 1.0.0 - version: 1.0.0(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.20.1) + version: 1.0.0(@types/node@25.9.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.20.1) graphile-bucket-provisioner-plugin: specifier: workspace:* version: link:../graphile-bucket-provisioner-plugin/dist @@ -899,7 +899,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.0.3 - version: 5.0.3(4080119c6ab3f2725faab12a7cbc5738) + version: 5.0.3(ad3b1ddbbeba5ca9cd3b71263258e931) request-ip: specifier: ^3.3.0 version: 3.3.0 @@ -930,7 +930,7 @@ importers: version: 3.1.14 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.19.19)(typescript@5.9.3) + version: 10.9.2(@types/node@25.9.1)(typescript@5.9.3) publishDirectory: dist graphile/graphile-sql-expression-validator: @@ -1178,7 +1178,7 @@ importers: version: 5.2.1 grafserv: specifier: 1.0.0 - version: 1.0.0(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.20.1) + version: 1.0.0(@types/node@25.9.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.20.1) graphile-cache: specifier: workspace:^ version: link:../../graphile/graphile-cache/dist @@ -1199,7 +1199,7 @@ importers: version: link:../../postgres/pg-env/dist postgraphile: specifier: 5.0.3 - version: 5.0.3(4080119c6ab3f2725faab12a7cbc5738) + version: 5.0.3(ad3b1ddbbeba5ca9cd3b71263258e931) devDependencies: '@types/express': specifier: ^5.0.6 @@ -1212,7 +1212,7 @@ importers: version: 3.1.14 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.19.19)(typescript@5.9.3) + version: 10.9.2(@types/node@25.9.1)(typescript@5.9.3) publishDirectory: dist graphql/gql-ast: @@ -1473,6 +1473,9 @@ importers: '@constructive-io/graphql-types': specifier: workspace:^ version: link:../types/dist + '@constructive-io/oauth': + specifier: workspace:^ + version: link:../../packages/oauth/dist '@constructive-io/s3-utils': specifier: workspace:^ version: link:../../uploads/s3-utils/dist @@ -1517,7 +1520,7 @@ importers: version: 1.0.2(graphql@16.13.0) grafserv: specifier: 1.0.0 - version: 1.0.0(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.20.1) + version: 1.0.0(@types/node@25.9.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.20.1) graphile-build: specifier: 5.0.2 version: 5.0.2(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0) @@ -1568,7 +1571,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.0.3 - version: 5.0.3(4080119c6ab3f2725faab12a7cbc5738) + version: 5.0.3(ad3b1ddbbeba5ca9cd3b71263258e931) request-ip: specifier: ^3.3.0 version: 3.3.0 @@ -1611,7 +1614,7 @@ importers: version: 3.1.14 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.19.19)(typescript@5.9.3) + version: 10.9.2(@types/node@25.9.1)(typescript@5.9.3) publishDirectory: dist graphql/server-test: @@ -2039,7 +2042,7 @@ importers: version: 7.2.2 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.19.19)(typescript@5.9.3) + version: 10.9.2(@types/node@25.9.1)(typescript@5.9.3) publishDirectory: dist jobs/knative-job-worker: @@ -2502,7 +2505,7 @@ importers: version: 0.3.0 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.19.19)(typescript@5.9.3) + version: 10.9.2(@types/node@25.9.1)(typescript@5.9.3) publishDirectory: dist packages/smtppostmaster: @@ -2531,7 +2534,7 @@ importers: version: 3.18.4 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.19.19)(typescript@5.9.3) + version: 10.9.2(@types/node@25.9.1)(typescript@5.9.3) publishDirectory: dist packages/upload-client: @@ -11328,20 +11331,6 @@ snapshots: - immer - use-sync-external-store - '@graphiql/plugin-doc-explorer@0.4.1(@graphiql/react@0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)))(@types/react@19.2.15)(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))': - dependencies: - '@graphiql/react': 0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) - '@headlessui/react': 2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - graphql: 16.13.0 - react: 19.2.4 - react-compiler-runtime: 19.1.0-rc.1(react@19.2.4) - react-dom: 19.2.4(react@19.2.4) - zustand: 5.0.11(@types/react@19.2.15)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) - transitivePeerDependencies: - - '@types/react' - - immer - - use-sync-external-store - '@graphiql/plugin-doc-explorer@0.4.1(@graphiql/react@0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@25.9.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)))(@types/react@19.2.15)(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))': dependencies: '@graphiql/react': 0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@25.9.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) @@ -11388,22 +11377,6 @@ snapshots: - immer - use-sync-external-store - '@graphiql/plugin-history@0.4.1(@graphiql/react@0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)))(@types/node@22.19.19)(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))': - dependencies: - '@graphiql/react': 0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) - '@graphiql/toolkit': 0.11.3(@types/node@22.19.19)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0) - react: 19.2.4 - react-compiler-runtime: 19.1.0-rc.1(react@19.2.4) - react-dom: 19.2.4(react@19.2.4) - zustand: 5.0.11(@types/react@19.2.15)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) - transitivePeerDependencies: - - '@types/node' - - '@types/react' - - graphql - - graphql-ws - - immer - - use-sync-external-store - '@graphiql/plugin-history@0.4.1(@graphiql/react@0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@25.9.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)))(@types/node@25.9.1)(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))': dependencies: '@graphiql/react': 0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@25.9.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) @@ -11482,37 +11455,6 @@ snapshots: - immer - use-sync-external-store - '@graphiql/react@0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))': - dependencies: - '@graphiql/toolkit': 0.11.3(@types/node@22.19.19)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-visually-hidden': 1.2.4(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - clsx: 1.2.1 - framer-motion: 12.36.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - get-value: 3.0.1 - graphql: 16.13.0 - graphql-language-service: 5.5.0(graphql@16.13.0) - jsonc-parser: 3.3.1 - markdown-it: 14.1.1 - monaco-editor: 0.52.2 - monaco-graphql: 1.7.3(graphql@16.13.0)(monaco-editor@0.52.2)(prettier@3.8.1) - prettier: 3.8.1 - react: 19.2.4 - react-compiler-runtime: 19.1.0-rc.1(react@19.2.4) - react-dom: 19.2.4(react@19.2.4) - set-value: 4.1.0 - zustand: 5.0.11(@types/react@19.2.15)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) - transitivePeerDependencies: - - '@emotion/is-prop-valid' - - '@types/node' - - '@types/react' - - '@types/react-dom' - - graphql-ws - - immer - - use-sync-external-store - '@graphiql/react@0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@25.9.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))': dependencies: '@graphiql/toolkit': 0.11.3(@types/node@25.9.1)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0) @@ -11564,16 +11506,6 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@graphiql/toolkit@0.11.3(@types/node@22.19.19)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)': - dependencies: - '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 - graphql: 16.13.0 - meros: 1.3.2(@types/node@22.19.19) - optionalDependencies: - graphql-ws: 6.0.8(graphql@16.13.0)(ws@8.20.1) - transitivePeerDependencies: - - '@types/node' - '@graphiql/toolkit@0.11.3(@types/node@25.9.1)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)': dependencies: '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 @@ -13273,7 +13205,6 @@ snapshots: '@types/node@25.9.1': dependencies: undici-types: 7.24.6 - optional: true '@types/nodemailer@7.0.11': dependencies: @@ -15090,31 +15021,6 @@ snapshots: - supports-color - use-sync-external-store - grafserv@1.0.0(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.20.1): - dependencies: - '@graphile/lru': 5.0.0 - debug: 4.4.3(supports-color@5.5.0) - eventemitter3: 5.0.4 - grafast: 1.0.2(graphql@16.13.0) - graphile-config: 1.0.1 - graphql: 16.13.0 - graphql-ws: 6.0.8(graphql@16.13.0)(ws@8.20.1) - ruru: 2.0.0(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(debug@4.4.3)(graphile-config@1.0.1)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) - tslib: 2.8.1 - optionalDependencies: - ws: 8.20.1 - transitivePeerDependencies: - - '@fastify/websocket' - - '@types/node' - - '@types/react' - - '@types/react-dom' - - crossws - - immer - - react - - react-dom - - supports-color - - use-sync-external-store - grafserv@1.0.0(@types/node@25.9.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.20.1): dependencies: '@graphile/lru': 5.0.0 @@ -15292,24 +15198,6 @@ snapshots: - immer - use-sync-external-store - graphiql@5.2.2(@emotion/is-prop-valid@1.4.0)(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): - dependencies: - '@graphiql/plugin-doc-explorer': 0.4.1(@graphiql/react@0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)))(@types/react@19.2.15)(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) - '@graphiql/plugin-history': 0.4.1(@graphiql/react@0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)))(@types/node@22.19.19)(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) - '@graphiql/react': 0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) - graphql: 16.13.0 - react: 19.2.4 - react-compiler-runtime: 19.1.0-rc.1(react@19.2.4) - react-dom: 19.2.4(react@19.2.4) - transitivePeerDependencies: - - '@emotion/is-prop-valid' - - '@types/node' - - '@types/react' - - '@types/react-dom' - - graphql-ws - - immer - - use-sync-external-store - graphiql@5.2.2(@emotion/is-prop-valid@1.4.0)(@types/node@25.9.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): dependencies: '@graphiql/plugin-doc-explorer': 0.4.1(@graphiql/react@0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@25.9.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)))(@types/react@19.2.15)(graphql@16.13.0)(react-compiler-runtime@19.1.0-rc.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) @@ -16528,10 +16416,6 @@ snapshots: optionalDependencies: '@types/node': 22.19.15 - meros@1.3.2(@types/node@22.19.19): - optionalDependencies: - '@types/node': 22.19.19 - meros@1.3.2(@types/node@25.9.1): optionalDependencies: '@types/node': 25.9.1 @@ -17611,33 +17495,6 @@ snapshots: - supports-color - utf-8-validate - postgraphile@5.0.3(4080119c6ab3f2725faab12a7cbc5738): - dependencies: - '@dataplan/json': 1.0.0(grafast@1.0.2(graphql@16.13.0)) - '@dataplan/pg': 1.0.3(@dataplan/json@1.0.0(grafast@1.0.2(graphql@16.13.0)))(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) - '@graphile/lru': 5.0.0 - '@types/node': 22.19.19 - '@types/pg': 8.20.0 - debug: 4.4.3(supports-color@5.5.0) - grafast: 1.0.2(graphql@16.13.0) - grafserv: 1.0.0(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.20.1) - graphile-build: 5.0.2(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0) - graphile-build-pg: 5.0.2(@dataplan/pg@1.0.3(@dataplan/json@1.0.0(grafast@1.0.2(graphql@16.13.0)))(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.0.2(graphql@16.13.0))(graphile-build@5.0.2(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) - graphile-config: 1.0.1 - graphile-utils: 5.0.1(@dataplan/pg@1.0.3(@dataplan/json@1.0.0(grafast@1.0.2(graphql@16.13.0)))(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.0.2(graphql@16.13.0))(graphile-build-pg@5.0.2(@dataplan/pg@1.0.3(@dataplan/json@1.0.0(grafast@1.0.2(graphql@16.13.0)))(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.0.2(graphql@16.13.0))(graphile-build@5.0.2(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.0.2(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(tamedevil@0.1.1) - graphql: 16.13.0 - iterall: 1.3.0 - jsonwebtoken: 9.0.3 - pg: 8.21.0 - pg-sql2: 5.0.1 - tamedevil: 0.1.1 - tslib: 2.8.1 - ws: 8.20.1 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - postgraphile@5.0.3(7bbb0860e34b6a7498373453b4dbfe21): dependencies: '@dataplan/json': 1.0.0(grafast@1.0.2(graphql@16.13.0)) @@ -18102,23 +17959,6 @@ snapshots: - immer - use-sync-external-store - ruru-types@2.0.0(@emotion/is-prop-valid@1.4.0)(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): - dependencies: - '@graphiql/toolkit': 0.11.3(@types/node@22.19.19)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0) - graphiql: 5.2.2(@emotion/is-prop-valid@1.4.0)(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) - graphql: 16.13.0 - optionalDependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - transitivePeerDependencies: - - '@emotion/is-prop-valid' - - '@types/node' - - '@types/react' - - '@types/react-dom' - - graphql-ws - - immer - - use-sync-external-store - ruru-types@2.0.0(@emotion/is-prop-valid@1.4.0)(@types/node@25.9.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): dependencies: '@graphiql/toolkit': 0.11.3(@types/node@25.9.1)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0) @@ -18178,27 +18018,6 @@ snapshots: - immer - use-sync-external-store - ruru@2.0.0(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(debug@4.4.3)(graphile-config@1.0.1)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): - dependencies: - '@emotion/is-prop-valid': 1.4.0 - graphile-config: 1.0.1 - graphql: 16.13.0 - http-proxy: 1.18.1(debug@4.4.3) - ruru-types: 2.0.0(@emotion/is-prop-valid@1.4.0)(@types/node@22.19.19)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) - tslib: 2.8.1 - yargs: 17.7.2 - optionalDependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - transitivePeerDependencies: - - '@types/node' - - '@types/react' - - '@types/react-dom' - - debug - - graphql-ws - - immer - - use-sync-external-store - ruru@2.0.0(@types/node@25.9.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(debug@4.4.3)(graphile-config@1.0.1)(graphql-ws@6.0.8(graphql@16.13.0)(ws@8.20.1))(graphql@16.13.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): dependencies: '@emotion/is-prop-valid': 1.4.0 @@ -18689,14 +18508,14 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@10.9.2(@types/node@22.19.19)(typescript@5.9.3): + ts-node@10.9.2(@types/node@25.9.1)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.19.19 + '@types/node': 25.9.1 acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -18777,8 +18596,7 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.24.6: - optional: true + undici-types@7.24.6: {} undici@7.25.0: {}