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/cookie.ts b/graphql/server/src/middleware/cookie.ts index bb9244639..d7a88131b 100644 --- a/graphql/server/src/middleware/cookie.ts +++ b/graphql/server/src/middleware/cookie.ts @@ -1,11 +1,28 @@ import type { Request, Response } from 'express'; -import type { AuthSettings } from '../types'; +import type { AuthSettings, PgInterval } from '../types'; export const SESSION_COOKIE_NAME = 'constructive_session'; export const DEVICE_TOKEN_COOKIE_NAME = 'constructive_device_token'; const DEVICE_TOKEN_MAX_AGE = 90 * 24 * 60 * 60; // 90 days in seconds +const parseIntervalToSeconds = (interval: string | PgInterval | null | undefined): number | null => { + if (!interval) return null; + if (typeof interval === 'string') { + const parsed = parseInt(interval, 10); + return isNaN(parsed) ? null : parsed; + } + let totalSeconds = 0; + if (interval.years) totalSeconds += interval.years * 365 * 24 * 60 * 60; + if (interval.months) totalSeconds += interval.months * 30 * 24 * 60 * 60; + if (interval.days) totalSeconds += interval.days * 24 * 60 * 60; + if (interval.hours) totalSeconds += interval.hours * 60 * 60; + if (interval.minutes) totalSeconds += interval.minutes * 60; + if (interval.seconds) totalSeconds += interval.seconds; + if (interval.milliseconds) totalSeconds += interval.milliseconds / 1000; + return totalSeconds > 0 ? totalSeconds : null; +}; + export interface CookieConfig { secure: boolean; sameSite: 'strict' | 'lax' | 'none'; @@ -25,11 +42,11 @@ export const getSessionCookieConfig = ( const DEFAULT_MAX_AGE = 86400; // 24 hours let maxAge = DEFAULT_MAX_AGE; if (rememberMe && authSettings?.rememberMeDuration) { - const parsed = parseInt(authSettings.rememberMeDuration, 10); - if (!isNaN(parsed)) maxAge = parsed; + const parsed = parseIntervalToSeconds(authSettings.rememberMeDuration); + if (parsed !== null) maxAge = parsed; } else if (authSettings?.cookieMaxAge) { - const parsed = parseInt(authSettings.cookieMaxAge, 10); - if (!isNaN(parsed)) maxAge = parsed; + const parsed = parseIntervalToSeconds(authSettings.cookieMaxAge); + if (parsed !== null) maxAge = parsed; } return { diff --git a/graphql/server/src/middleware/oauth.ts b/graphql/server/src/middleware/oauth.ts new file mode 100644 index 000000000..4d20b772e --- /dev/null +++ b/graphql/server/src/middleware/oauth.ts @@ -0,0 +1,762 @@ +/** + * 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, + ConnectedAccountsConfig, + 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'; + +interface PgInterval { + years?: number; + months?: number; + days?: number; + hours?: number; + minutes?: number; + seconds?: number; + milliseconds?: number; +} + +/** + * Parse PostgreSQL interval to milliseconds. + * Handles: pg library object {minutes: 10}, string '10 minutes', '00:10:00' + */ +function parseIntervalToMs( + interval: string | PgInterval | null | undefined, +): number { + if (!interval) return DEFAULT_OAUTH_STATE_MAX_AGE; + + // Handle pg library interval object (e.g., {minutes: 10}) + if (typeof interval === 'object') { + const ms = + (interval.days || 0) * 24 * 60 * 60 * 1000 + + (interval.hours || 0) * 60 * 60 * 1000 + + (interval.minutes || 0) * 60 * 1000 + + (interval.seconds || 0) * 1000 + + (interval.milliseconds || 0); + return ms || DEFAULT_OAUTH_STATE_MAX_AGE; + } + + // Handle HH:MM:SS format (PostgreSQL default interval output) + const hhmmss = interval.match(/^(\d+):(\d+):(\d+)$/); + if (hhmmss) { + const hours = parseInt(hhmmss[1], 10); + const minutes = parseInt(hhmmss[2], 10); + const seconds = parseInt(hhmmss[3], 10); + return (hours * 3600 + minutes * 60 + seconds) * 1000; + } + + // Handle "N unit" format (e.g., "10 minutes") + const match = interval.match(/^(\d+)\s*(second|minute|hour|day)s?$/i); + if (!match) return DEFAULT_OAUTH_STATE_MAX_AGE; + const value = parseInt(match[1], 10); + const unit = match[2].toLowerCase(); + const multipliers: Record = { + second: 1000, + minute: 60 * 1000, + hour: 60 * 60 * 1000, + day: 24 * 60 * 60 * 1000, + }; + return value * (multipliers[unit] || 60 * 1000); +} + +// ============================================================================= +// 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; + connectedAccounts: ConnectedAccountsConfig | undefined; +} + +async function resolveOAuthModules( + ctx: ConstructiveContext, +): Promise { + const [identityProviders, encryptedSecrets, userAuth, authSettings, connectedAccounts] = + await Promise.all([ + ctx.useModule('identityProviders'), + ctx.useModule('encryptedSecrets'), + ctx.useModule('userAuth'), + ctx.useModule('authSettings'), + ctx.useModule('connectedAccounts'), + ]); + + if (!identityProviders || !encryptedSecrets || !userAuth) { + return null; + } + + return { identityProviders, encryptedSecrets, userAuth, authSettings, connectedAccounts }; +} + +// ============================================================================= +// 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, tableName: encryptedTableName } = + modules.encryptedSecrets; + + const sql = ` + SELECT + ip.slug, + ip.kind, + ip.display_name, + ip.enabled, + ip.client_id, + convert_from(secrets.value, 'UTF8') as client_secret, + ip.authorization_url, + ip.token_url, + ip.userinfo_url, + ip.scopes, + ip.pkce_enabled + FROM "${privateSchemaName}"."${tableName}" ip + LEFT JOIN "${encryptedSchema}"."${encryptedTableName}" secrets ON secrets.id = ip.client_secret_id + 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); + const { authSettings } = modules; + const errorRedirectPath = + authSettings?.oauthErrorRedirectPath || DEFAULT_ERROR_REDIRECT_PATH; + + if (!providerConfig) { + log.warn(`[oauth] Provider ${provider} not found or not configured`); + return redirectToError( + res, + baseUrl, + errorRedirectPath, + 'PROVIDER_NOT_CONFIGURED', + provider, + ); + } + + const stateMaxAge = parseIntervalToMs(authSettings?.oauthStateMaxAge); + const state = createSignedState( + { redirect_uri: redirectUri, provider }, + stateMaxAge, + ); + + res.cookie(OAUTH_STATE_COOKIE, state, { + httpOnly: authSettings?.cookieHttponly ?? true, + secure: authSettings?.cookieSecure ?? isProduction, + maxAge: stateMaxAge, + sameSite: (authSettings?.cookieSamesite as 'lax' | 'strict' | 'none') ?? '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, + ); + } + + let modules: OAuthModules | null = null; + try { + 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 { authSettings } = modules; + const errorRedirectPath = + authSettings?.oauthErrorRedirectPath || DEFAULT_ERROR_REDIRECT_PATH; + const requireVerifiedEmail = + authSettings?.oauthRequireVerifiedEmail ?? true; + + const providerConfig = await getIdentityProvider( + ctx, + modules, + provider, + ); + if (!providerConfig) { + log.error(`[oauth] Provider ${provider} not found in database`); + return redirectToError( + res, + baseUrl, + errorRedirectPath, + '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 { identityProviders, connectedAccounts } = modules; + const authPrivateSchema = identityProviders.privateSchemaName; + + // Check if identity exists using ctx.pool (bypasses RLS) + let identityExists = false; + if (connectedAccounts) { + const checkSql = ` + SELECT 1 FROM "${connectedAccounts.privateSchemaName}"."${connectedAccounts.tableName}" + WHERE service = $1 AND identifier = $2 + LIMIT 1 + `; + const checkResult = await ctx.pool.query(checkSql, [ + profile.provider, + profile.providerId, + ]); + identityExists = checkResult.rows.length > 0; + } + + const emailVerified = isEmailVerified(profile); + + // For signup, check email verification requirement before entering transaction + if (!identityExists && requireVerifiedEmail && !emailVerified) { + log.warn( + `[oauth] Rejecting unverified email for signup: ${profile.email}`, + ); + return redirectToError( + res, + baseUrl, + errorRedirectPath, + 'EMAIL_NOT_VERIFIED', + provider, + ); + } + + // 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 details = { + provider: profile.provider, + sub: profile.providerId, + email: profile.email, + email_verified: emailVerified, + name: profile.name, + picture: profile.picture, + raw_userinfo: profile.raw, + }; + + if (identityExists) { + // Identity exists, sign in + const signInSql = ` + SELECT * FROM "${authPrivateSchema}".sign_in_identity( + $1::text, $2::text, $3::jsonb, $4::text, 'access_token'::text, $5::boolean, $6::text + ) + `; + const signInResult = await client.query(signInSql, [ + profile.provider, + profile.providerId, + JSON.stringify(details), + profile.email, + true, + deviceToken, + ]); + return signInResult.rows[0] || {}; + } else { + // Identity doesn't exist, sign up + log.info( + `[oauth] Account not found for ${profile.email}, attempting signup`, + ); + + const signUpSql = ` + SELECT * FROM "${authPrivateSchema}".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 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); + const fallbackPath = + modules?.authSettings?.oauthErrorRedirectPath || + DEFAULT_ERROR_REDIRECT_PATH; + redirectToError(res, baseUrl, fallbackPath, '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/graphql/server/src/types.ts b/graphql/server/src/types.ts index 474ad0fc2..12435f624 100644 --- a/graphql/server/src/types.ts +++ b/graphql/server/src/types.ts @@ -11,6 +11,7 @@ export type { CorsModuleData, DatabaseSettings, GenericModuleData, + PgInterval, PubkeyChallengeSettings, PublicKeyChallengeData, RlsModule, diff --git a/packages/express-context/src/index.ts b/packages/express-context/src/index.ts index f70907c6a..28e677d2d 100644 --- a/packages/express-context/src/index.ts +++ b/packages/express-context/src/index.ts @@ -52,8 +52,13 @@ export type { GenericModuleData, InferenceLogConfig, PublicKeyChallengeData, + PgInterval, PubkeyChallengeSettings, RlsModule, + ConnectedAccountsConfig, + EncryptedSecretsConfig, + IdentityProvidersConfig, + UserAuthConfig, WebauthnSettings, WithPgClient, } from './types'; @@ -92,6 +97,10 @@ export { pubkeyLoader, rlsLoader, webauthnLoader, + encryptedSecretsLoader, + userAuthLoader, + identityProvidersLoader, + connectedAccountsLoader, } from './loaders'; // Side-effect: Express type augmentation diff --git a/packages/express-context/src/loaders/auth-settings.ts b/packages/express-context/src/loaders/auth-settings.ts index ff5fcfb93..413eb3696 100644 --- a/packages/express-context/src/loaders/auth-settings.ts +++ b/packages/express-context/src/loaders/auth-settings.ts @@ -10,7 +10,7 @@ * database rather than the services database. */ -import type { AuthSettings } from '../types'; +import type { AuthSettings, PgInterval } from '../types'; import type { LoaderContext, ModuleLoader } from './types'; import { createModuleLoader } from './create-loader'; @@ -33,7 +33,10 @@ const buildAuthSettingsQuery = (schemaName: string, tableName: string) => ` cookie_path, remember_me_duration, enable_captcha, - captcha_site_key + captcha_site_key, + oauth_state_max_age, + oauth_require_verified_email, + oauth_error_redirect_path FROM "${schemaName}"."${tableName}" LIMIT 1 `; @@ -45,11 +48,14 @@ interface AuthSettingsRow { cookie_samesite: string; cookie_domain: string | null; cookie_httponly: boolean; - cookie_max_age: string | null; + cookie_max_age: string | PgInterval | null; cookie_path: string; - remember_me_duration: string | null; + remember_me_duration: string | PgInterval | null; enable_captcha: boolean; captcha_site_key: string | null; + oauth_state_max_age: string | PgInterval | null; + oauth_require_verified_email: boolean; + oauth_error_redirect_path: string | null; } // ─── Loader ───────────────────────────────────────────────────────────────── @@ -84,6 +90,9 @@ export const authSettingsLoader: ModuleLoader = createModuleLoader rememberMeDuration: row.remember_me_duration, enableCaptcha: row.enable_captcha, captchaSiteKey: row.captcha_site_key, + oauthStateMaxAge: row.oauth_state_max_age, + oauthRequireVerifiedEmail: row.oauth_require_verified_email, + oauthErrorRedirectPath: row.oauth_error_redirect_path, }; }, }); diff --git a/packages/express-context/src/loaders/connected-accounts.ts b/packages/express-context/src/loaders/connected-accounts.ts new file mode 100644 index 000000000..fe56332e5 --- /dev/null +++ b/packages/express-context/src/loaders/connected-accounts.ts @@ -0,0 +1,63 @@ +/** + * Connected Accounts Module Loader + * + * Resolves the connected_accounts_module config from metaschema_modules_public. + * Provides schema names for querying OAuth identity associations. + */ + +import type { LoaderContext, ModuleLoader } from './types'; +import { createModuleLoader } from './create-loader'; + +// ─── Types ────────────────────────────────────────────────────────────────── + +export interface ConnectedAccountsConfig { + schemaName: string; + privateSchemaName: string; + tableName: string; +} + +// ─── SQL ──────────────────────────────────────────────────────────────────── + +const CONNECTED_ACCOUNTS_MODULE_SQL = ` + SELECT + s.schema_name, + ps.schema_name AS private_schema_name, + cam.table_name + FROM metaschema_modules_public.connected_accounts_module cam + JOIN metaschema_public.schema s ON s.id = cam.schema_id + JOIN metaschema_public.schema ps ON ps.id = cam.private_schema_id + WHERE cam.database_id = $1 + LIMIT 1 +`; + +// ─── Row Types ────────────────────────────────────────────────────────────── + +interface ConnectedAccountsModuleRow { + schema_name: string; + private_schema_name: string; + table_name: string; +} + +// ─── Loader ───────────────────────────────────────────────────────────────── + +export const connectedAccountsLoader: ModuleLoader = + createModuleLoader({ + name: 'connectedAccounts', + ttlMs: 5 * 60_000, + async resolve(ctx: LoaderContext) { + const { tenantPool, databaseId } = ctx; + + const result = await tenantPool.query( + CONNECTED_ACCOUNTS_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/encrypted-secrets.ts b/packages/express-context/src/loaders/encrypted-secrets.ts new file mode 100644 index 000000000..241396449 --- /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 app_secrets table from + * metaschema_modules_public.config_secrets_user_module. The generator creates + * both user_secrets and app_secrets in the same schema, so we use the schema + * from config_secrets_user_module and hardcode table name to 'app_secrets'. + * + * 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 + FROM metaschema_modules_public.config_secrets_user_module csm + JOIN metaschema_public.schema s ON s.id = csm.schema_id + WHERE csm.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; + }>(ENCRYPTED_SECRETS_MODULE_SQL, [databaseId]); + + const row = result.rows[0]; + if (!row) return undefined; + + return { + schemaName: row.schema_name, + tableName: 'app_secrets', + }; + }, + }); 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..31c490d95 100644 --- a/packages/express-context/src/loaders/index.ts +++ b/packages/express-context/src/loaders/index.ts @@ -12,6 +12,10 @@ * - pubkeyChallengeSettings (services_public.pubkey_settings) * - webauthnSettings(services_public.webauthn_settings) * - authSettings (metaschema_modules_public.sessions_module → tenant DB) + * - encryptedSecrets (metaschema_modules_public.config_secrets_user_module → app_secrets) + * - userAuth (metaschema_modules_public.user_auth_module) + * - identityProviders (metaschema_modules_public.identity_providers_module) + * - connectedAccounts (metaschema_modules_public.connected_accounts_module) * * To add a new per-db lookup, implement a ModuleLoader and register it: * @@ -47,6 +51,10 @@ 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'; +export { connectedAccountsLoader } from './connected-accounts'; /** * Convenience: create a registry pre-loaded with all built-in loaders. @@ -61,6 +69,10 @@ 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'; +import { connectedAccountsLoader } from './connected-accounts'; export function createDefaultRegistry() { const registry = createLoaderRegistry(); @@ -73,5 +85,9 @@ export function createDefaultRegistry() { registry.register(billingLoader); registry.register(inferenceLogLoader); registry.register(agentChatLoader); + registry.register(encryptedSecretsLoader); + registry.register(userAuthLoader); + registry.register(identityProvidersLoader); + registry.register(connectedAccountsLoader); 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..b4447d8b6 --- /dev/null +++ b/packages/express-context/src/loaders/user-auth.ts @@ -0,0 +1,86 @@ +/** + * 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; + sessionCredentialsSchemaName: 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, + sc_schema.schema_name AS session_credentials_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 + LEFT JOIN metaschema_public.table sc_table ON sc_table.id = uam.session_credentials_table_id + LEFT JOIN metaschema_public.schema sc_schema ON sc_schema.id = sc_table.schema_id + WHERE uam.database_id = $1 + LIMIT 1 +`; + +// ─── Row Types ────────────────────────────────────────────────────────────── + +interface UserAuthModuleRow { + schema_name: string; + session_credentials_schema_name: string | null; + 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, + sessionCredentialsSchemaName: + row.session_credentials_schema_name || 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..25193f6a9 100644 --- a/packages/express-context/src/types.ts +++ b/packages/express-context/src/types.ts @@ -85,16 +85,29 @@ export interface RlsModule { currentUserAgent: string; } +export interface PgInterval { + years?: number; + months?: number; + days?: number; + hours?: number; + minutes?: number; + seconds?: number; + milliseconds?: number; +} + export interface AuthSettings { cookieSecure?: boolean; cookieSamesite?: string; cookieDomain?: string | null; cookieHttponly?: boolean; - cookieMaxAge?: string | null; + cookieMaxAge?: string | PgInterval | null; cookiePath?: string; - rememberMeDuration?: string | null; + rememberMeDuration?: string | PgInterval | null; enableCaptcha?: boolean; captchaSiteKey?: string | null; + oauthStateMaxAge?: string | PgInterval | null; + oauthRequireVerifiedEmail?: boolean; + oauthErrorRedirectPath?: string | null; } export interface ApiStructure { @@ -150,6 +163,36 @@ export interface AgentChatConfig { taskTableName: string | null; } +// ─── OAuth / Identity Types ───────────────────────────────────────────────── + +export interface EncryptedSecretsConfig { + schemaName: string; + tableName: string; +} + +export interface UserAuthConfig { + schemaName: string; + sessionCredentialsSchemaName: string; + signInFunction: string; + signUpFunction: string; + signOutFunction: string; + signInCrossOriginFunction: string | null; + requestCrossOriginTokenFunction: string | null; + extendTokenExpires: string; +} + +export interface IdentityProvidersConfig { + schemaName: string; + privateSchemaName: string; + tableName: string; +} + +export interface ConnectedAccountsConfig { + schemaName: string; + privateSchemaName: string; + tableName: string; +} + // ─── Module Types Map ─────────────────────────────────────────────────────── /** @@ -170,6 +213,10 @@ export interface BuiltinModuleMap { billing: BillingConfig; inferenceLog: InferenceLogConfig; agentChat: AgentChatConfig; + encryptedSecrets: EncryptedSecretsConfig; + userAuth: UserAuthConfig; + identityProviders: IdentityProvidersConfig; + connectedAccounts: ConnectedAccountsConfig; } // ─── 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: {}