diff --git a/graphql/env/README.md b/graphql/env/README.md index e5084a59d..c31dd2603 100644 --- a/graphql/env/README.md +++ b/graphql/env/README.md @@ -57,6 +57,14 @@ In addition to all environment variables supported by `@pgpmjs/env`, this packag - `API_ANON_ROLE` - Anonymous role name - `API_ROLE_NAME` - Default role name +### OAuth Server +- `OAUTH_ENABLED` - Explicitly enable the unified-auth Provider flow (default: `false`) +- `OAUTH_PROVIDER_REQUEST_TIMEOUT_MS` - Per-request Provider timeout in milliseconds (default: `10000`, maximum: `60000`) + +Provider endpoints, client IDs, secrets, scopes, and policy are Tenant data; +they are not process environment variables. Explicit malformed OAuth values +fail during option resolution instead of falling back silently. + ## Defaults GraphQL defaults are provided by `@constructive-io/graphql-types`: @@ -76,6 +84,10 @@ GraphQL defaults are provided by `@constructive-io/graphql-types`: isPublic: true, metaSchemas: ['routing_public', 'metaschema_public', 'metaschema_modules_public'], routingSchema: 'routing_public' + }, + oauth: { + enabled: false, + providerRequestTimeoutMs: 10000 } } ``` diff --git a/graphql/env/__tests__/__snapshots__/merge.test.ts.snap b/graphql/env/__tests__/__snapshots__/merge.test.ts.snap index 6383de204..ace18ac67 100644 --- a/graphql/env/__tests__/__snapshots__/merge.test.ts.snap +++ b/graphql/env/__tests__/__snapshots__/merge.test.ts.snap @@ -80,6 +80,10 @@ exports[`getEnvOptions merges pgpm defaults, graphql defaults, config, env, and "useTx": false, }, }, + "oauth": { + "enabled": false, + "providerRequestTimeoutMs": 10000, + }, "pg": { "database": "config-db", "host": "override-host", diff --git a/graphql/env/__tests__/merge.test.ts b/graphql/env/__tests__/merge.test.ts index fa7dd645e..f16d841a2 100644 --- a/graphql/env/__tests__/merge.test.ts +++ b/graphql/env/__tests__/merge.test.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import { getGraphQLEnvVars } from '../src/env'; import { getEnvOptions } from '../src/merge'; +import { OAUTH_PROVIDER_REQUEST_TIMEOUT_MAX_MS } from '../src/oauth'; const writeConfig = (dir: string, config: Record): void => { fs.writeFileSync(path.join(dir, 'pgpm.json'), JSON.stringify(config, null, 2)); @@ -230,6 +231,108 @@ describe('getEnvOptions', () => { expect(result.sms).toBeUndefined(); }); + it('defaults OAuth off with a ten-second Provider timeout', () => { + expect(getEnvOptions({}, process.cwd(), {}).oauth).toEqual({ + enabled: false, + providerRequestTimeoutMs: 10_000 + }); + }); + + it('keeps absent OAuth environment variables out of partial overrides', () => { + expect(getGraphQLEnvVars({})).not.toHaveProperty('oauth'); + }); + + it('parses explicit OAuth environment overrides', () => { + expect( + getGraphQLEnvVars({ + OAUTH_ENABLED: 'true', + OAUTH_PROVIDER_REQUEST_TIMEOUT_MS: '2500' + }).oauth + ).toEqual({ + enabled: true, + providerRequestTimeoutMs: 2500 + }); + }); + + it('preserves config OAuth enablement when environment overrides are absent', () => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'graphql-env-oauth-')); + writeConfig(tempDir, { + oauth: { + enabled: true, + providerRequestTimeoutMs: 8000 + } + }); + + expect(getEnvOptions({}, tempDir, {}).oauth).toEqual({ + enabled: true, + providerRequestTimeoutMs: 8000 + }); + }); + + it('honors config, env, and runtime priority for OAuth', () => { + tempDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'graphql-env-oauth-priority-') + ); + writeConfig(tempDir, { + oauth: { + enabled: false, + providerRequestTimeoutMs: 5000 + } + }); + + const result = getEnvOptions( + { oauth: { providerRequestTimeoutMs: 9000 } }, + tempDir, + { OAUTH_ENABLED: 'true', OAUTH_PROVIDER_REQUEST_TIMEOUT_MS: '7000' } + ); + + expect(result.oauth).toEqual({ + enabled: true, + providerRequestTimeoutMs: 9000 + }); + }); + + it.each(['not-a-boolean', '', 'enabled'])( + 'rejects an explicitly malformed OAuth enabled value %p', + value => { + expect(() => getGraphQLEnvVars({ OAUTH_ENABLED: value })).toThrow( + /OAUTH_ENABLED/ + ); + } + ); + + it.each([ + 'not-a-number', + '0', + '-1', + '1.5', + String(OAUTH_PROVIDER_REQUEST_TIMEOUT_MAX_MS + 1) + ])('rejects an invalid OAuth Provider timeout %p', value => { + expect(() => + getEnvOptions({}, process.cwd(), { + OAUTH_PROVIDER_REQUEST_TIMEOUT_MS: value + }) + ).toThrow(/providerRequestTimeoutMs|OAUTH_PROVIDER_REQUEST_TIMEOUT_MS/); + }); + + it('rejects invalid OAuth config and runtime override types after merging', () => { + tempDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'graphql-env-oauth-invalid-') + ); + writeConfig(tempDir, { oauth: { enabled: 'yes' } }); + + expect(() => getEnvOptions({}, tempDir, {})).toThrow( + /oauth.enabled must be a boolean/ + ); + expect(() => + getEnvOptions( + { oauth: { providerRequestTimeoutMs: 60_001 } }, + process.cwd(), + {} + ) + ).toThrow(/providerRequestTimeoutMs/); + }); + it('omits an invalid SMS timeout from partial env overrides', () => { const result = getGraphQLEnvVars({ SMS_REQUEST_TIMEOUT_MS: '5s' diff --git a/graphql/env/src/env.ts b/graphql/env/src/env.ts index 014924ef2..358d32c60 100644 --- a/graphql/env/src/env.ts +++ b/graphql/env/src/env.ts @@ -1,6 +1,8 @@ import { ConstructiveOptions } from '@constructive-io/graphql-types'; import { parseEnvBoolean, parseEnvNumber } from '12factor-env'; +import { getOAuthEnvVars } from './oauth'; + /** * @param env - Environment object to read from (defaults to process.env for backwards compatibility) */ @@ -38,6 +40,7 @@ export const getGraphQLEnvVars = (env: NodeJS.ProcessEnv = process.env): Partial // let an absent env var overwrite pgpm.json or consumer-specific values. const smsRequestTimeoutMs = parseEnvNumber(SMS_REQUEST_TIMEOUT_MS); const smsDryRun = parseEnvBoolean(SEND_SMS_DRY_RUN); + const oauth = getOAuthEnvVars(env); const hasSmsEnvOverrides = Boolean( SMS_PROVIDER || SMS_SENDER_ID || @@ -67,6 +70,7 @@ export const getGraphQLEnvVars = (env: NodeJS.ProcessEnv = process.env): Partial ...(API_ANON_ROLE && { anonRole: API_ANON_ROLE }), ...(API_ROLE_NAME && { roleName: API_ROLE_NAME }) }, + ...(oauth && { oauth }), ...((EMBEDDER_PROVIDER || CHAT_PROVIDER) && { llm: { ...((EMBEDDER_PROVIDER || EMBEDDER_MODEL || EMBEDDER_BASE_URL) && { diff --git a/graphql/env/src/index.ts b/graphql/env/src/index.ts index 50627b7d5..6fb27c2f2 100644 --- a/graphql/env/src/index.ts +++ b/graphql/env/src/index.ts @@ -1,4 +1,9 @@ // Export Constructive-specific env functions export { getGraphQLEnvVars } from './env'; export { getConstructiveEnvOptions,getEnvOptions } from './merge'; +export { + getOAuthEnvVars, + OAUTH_PROVIDER_REQUEST_TIMEOUT_MAX_MS, + validateOAuthServerOptions +} from './oauth'; export type { DevSmsOptions, SmsOptions } from '@constructive-io/graphql-types'; diff --git a/graphql/env/src/merge.ts b/graphql/env/src/merge.ts index 15f1402c5..a371cfc56 100644 --- a/graphql/env/src/merge.ts +++ b/graphql/env/src/merge.ts @@ -3,6 +3,7 @@ import { getEnvOptions as getPgpmEnvOptions, loadConfigSync, replaceArrays } fro import deepmerge from 'deepmerge'; import { getGraphQLEnvVars } from './env'; +import { validateOAuthServerOptions } from './oauth'; /** * Get Constructive environment options by merging: @@ -36,7 +37,7 @@ export const getEnvOptions = ( const configOptions = loadConfigSync(cwd) as Partial; // Merge in order: core -> graphql defaults -> config (for graphql keys) -> graphql env -> overrides - return deepmerge.all([ + const merged = deepmerge.all([ coreOptions, constructiveGraphqlDefaults, // Only merge graphql-related keys from config (if present) @@ -44,6 +45,7 @@ export const getEnvOptions = ( ...(configOptions.graphile && { graphile: configOptions.graphile }), ...(configOptions.features && { features: configOptions.features }), ...(configOptions.api && { api: configOptions.api }), + ...(configOptions.oauth && { oauth: configOptions.oauth }), ...(configOptions.sms && { sms: configOptions.sms }), }, graphqlEnvOptions, @@ -51,6 +53,11 @@ export const getEnvOptions = ( ], { arrayMerge: replaceArrays }) as ConstructiveOptions; + + return { + ...merged, + oauth: validateOAuthServerOptions(merged.oauth) + }; }; /** diff --git a/graphql/env/src/oauth.ts b/graphql/env/src/oauth.ts new file mode 100644 index 000000000..4262e2f57 --- /dev/null +++ b/graphql/env/src/oauth.ts @@ -0,0 +1,74 @@ +import { + oauthServerDefaults, + type OAuthServerOptions +} from '@constructive-io/graphql-types'; +import { bool, env as validateEnv, EnvError, num } from '12factor-env'; + +export const OAUTH_PROVIDER_REQUEST_TIMEOUT_MAX_MS = 60_000; + +const assertProviderRequestTimeout = (value: unknown): number => { + if ( + typeof value !== 'number' || + !Number.isInteger(value) || + value <= 0 || + value > OAUTH_PROVIDER_REQUEST_TIMEOUT_MAX_MS + ) { + throw new EnvError( + `oauth.providerRequestTimeoutMs must be an integer between 1 and ${OAUTH_PROVIDER_REQUEST_TIMEOUT_MAX_MS}` + ); + } + return value; +}; + +/** Parse only explicitly supplied OAuth environment overrides. */ +export const getOAuthEnvVars = ( + input: NodeJS.ProcessEnv +): OAuthServerOptions | undefined => { + const overrides: OAuthServerOptions = {}; + let configured = false; + + if (input.OAUTH_ENABLED !== undefined) { + const parsed = validateEnv( + { OAUTH_ENABLED: input.OAUTH_ENABLED }, + {}, + { OAUTH_ENABLED: bool() } + ); + overrides.enabled = parsed.OAUTH_ENABLED; + configured = true; + } + + if (input.OAUTH_PROVIDER_REQUEST_TIMEOUT_MS !== undefined) { + const parsed = validateEnv( + { + OAUTH_PROVIDER_REQUEST_TIMEOUT_MS: + input.OAUTH_PROVIDER_REQUEST_TIMEOUT_MS + }, + {}, + { OAUTH_PROVIDER_REQUEST_TIMEOUT_MS: num() } + ); + overrides.providerRequestTimeoutMs = assertProviderRequestTimeout( + parsed.OAUTH_PROVIDER_REQUEST_TIMEOUT_MS + ); + configured = true; + } + + return configured ? overrides : undefined; +}; + +/** Validate and complete the effective OAuth options after all merge layers. */ +export const validateOAuthServerOptions = ( + input: OAuthServerOptions | undefined +): Required => { + const enabled = input?.enabled ?? oauthServerDefaults.enabled; + if (typeof enabled !== 'boolean') { + throw new EnvError('oauth.enabled must be a boolean'); + } + + return { + enabled, + providerRequestTimeoutMs: assertProviderRequestTimeout( + input?.providerRequestTimeoutMs ?? + oauthServerDefaults.providerRequestTimeoutMs + ) + }; +}; diff --git a/graphql/server/src/middleware/error-handler.ts b/graphql/server/src/middleware/error-handler.ts index bbf63de19..1111e1847 100644 --- a/graphql/server/src/middleware/error-handler.ts +++ b/graphql/server/src/middleware/error-handler.ts @@ -1,5 +1,6 @@ import './types'; +import { ConstructiveError } from '@constructive-io/errors'; import { getNodeEnv } from '@pgpmjs/env'; import { Logger } from '@pgpmjs/logger'; import type { ErrorRequestHandler, NextFunction, Request, Response } from 'express'; @@ -39,6 +40,14 @@ const isCsrfError = (err: Error): boolean => { }; const categorizeError = (err: Error): ErrorResponse => { + if (err instanceof ConstructiveError) { + return { + statusCode: err.http, + code: err.code, + message: err.isPublic ? err.message : 'An unexpected error occurred', + logLevel: err.http >= 500 ? 'error' : 'warn' + }; + } if (isApiError(err)) { return { statusCode: err.statusCode, @@ -79,7 +88,15 @@ const logError = (err: Error, req: Request, level: 'warn' | 'error'): void => { clientIp: req.clientIp, }; - if (isApiError(err)) { + if (err instanceof ConstructiveError) { + log[level]({ + event: 'constructive_error', + code: err.code, + statusCode: err.http, + message: err.message, + ...context + }); + } else if (isApiError(err)) { log[level]({ event: 'api_error', code: err.code, statusCode: err.statusCode, message: err.message, ...context }); } else { log[level]({ event: 'unexpected_error', name: err.name, message: err.message, stack: isDevelopment() ? err.stack : undefined, ...context }); diff --git a/graphql/types/README.md b/graphql/types/README.md index e071cedb4..5160fec84 100644 --- a/graphql/types/README.md +++ b/graphql/types/README.md @@ -47,6 +47,10 @@ const config: ConstructiveOptions = { simpleInflection: true, postgis: true, }, + oauth: { + enabled: false, + providerRequestTimeoutMs: 10_000, + }, }; ``` @@ -68,6 +72,12 @@ Configuration for the Constructive API including meta API settings, exposed sche Feature flags for GraphQL/Graphile including inflection settings and PostGIS support. +### OAuthServerOptions + +GraphQL-server-owned OAuth enablement and bounded Provider request timeout. +Provider credentials and endpoint configuration remain Tenant data and are not +part of this type. + ## Re-exports This package re-exports all types from `@pgpmjs/types` for convenience, so you can import both core PGPM types and GraphQL types from a single package. diff --git a/graphql/types/src/constructive.ts b/graphql/types/src/constructive.ts index 485a4f4a5..cb88edbdf 100644 --- a/graphql/types/src/constructive.ts +++ b/graphql/types/src/constructive.ts @@ -17,6 +17,7 @@ import { GraphileFeatureOptions, GraphileOptions} from './graphile'; import { LlmOptions } from './llm'; +import { oauthServerDefaults, type OAuthServerOptions } from './oauth'; import { SmsOptions } from './sms'; /** @@ -29,6 +30,8 @@ export interface ConstructiveGraphQLOptions { features?: GraphileFeatureOptions; /** API configuration options */ api?: ApiOptions; + /** GraphQL server OAuth feature and transport options */ + oauth?: OAuthServerOptions; } /** @@ -58,6 +61,8 @@ export interface ConstructiveOptions extends PgpmOptions, ConstructiveGraphQLOpt llm?: LlmOptions; /** SMS provider configuration */ sms?: SmsOptions; + /** GraphQL server OAuth feature and transport options */ + oauth?: OAuthServerOptions; } /** @@ -66,7 +71,8 @@ export interface ConstructiveOptions extends PgpmOptions, ConstructiveGraphQLOpt export const constructiveGraphqlDefaults: ConstructiveGraphQLOptions = { graphile: graphileDefaults, features: graphileFeatureDefaults, - api: apiDefaults + api: apiDefaults, + oauth: oauthServerDefaults }; /** diff --git a/graphql/types/src/index.ts b/graphql/types/src/index.ts index 895604e13..dd8eaa6fb 100644 --- a/graphql/types/src/index.ts +++ b/graphql/types/src/index.ts @@ -27,6 +27,11 @@ export { LlmEmbedderOptions, LlmOptions} from './llm'; +// Export GraphQL-server OAuth options +export { + oauthServerDefaults, + type OAuthServerOptions} from './oauth'; + // Export SMS types export { DevSmsOptions, diff --git a/graphql/types/src/oauth.ts b/graphql/types/src/oauth.ts new file mode 100644 index 000000000..2b52a2003 --- /dev/null +++ b/graphql/types/src/oauth.ts @@ -0,0 +1,17 @@ +/** + * GraphQL-server-owned OAuth runtime options. + * + * Provider endpoints, credentials, scopes, and policy remain Tenant data and + * are deliberately absent from this process-level configuration surface. + */ +export interface OAuthServerOptions { + /** Explicitly enables the unified-auth Provider flow. */ + enabled?: boolean; + /** Maximum duration of one outbound Provider HTTP request. */ + providerRequestTimeoutMs?: number; +} + +export const oauthServerDefaults: Required = { + enabled: false, + providerRequestTimeoutMs: 10_000 +}; diff --git a/packages/errors/README.md b/packages/errors/README.md index 4668324e5..5ad2b283c 100644 --- a/packages/errors/README.md +++ b/packages/errors/README.md @@ -30,6 +30,9 @@ if (parsed.class === 'public') { // Throw a structured error throw errors.ACCOUNT_EXISTS(); + +// Preserve an internal cause without exposing it in transport extensions +throw errors.INVALID_OAUTH_STATE(undefined, undefined, { cause: caught }); ``` ## Design notes diff --git a/packages/errors/__tests__/parse.test.ts b/packages/errors/__tests__/parse.test.ts index da301fd44..f1fbf0dc8 100644 --- a/packages/errors/__tests__/parse.test.ts +++ b/packages/errors/__tests__/parse.test.ts @@ -126,10 +126,12 @@ describe('toError', () => { }); it('falls back to UNKNOWN_ERROR and the raw message for unresolved errors', () => { - const err = toError(new Error('totally opaque failure')); + const original = new Error('totally opaque failure'); + const err = toError(original); expect(err.code).toBe('UNKNOWN_ERROR'); expect(err.errorClass).toBe('internal'); expect(err.message).toBe('totally opaque failure'); + expect(err.cause).toBe(original); }); it('returns a ConstructiveError unchanged', () => { diff --git a/packages/errors/__tests__/sso.test.ts b/packages/errors/__tests__/sso.test.ts new file mode 100644 index 000000000..36eea0d62 --- /dev/null +++ b/packages/errors/__tests__/sso.test.ts @@ -0,0 +1,39 @@ +import { ConstructiveError, errors, getDefinition } from '../src'; + +const PUBLIC_SSO_CODES = [ + 'INVALID_SSO_SITE_STATE', + 'INVALID_SSO_CALLBACK', + 'INVALID_SSO_RETURN_TARGET', + 'SSO_LOGIN_TRANSACTION_EXPIRED', + 'SSO_LOGIN_TRANSACTION_ALREADY_USED', + 'OAUTH_SIGN_IN_DISABLED', + 'INVALID_OAUTH_STATE', + 'INVALID_OAUTH_PKCE', + 'IDENTITY_PROVIDER_NOT_CONFIGURED', + 'IDENTITY_PROVIDER_UNSUPPORTED', + 'SSO_ACCOUNT_CONFLICT', + 'INVALID_SSO_HANDOFF', + 'SSO_HANDOFF_EXPIRED', + 'SSO_HANDOFF_ALREADY_USED' +] as const; + +describe('OAuth/SSO error contract', () => { + it.each(PUBLIC_SSO_CODES)('registers %s as a stable public error', code => { + const definition = getDefinition(code); + expect(definition).toMatchObject({ code, class: 'public' }); + expect(definition?.message).not.toEqual(code); + }); + + it('preserves a cause without exposing it in transport extensions', () => { + const cause = new Error('provider response contained a secret'); + const error = errors.INVALID_OAUTH_STATE(undefined, undefined, { cause }); + + expect(error).toBeInstanceOf(ConstructiveError); + expect(error.cause).toBe(cause); + expect(error.toExtensions()).toEqual({ + code: 'INVALID_OAUTH_STATE', + class: 'public', + http: 400 + }); + }); +}); diff --git a/packages/errors/src/error.ts b/packages/errors/src/error.ts index 98f9269f1..a91f27621 100644 --- a/packages/errors/src/error.ts +++ b/packages/errors/src/error.ts @@ -1,6 +1,6 @@ import type { ErrorClass, ErrorContext } from './types'; -export interface ConstructiveErrorArgs { +export interface ConstructiveErrorArgs extends ErrorOptions { code: string; message: string; errorClass: ErrorClass; @@ -22,7 +22,10 @@ export class ConstructiveError extends Error { readonly context?: ErrorContext; constructor(args: ConstructiveErrorArgs) { - super(args.message); + super( + args.message, + args.cause === undefined ? undefined : { cause: args.cause } + ); this.name = 'ConstructiveError'; this.code = args.code; this.errorClass = args.errorClass; diff --git a/packages/errors/src/factory.ts b/packages/errors/src/factory.ts index 8d34d01c9..48b535a3c 100644 --- a/packages/errors/src/factory.ts +++ b/packages/errors/src/factory.ts @@ -10,8 +10,16 @@ import type { ErrorClass, ErrorContext, ErrorDefinition } from './types'; * The `[keyof C]` tuple wrapper prevents `never` from distributing. */ export type ErrorFactory = [keyof C] extends [never] - ? (context?: Record, overrideMessage?: string) => ConstructiveError - : (context: C, overrideMessage?: string) => ConstructiveError; + ? ( + context?: Record, + overrideMessage?: string, + options?: ErrorOptions + ) => ConstructiveError + : ( + context: C, + overrideMessage?: string, + options?: ErrorOptions + ) => ConstructiveError; export type ErrorsApi = { [K in keyof R]: R[K] extends { __context: (context: infer C) => void } @@ -25,13 +33,18 @@ export type ErrorsApi = { export function makeErrorFromDefinition( def: ErrorDefinition ): ErrorFactory { - const factory = (context?: ErrorContext, overrideMessage?: string): ConstructiveError => + const factory = ( + context?: ErrorContext, + overrideMessage?: string, + options?: ErrorOptions + ): ConstructiveError => new ConstructiveError({ code: def.code, message: overrideMessage ?? format(def.code, context ?? {}), errorClass: def.class, http: def.http, - context + context, + cause: options?.cause }); return factory as ErrorFactory; } @@ -59,14 +72,19 @@ export function makeError( messageFn: (context: C) => string, httpCode = 500, errorClass: ErrorClass = 'internal' -): (context: C, overrideMessage?: string) => ConstructiveError { - return (context: C, overrideMessage?: string) => +): ( + context: C, + overrideMessage?: string, + options?: ErrorOptions +) => ConstructiveError { + return (context: C, overrideMessage?: string, options?: ErrorOptions) => new ConstructiveError({ code, message: overrideMessage ?? messageFn(context), errorClass, http: httpCode, - context + context, + cause: options?.cause }); } diff --git a/packages/errors/src/parse.ts b/packages/errors/src/parse.ts index 807076af1..df1b95397 100644 --- a/packages/errors/src/parse.ts +++ b/packages/errors/src/parse.ts @@ -207,6 +207,7 @@ export function toError(error: unknown, locale?: string): ConstructiveError { message, errorClass: parsed.class, http: def ? def.http : httpStatusFor(code).status, - context: parsed.context + context: parsed.context, + cause: parsed.originalError }); } diff --git a/packages/errors/src/registry.ts b/packages/errors/src/registry.ts index 7c5cf70fc..6886dbd34 100644 --- a/packages/errors/src/registry.ts +++ b/packages/errors/src/registry.ts @@ -148,6 +148,90 @@ export const registry = { http: 404, message: 'No single sign-on account was found.' }), + OAUTH_SIGN_IN_DISABLED: defineError({ + code: 'OAUTH_SIGN_IN_DISABLED', + class: 'public', + http: 403, + message: 'OAuth sign in is not enabled.' + }), + INVALID_SSO_SITE_STATE: defineError({ + code: 'INVALID_SSO_SITE_STATE', + class: 'public', + http: 400, + message: 'The sign-in request is invalid or has expired. Please restart sign in.' + }), + INVALID_SSO_CALLBACK: defineError({ + code: 'INVALID_SSO_CALLBACK', + class: 'public', + http: 400, + message: 'The requested sign-in callback is not registered for this Site.' + }), + INVALID_SSO_RETURN_TARGET: defineError({ + code: 'INVALID_SSO_RETURN_TARGET', + class: 'public', + http: 400, + message: 'The requested return location is invalid.' + }), + SSO_LOGIN_TRANSACTION_EXPIRED: defineError({ + code: 'SSO_LOGIN_TRANSACTION_EXPIRED', + class: 'public', + http: 410, + message: 'The sign-in request has expired. Please restart sign in.' + }), + SSO_LOGIN_TRANSACTION_ALREADY_USED: defineError({ + code: 'SSO_LOGIN_TRANSACTION_ALREADY_USED', + class: 'public', + http: 409, + message: 'The sign-in request has already been completed. Please restart sign in.' + }), + INVALID_OAUTH_STATE: defineError({ + code: 'INVALID_OAUTH_STATE', + class: 'public', + http: 400, + message: 'The external sign-in state is invalid or has expired. Please restart sign in.' + }), + INVALID_OAUTH_PKCE: defineError({ + code: 'INVALID_OAUTH_PKCE', + class: 'public', + http: 400, + message: 'The external sign-in verification failed. Please restart sign in.' + }), + IDENTITY_PROVIDER_NOT_CONFIGURED: defineError({ + code: 'IDENTITY_PROVIDER_NOT_CONFIGURED', + class: 'public', + http: 400, + message: 'This identity provider is not configured.' + }), + IDENTITY_PROVIDER_UNSUPPORTED: defineError({ + code: 'IDENTITY_PROVIDER_UNSUPPORTED', + class: 'public', + http: 400, + message: 'This identity provider is not supported.' + }), + SSO_ACCOUNT_CONFLICT: defineError({ + code: 'SSO_ACCOUNT_CONFLICT', + class: 'public', + http: 409, + message: 'An account already uses this email. Sign in with its existing method.' + }), + INVALID_SSO_HANDOFF: defineError({ + code: 'INVALID_SSO_HANDOFF', + class: 'public', + http: 400, + message: 'The Site sign-in handoff is invalid.' + }), + SSO_HANDOFF_EXPIRED: defineError({ + code: 'SSO_HANDOFF_EXPIRED', + class: 'public', + http: 410, + message: 'The Site sign-in handoff has expired. Please restart sign in.' + }), + SSO_HANDOFF_ALREADY_USED: defineError({ + code: 'SSO_HANDOFF_ALREADY_USED', + class: 'public', + http: 409, + message: 'The Site sign-in handoff has already been used.' + }), MAGIC_LINK_SIGN_IN_DISABLED: defineError({ code: 'MAGIC_LINK_SIGN_IN_DISABLED', class: 'public',