From 6faac1c80212ebb03c4f860df557865ac4fd970c Mon Sep 17 00:00:00 2001 From: zetazzz Date: Sun, 16 Aug 2026 22:35:25 +0800 Subject: [PATCH] add trusted Graphile preset composition --- .../graphile-preset-composition.test.ts | 342 ++++++++++++++++++ .../middleware/graphile-preset-composition.ts | 285 +++++++++++++++ graphql/server/src/middleware/graphile.ts | 32 +- .../__tests__/graphile-preset-errors.test.ts | 42 +++ packages/errors/src/registry.ts | 26 ++ 5 files changed, 725 insertions(+), 2 deletions(-) create mode 100644 graphql/server/src/middleware/__tests__/graphile-preset-composition.test.ts create mode 100644 graphql/server/src/middleware/graphile-preset-composition.ts create mode 100644 packages/errors/__tests__/graphile-preset-errors.test.ts diff --git a/graphql/server/src/middleware/__tests__/graphile-preset-composition.test.ts b/graphql/server/src/middleware/__tests__/graphile-preset-composition.test.ts new file mode 100644 index 0000000000..88370d4def --- /dev/null +++ b/graphql/server/src/middleware/__tests__/graphile-preset-composition.test.ts @@ -0,0 +1,342 @@ +import type { GraphileConfig } from 'graphile-config'; +import { resolvePreset } from 'graphile-config'; + +import { + assertGraphileCallerPresetsSafe, + composeGraphilePreset, + type ComposeGraphilePresetInput, + type GraphilePresetProtectionPolicy, +} from '../graphile-preset-composition'; + +const callerPlugin: GraphileConfig.Plugin = { + name: 'CallerPlugin', + version: '1.0.0', +}; + +const protectedPlugin: GraphileConfig.Plugin = { + name: 'ProtectedPlugin', + version: '1.0.0', +}; + +const exactService = { + name: 'main', + adaptor: 'constructive-test-adaptor', +} as unknown as NonNullable[number]; + +const protectedContext = jest.fn(() => ({ + pgSettings: { role: 'tenant_runtime' }, +})); +const protectedMaskError = jest.fn((error) => error); + +const protection: GraphilePresetProtectionPolicy = { + protectedPaths: ['pgServices', 'grafast.context', 'grafserv.maskError'], + protectedPluginNames: ['ProtectedPlugin'], +}; + +const compose = ( + overrides: Partial = {} +): GraphileConfig.Preset => + composeGraphilePreset({ + basePresets: [], + callerPresetsTrusted: true, + protection, + protectedPreset: { + plugins: [protectedPlugin], + pgServices: [exactService], + grafserv: { maskError: protectedMaskError }, + grafast: { context: protectedContext }, + }, + ...overrides, + }); + +const captureError = (callback: () => unknown): unknown => { + try { + callback(); + } catch (error) { + return error; + } + throw new Error('Expected callback to throw'); +}; + +describe('Graphile caller preset composition', () => { + it.each([ + { callerExtends: [{ plugins: [callerPlugin] }] }, + { callerPreset: { schema: { defaultBehavior: '-delete' } } }, + ])( + 'rejects non-empty caller code until it is explicitly trusted', + (caller) => { + const getter = jest.fn(() => ({ context: protectedContext })); + const callerPreset = caller.callerPreset ?? {}; + Object.defineProperty(callerPreset, 'unrelatedAccessor', { + enumerable: true, + get: getter, + }); + + expect(() => + compose({ + ...caller, + callerPreset, + callerPresetsTrusted: false, + }) + ).toThrow( + expect.objectContaining({ code: 'GRAPHILE_CALLER_PRESET_NOT_TRUSTED' }) + ); + expect(getter).not.toHaveBeenCalled(); + } + ); + + it('allows empty defaults without widening the trust boundary', () => { + expect(() => + compose({ + callerExtends: [], + callerPreset: {}, + callerPresetsTrusted: false, + }) + ).not.toThrow(); + }); + + it('applies layers in deterministic base, caller, protected order', () => { + const basePreset: GraphileConfig.Preset = { schema: {} }; + const callerExtension: GraphileConfig.Preset = { grafserv: {} }; + const callerPreset: GraphileConfig.Preset = { grafast: {} }; + const protectedExtension: GraphileConfig.Preset = { schema: {} }; + const protectedRootExtension: GraphileConfig.Preset = { grafserv: {} }; + + const result = compose({ + basePresets: [basePreset], + callerExtends: [callerExtension], + callerPreset, + protectedPresets: [protectedExtension], + protectedPreset: { + extends: [protectedRootExtension], + plugins: [protectedPlugin], + }, + }); + + expect(result).toEqual({ + extends: [ + basePreset, + callerExtension, + callerPreset, + protectedExtension, + protectedRootExtension, + ], + plugins: [protectedPlugin], + }); + }); + + it('allows caller plugins and unprotected scope settings', () => { + const resolved = resolvePreset( + compose({ + callerPreset: { + plugins: [callerPlugin], + schema: { defaultBehavior: '-delete' }, + grafserv: { maxRequestLength: 123_456 }, + }, + }) + ); + + expect(resolved.plugins).toEqual( + expect.arrayContaining([callerPlugin, protectedPlugin]) + ); + expect(resolved.schema).toMatchObject({ defaultBehavior: '-delete' }); + expect(resolved.grafserv).toMatchObject({ + maxRequestLength: 123_456, + maskError: protectedMaskError, + }); + expect(resolved.grafast).toMatchObject({ context: protectedContext }); + expect(resolved.pgServices).toEqual([exactService]); + }); + + it.each([ + [{ pgServices: [{ name: 'other' }] }, 'pgServices'], + [{ grafast: { context: () => ({}) } }, 'grafast.context'], + [ + { grafserv: { maskError: (error: unknown) => error } }, + 'grafserv.maskError', + ], + ])( + 'rejects caller ownership of protected paths', + (callerPreset, protectedSetting) => { + const error = captureError(() => + compose({ + callerPreset: callerPreset as unknown as GraphileConfig.Preset, + }) + ); + + expect(error).toMatchObject({ + code: 'GRAPHILE_PROTECTED_PRESET_OVERRIDE', + context: { + presetPath: 'graphile.preset', + protectedSetting, + }, + }); + } + ); + + it('rejects protected paths hidden behind accessors without invoking them', () => { + const getter = jest.fn(() => ({ context: protectedContext })); + const callerPreset: GraphileConfig.Preset = {}; + Object.defineProperty(callerPreset, 'grafast', { + enumerable: true, + get: getter, + }); + + expect(() => compose({ callerPreset })).toThrow( + expect.objectContaining({ + code: 'GRAPHILE_PROTECTED_PRESET_OVERRIDE', + context: expect.objectContaining({ + protectedSetting: 'grafast.context', + }), + }) + ); + expect(getter).not.toHaveBeenCalled(); + }); + + it.each([ + [{ plugins: [protectedPlugin] }, 'plugins.ProtectedPlugin'], + [{ disablePlugins: ['ProtectedPlugin'] }, 'disablePlugins.ProtectedPlugin'], + ])( + 'rejects protected plugin replacement and disablement', + (callerPreset, protectedSetting) => { + const error = captureError(() => + compose({ callerPreset: callerPreset as GraphileConfig.Preset }) + ); + + expect(error).toMatchObject({ + code: 'GRAPHILE_PROTECTED_PRESET_OVERRIDE', + context: { + presetPath: 'graphile.preset', + protectedSetting, + }, + }); + } + ); + + it('rejects protected settings in nested extends with their exact path', () => { + const error = captureError(() => + compose({ + callerExtends: [ + { + extends: [ + { + grafserv: { maskError: protectedMaskError }, + }, + ], + }, + ], + }) + ); + + expect(error).toMatchObject({ + code: 'GRAPHILE_PROTECTED_PRESET_OVERRIDE', + context: { + presetPath: 'graphile.extends[0].extends[0]', + protectedSetting: 'grafserv.maskError', + }, + }); + }); + + it('rejects circular caller preset graphs deterministically', () => { + const cyclic: GraphileConfig.Preset = {}; + cyclic.extends = [cyclic]; + + const error = captureError(() => + assertGraphileCallerPresetsSafe( + { + callerExtends: [cyclic], + callerPresetsTrusted: true, + }, + protection + ) + ); + + expect(error).toMatchObject({ + code: 'GRAPHILE_CALLER_PRESET_INVALID', + context: { + presetPath: 'graphile.extends[0].extends[0]', + reason: 'extends must not contain a cycle', + }, + }); + }); + + it('does not admit configuration inherited through a preset prototype', () => { + const callerPreset = Object.create({ + extends: [{ pgServices: [{ name: 'other' }] }], + }) as GraphileConfig.Preset; + + expect(() => + compose({ callerPreset, callerPresetsTrusted: false }) + ).toThrow( + expect.objectContaining({ code: 'GRAPHILE_CALLER_PRESET_NOT_TRUSTED' }) + ); + + expect(() => compose({ callerPreset })).toThrow( + expect.objectContaining({ + code: 'GRAPHILE_CALLER_PRESET_INVALID', + context: expect.objectContaining({ + presetPath: 'graphile.preset', + reason: 'preset must be a plain object', + }), + }) + ); + }); + + it.each([ + [{ extends: {} }, 'extends must be an array'], + [{ plugins: {} }, 'plugins must be an array'], + [ + { disablePlugins: [protectedPlugin] }, + 'disabled plugin name must be a string', + ], + ])('rejects malformed caller preset fields', (callerPreset, reason) => { + const error = captureError(() => + compose({ + callerPreset: callerPreset as unknown as GraphileConfig.Preset, + }) + ); + + expect(error).toMatchObject({ + code: 'GRAPHILE_CALLER_PRESET_INVALID', + context: expect.objectContaining({ reason }), + }); + }); + + it('accepts a shared nested preset that is not circular', () => { + const shared: GraphileConfig.Preset = { + schema: { defaultBehavior: '-delete' }, + }; + + expect(() => + compose({ + callerExtends: [{ extends: [shared] }, { extends: [shared] }], + }) + ).not.toThrow(); + }); + + it('accepts future feature-owned protection without changing the primitive', () => { + const futureProtection: GraphilePresetProtectionPolicy = { + protectedPaths: ['schema.futureSecuritySetting'], + protectedPluginNames: ['FutureAdmissionPlugin'], + }; + + expect(() => + assertGraphileCallerPresetsSafe( + { + callerPreset: { + schema: { futureSecuritySetting: false }, + } as unknown as GraphileConfig.Preset, + callerPresetsTrusted: true, + }, + futureProtection + ) + ).toThrow( + expect.objectContaining({ + code: 'GRAPHILE_PROTECTED_PRESET_OVERRIDE', + context: expect.objectContaining({ + protectedSetting: 'schema.futureSecuritySetting', + }), + }) + ); + }); +}); diff --git a/graphql/server/src/middleware/graphile-preset-composition.ts b/graphql/server/src/middleware/graphile-preset-composition.ts new file mode 100644 index 0000000000..b09a8a55b2 --- /dev/null +++ b/graphql/server/src/middleware/graphile-preset-composition.ts @@ -0,0 +1,285 @@ +import { errors } from '@constructive-io/errors'; +import type { GraphileConfig } from 'graphile-config'; + +type PresetRecord = Record; + +export interface GraphilePresetProtectionPolicy { + /** Dot-separated preset fields that caller configuration may not own. */ + protectedPaths: readonly string[]; + /** Plugin names that callers may neither register nor disable. */ + protectedPluginNames: readonly string[]; +} + +export interface GraphileCallerPresetInput { + callerExtends?: readonly GraphileConfig.Preset[]; + callerPreset?: Partial; + /** Whether all caller preset code has been admitted into the process TCB. */ + callerPresetsTrusted: boolean; +} + +export interface ComposeGraphilePresetInput extends GraphileCallerPresetInput { + /** CNC defaults that trusted callers may customize. */ + basePresets: readonly GraphileConfig.Preset[]; + /** CNC-owned presets resolved after caller customization. */ + protectedPresets?: readonly GraphileConfig.Preset[]; + /** CNC-owned root fields that retain final precedence. */ + protectedPreset: GraphileConfig.Preset; + protection: GraphilePresetProtectionPolicy; +} + +const isObjectRecord = (value: unknown): value is PresetRecord => + typeof value === 'object' && value !== null && !Array.isArray(value); + +const isPlainObjectRecord = (value: unknown): value is PresetRecord => { + if (!isObjectRecord(value)) return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +}; + +const invalidCallerPreset = (presetPath: string, reason: string): never => { + throw errors.GRAPHILE_CALLER_PRESET_INVALID({ presetPath, reason }); +}; + +const rejectProtectedOverride = ( + presetPath: string, + protectedSetting: string +): never => { + throw errors.GRAPHILE_PROTECTED_PRESET_OVERRIDE({ + presetPath, + protectedSetting, + }); +}; + +const readOwnDataProperty = ( + record: PresetRecord, + field: string, + presetPath: string +): { present: false } | { present: true; value: unknown } => { + const descriptor = Object.getOwnPropertyDescriptor(record, field); + if (!descriptor) return { present: false }; + if (!('value' in descriptor)) { + return invalidCallerPreset( + presetPath, + `${field} must be declared as a data property` + ); + } + return { present: true, value: descriptor.value }; +}; + +/** + * Accessors and non-object intermediate values count as overrides. They could + * otherwise hide a protected value from validation and reveal it at resolve + * time. + */ +const ownsProtectedPath = ( + preset: PresetRecord, + pathSegments: readonly string[] +): boolean => { + let current = preset; + for (let index = 0; index < pathSegments.length; index += 1) { + const descriptor = Object.getOwnPropertyDescriptor( + current, + pathSegments[index] + ); + if (!descriptor) return false; + if (!('value' in descriptor)) return true; + if (index === pathSegments.length - 1) return true; + if (!isObjectRecord(descriptor.value)) return true; + current = descriptor.value; + } + return false; +}; + +const assertProtectedPathsAreNotOwned = ( + preset: PresetRecord, + presetPath: string, + protectedPaths: readonly string[] +): void => { + for (const protectedSetting of protectedPaths) { + const pathSegments = protectedSetting.split('.'); + if (ownsProtectedPath(preset, pathSegments)) { + rejectProtectedOverride(presetPath, protectedSetting); + } + } +}; + +const assertProtectedPluginsAreNotOwned = ( + preset: PresetRecord, + presetPath: string, + protectedPluginNames: ReadonlySet +): void => { + const pluginsProperty = readOwnDataProperty(preset, 'plugins', presetPath); + if (pluginsProperty.present) { + const plugins = pluginsProperty.value; + if (!Array.isArray(plugins)) { + return invalidCallerPreset(presetPath, 'plugins must be an array'); + } + plugins.forEach((plugin, index) => { + const pluginPath = `${presetPath}.plugins[${index}]`; + if (!isPlainObjectRecord(plugin)) { + return invalidCallerPreset(pluginPath, 'plugin must be an object'); + } + const nameProperty = readOwnDataProperty(plugin, 'name', pluginPath); + const pluginName = nameProperty.present ? nameProperty.value : undefined; + if (typeof pluginName !== 'string') { + return invalidCallerPreset(pluginPath, 'plugin name must be a string'); + } + if (protectedPluginNames.has(pluginName)) { + rejectProtectedOverride(presetPath, `plugins.${pluginName}`); + } + }); + } + + const disabledProperty = readOwnDataProperty( + preset, + 'disablePlugins', + presetPath + ); + if (disabledProperty.present) { + const disabledPlugins = disabledProperty.value; + if (!Array.isArray(disabledPlugins)) { + return invalidCallerPreset(presetPath, 'disablePlugins must be an array'); + } + disabledPlugins.forEach((pluginName, index) => { + if (typeof pluginName !== 'string') { + return invalidCallerPreset( + `${presetPath}.disablePlugins[${index}]`, + 'disabled plugin name must be a string' + ); + } + if (protectedPluginNames.has(pluginName)) { + rejectProtectedOverride(presetPath, `disablePlugins.${pluginName}`); + } + }); + } +}; + +const assertPresetDoesNotOverrideProtectedSettings = ( + preset: unknown, + presetPath: string, + protection: GraphilePresetProtectionPolicy, + protectedPluginNames: ReadonlySet, + visiting: Set, + validated: Set +): void => { + if (!isPlainObjectRecord(preset)) { + return invalidCallerPreset(presetPath, 'preset must be a plain object'); + } + const presetRecord = preset as PresetRecord; + if (visiting.has(presetRecord)) { + invalidCallerPreset(presetPath, 'extends must not contain a cycle'); + } + if (validated.has(presetRecord)) return; + + visiting.add(presetRecord); + assertProtectedPathsAreNotOwned( + presetRecord, + presetPath, + protection.protectedPaths + ); + assertProtectedPluginsAreNotOwned( + presetRecord, + presetPath, + protectedPluginNames + ); + + const extendsProperty = readOwnDataProperty( + presetRecord, + 'extends', + presetPath + ); + if (extendsProperty.present) { + const extendedPresets = extendsProperty.value; + if (!Array.isArray(extendedPresets)) { + return invalidCallerPreset(presetPath, 'extends must be an array'); + } + extendedPresets.forEach((nestedPreset, index) => { + assertPresetDoesNotOverrideProtectedSettings( + nestedPreset, + `${presetPath}.extends[${index}]`, + protection, + protectedPluginNames, + visiting, + validated + ); + }); + } + + visiting.delete(presetRecord); + validated.add(presetRecord); +}; + +const hasCallerPresetConfiguration = ( + input: GraphileCallerPresetInput +): boolean => { + if (input.callerExtends !== undefined) { + if (!Array.isArray(input.callerExtends)) return true; + if (input.callerExtends.length > 0) return true; + } + if (input.callerPreset === undefined) return false; + if (!isPlainObjectRecord(input.callerPreset)) return true; + return Reflect.ownKeys(input.callerPreset).length > 0; +}; + +/** Validate caller code before it enters Graphile's preset resolver. */ +export const assertGraphileCallerPresetsSafe = ( + input: GraphileCallerPresetInput, + protection: GraphilePresetProtectionPolicy +): void => { + if (!input.callerPresetsTrusted && hasCallerPresetConfiguration(input)) { + throw errors.GRAPHILE_CALLER_PRESET_NOT_TRUSTED(); + } + + const callerExtends = input.callerExtends ?? []; + if (!Array.isArray(callerExtends)) { + invalidCallerPreset('graphile.extends', 'value must be an array'); + } + + const protectedPluginNames = new Set(protection.protectedPluginNames); + const validated = new Set(); + callerExtends.forEach((preset, index) => { + assertPresetDoesNotOverrideProtectedSettings( + preset, + `graphile.extends[${index}]`, + protection, + protectedPluginNames, + new Set(), + validated + ); + }); + if (input.callerPreset !== undefined) { + assertPresetDoesNotOverrideProtectedSettings( + input.callerPreset, + 'graphile.preset', + protection, + protectedPluginNames, + new Set(), + validated + ); + } +}; + +/** + * Compose Graphile configuration in deterministic trust order. CNC-owned root + * fields are emitted last and therefore keep final precedence. + */ +export const composeGraphilePreset = ( + input: ComposeGraphilePresetInput +): GraphileConfig.Preset => { + assertGraphileCallerPresetsSafe(input, input.protection); + + const callerExtends = input.callerExtends ?? []; + const { extends: protectedRootExtends = [], ...protectedRoot } = + input.protectedPreset; + + return { + extends: [ + ...input.basePresets, + ...callerExtends, + ...(input.callerPreset ? [input.callerPreset] : []), + ...(input.protectedPresets ?? []), + ...protectedRootExtends, + ], + ...protectedRoot, + }; +}; diff --git a/graphql/server/src/middleware/graphile.ts b/graphql/server/src/middleware/graphile.ts index e6de98f7ad..28705b25fb 100644 --- a/graphql/server/src/middleware/graphile.ts +++ b/graphql/server/src/middleware/graphile.ts @@ -21,6 +21,10 @@ import { HandlerCreationError } from '../errors/api-errors'; import { respondWithGraphQLError } from '../errors/graphql-response'; import { AuthCookiePlugin } from '../plugins/auth-cookie-plugin'; import type { DatabaseSettings } from '../types'; +import { + composeGraphilePreset, + type GraphilePresetProtectionPolicy +} from './graphile-preset-composition'; import { observeGraphileBuild } from './observability/graphile-build-stats'; const maskErrorLog = new Logger('graphile:maskError'); @@ -152,6 +156,22 @@ export function clearInFlightMap(): void { const log = new Logger('graphile'); const reqLabel = (req: Request): string => (req.requestId ? `[${req.requestId}]` : '[req]'); +// Protect only fields and plugins currently owned by this server. Future +// features extend this policy alongside their own activation. +const GRAPHILE_PRESET_PROTECTION = { + protectedPaths: [ + 'pgServices', + 'grafast.context', + 'grafast.explain', + 'grafserv.graphqlPath', + 'grafserv.graphiqlPath', + 'grafserv.graphiql', + 'grafserv.graphiqlOnGraphQLGET', + 'grafserv.maskError' + ], + protectedPluginNames: ['AuthCookiePlugin', 'FunctionBindingsPlugin'] +} as const satisfies GraphilePresetProtectionPolicy; + /** * Build a PostGraphile v5 preset for a tenant. * @@ -169,8 +189,7 @@ const buildPreset = ( apiId?: string, compute?: ComputeConfig ): GraphileConfig.Preset => { - return { - extends: [createConstructivePreset(databaseSettings)], + const protectedPreset: GraphileConfig.Preset = { plugins: [ AuthCookiePlugin, // Only registered when the compute module is provisioned for this @@ -317,6 +336,15 @@ const buildPreset = ( } } }; + + return composeGraphilePreset({ + basePresets: [createConstructivePreset(databaseSettings)], + // Caller configuration remains intentionally dormant until build/cache + // identity includes preset composition (F17). + callerPresetsTrusted: false, + protection: GRAPHILE_PRESET_PROTECTION, + protectedPreset + }); }; export const graphile = (opts: ConstructiveOptions): RequestHandler => { diff --git a/packages/errors/__tests__/graphile-preset-errors.test.ts b/packages/errors/__tests__/graphile-preset-errors.test.ts new file mode 100644 index 0000000000..789bef9662 --- /dev/null +++ b/packages/errors/__tests__/graphile-preset-errors.test.ts @@ -0,0 +1,42 @@ +import { classify, errors } from '../src'; + +describe('Graphile preset configuration errors', () => { + it('classifies caller trust failures as internal startup errors', () => { + const error = errors.GRAPHILE_CALLER_PRESET_NOT_TRUSTED(); + + expect(error).toMatchObject({ + code: 'GRAPHILE_CALLER_PRESET_NOT_TRUSTED', + errorClass: 'internal', + http: 500, + }); + expect(classify(error.code)).toBe('internal'); + }); + + it('retains safe context for malformed caller presets', () => { + const error = errors.GRAPHILE_CALLER_PRESET_INVALID({ + presetPath: 'graphile.extends[0]', + reason: 'extends must not contain a cycle', + }); + + expect(error.code).toBe('GRAPHILE_CALLER_PRESET_INVALID'); + expect(error.context).toEqual({ + presetPath: 'graphile.extends[0]', + reason: 'extends must not contain a cycle', + }); + expect(error.message).toContain('graphile.extends[0]'); + }); + + it('identifies the protected setting without including its value', () => { + const error = errors.GRAPHILE_PROTECTED_PRESET_OVERRIDE({ + presetPath: 'graphile.preset', + protectedSetting: 'grafast.context', + }); + + expect(error.code).toBe('GRAPHILE_PROTECTED_PRESET_OVERRIDE'); + expect(error.context).toEqual({ + presetPath: 'graphile.preset', + protectedSetting: 'grafast.context', + }); + expect(error.message).not.toContain('pgSettings'); + }); +}); diff --git a/packages/errors/src/registry.ts b/packages/errors/src/registry.ts index 7c5cf70fc1..e61af46746 100644 --- a/packages/errors/src/registry.ts +++ b/packages/errors/src/registry.ts @@ -358,6 +358,32 @@ export const registry = { message: 'A value conflicts with an existing record.' }), + // =========================================================================== + // Graphile startup configuration (internal) + // =========================================================================== + GRAPHILE_CALLER_PRESET_NOT_TRUSTED: defineError({ + code: 'GRAPHILE_CALLER_PRESET_NOT_TRUSTED', + class: 'internal', + http: 500, + message: 'Graphile caller presets have not been admitted into the server trust boundary.' + }), + GRAPHILE_CALLER_PRESET_INVALID: defineError<{ presetPath: string; reason: string }>({ + code: 'GRAPHILE_CALLER_PRESET_INVALID', + class: 'internal', + http: 500, + message: 'Graphile caller preset "{{presetPath}}" is invalid: {{reason}}.' + }), + GRAPHILE_PROTECTED_PRESET_OVERRIDE: defineError<{ + presetPath: string; + protectedSetting: string; + }>({ + code: 'GRAPHILE_PROTECTED_PRESET_OVERRIDE', + class: 'internal', + http: 500, + message: + 'Graphile caller preset "{{presetPath}}" may not configure protected setting "{{protectedSetting}}".' + }), + // =========================================================================== // pgpm CLI / engine (mostly internal) — behavior preserved from the former // pgpm/types error-factory so existing call sites are unchanged.