Skip to content

Commit d10f74e

Browse files
committed
feat(api): add config verification endpoint
1 parent f592e33 commit d10f74e

File tree

9 files changed

+771
-131
lines changed

9 files changed

+771
-131
lines changed

API/src/routes/root/config-docs.ts

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
export const configExample = {
2+
domain: 'client.example.com',
3+
redirect_urls: ['https://client.example.com/oauth/callback'],
4+
enabled_auth_methods: ['email_password', 'google'],
5+
ui_theme: {
6+
colors: {
7+
bg: '#f8fafc',
8+
surface: '#ffffff',
9+
text: '#0f172a',
10+
muted: '#475569',
11+
primary: '#2563eb',
12+
primary_text: '#ffffff',
13+
border: '#e2e8f0',
14+
danger: '#dc2626',
15+
danger_text: '#ffffff',
16+
},
17+
radii: {
18+
card: '16px',
19+
button: '12px',
20+
input: '12px',
21+
},
22+
density: 'comfortable',
23+
typography: {
24+
font_family: 'sans',
25+
base_text_size: 'md',
26+
},
27+
button: {
28+
style: 'solid',
29+
},
30+
card: {
31+
style: 'bordered',
32+
},
33+
logo: {
34+
url: '',
35+
alt: 'Client logo',
36+
text: 'Client',
37+
font_size: '24px',
38+
color: '#0f172a',
39+
style: {
40+
'font-weight': '800',
41+
'letter-spacing': '-0.02em',
42+
},
43+
},
44+
},
45+
language_config: 'en',
46+
debug_enabled: true,
47+
allow_registration: true,
48+
registration_mode: 'password_required',
49+
user_scope: 'global',
50+
};
51+
52+
export const configJwtDocumentation = {
53+
description:
54+
'The config JWT is a signed JWT containing all client-specific settings. The payload is the config. The signature must be created with the shared secret and the JWT aud must match AUTH_SERVICE_IDENTIFIER.',
55+
signing: {
56+
algorithms: ['HS256', 'HS384', 'HS512'],
57+
audience:
58+
'The JWT aud claim must match the auth service identifier configured in AUTH_SERVICE_IDENTIFIER.',
59+
},
60+
required_fields: {
61+
domain:
62+
'string — client domain. This must exactly match the hostname of config_url when the auth service fetches the JWT.',
63+
redirect_urls:
64+
'string[] — non-empty list of absolute HTTP/HTTPS callback URLs. redirect_url matching is exact.',
65+
enabled_auth_methods:
66+
'string[] — non-empty list. Supported values are email_password, google, facebook, github, linkedin, apple.',
67+
language_config:
68+
'string | string[] — either one language code or a non-empty array of language codes.',
69+
ui_theme: {
70+
description:
71+
'object — every auth page style comes from ui_theme. This object is required and the main source of integration mistakes.',
72+
required_sections: {
73+
colors: {
74+
required_keys: [
75+
'bg',
76+
'surface',
77+
'text',
78+
'muted',
79+
'primary',
80+
'primary_text',
81+
'border',
82+
'danger',
83+
'danger_text',
84+
],
85+
value_format:
86+
'hex color only (#RGB, #RGBA, #RRGGBB, #RRGGBBAA) or transparent',
87+
},
88+
radii: {
89+
required_keys: ['card', 'button', 'input'],
90+
value_format: 'CSS length string: px, rem, em, %, or 0',
91+
},
92+
density: 'compact | comfortable | spacious',
93+
typography: {
94+
required_keys: ['font_family', 'base_text_size'],
95+
font_family:
96+
'Preset values like sans, serif, mono are valid. Custom CSS font-family names are also valid.',
97+
base_text_size: 'sm | md | lg',
98+
font_import_url:
99+
'optional HTTP/HTTPS stylesheet URL. Use this when font_family depends on a remote font import.',
100+
},
101+
button: {
102+
required_keys: ['style'],
103+
style: 'solid | outline | ghost',
104+
},
105+
card: {
106+
required_keys: ['style'],
107+
style: 'plain | bordered | shadow',
108+
},
109+
logo: {
110+
required_keys: ['url', 'alt'],
111+
url: 'HTTP/HTTPS URL or empty string',
112+
alt: 'required non-empty string',
113+
text: 'optional text logo, max 100 chars, used when url is empty',
114+
font_size: 'optional CSS length string',
115+
color: 'optional hex color',
116+
style:
117+
'optional flat object of CSS property -> safe CSS value. Do not include semicolons, braces, url(), or expression().',
118+
},
119+
},
120+
optional_sections: {
121+
css_vars:
122+
'Record<string, string> — optional advanced CSS variable overrides.',
123+
},
124+
common_failures: [
125+
'Missing one of the required ui_theme sections such as colors, radii, button, card, typography, or logo.',
126+
'Using non-hex colors like rgb(...), hsl(...), or named colors.',
127+
'Using bare numbers for radii/font sizes instead of CSS length strings like 12px or 1rem.',
128+
'Providing a text logo without logo.alt.',
129+
'Forgetting button.style or card.style.',
130+
],
131+
example: configExample.ui_theme,
132+
},
133+
},
134+
optional_fields: {
135+
'2fa_enabled': 'boolean (default false)',
136+
debug_enabled: 'boolean (default false)',
137+
allowed_social_providers: 'string[] — subset of enabled social providers',
138+
user_scope: '"global" | "per_domain" (default "global")',
139+
allow_registration: 'boolean (default true)',
140+
registration_mode:
141+
'"password_required" | "passwordless" (default "password_required")',
142+
allowed_registration_domains:
143+
'string[] — lowercase email domains allowed to register',
144+
registration_domain_mapping:
145+
'array of { email_domain, org_id, team_id? } — email-domain-based org/team placement',
146+
language: 'string — currently selected language override',
147+
session: {
148+
remember_me_enabled: 'boolean (default true)',
149+
remember_me_default: 'boolean (default true)',
150+
short_refresh_token_ttl_hours: 'number (1-168, default 1)',
151+
long_refresh_token_ttl_days: 'number (1-90, default 30)',
152+
access_token_ttl_minutes: 'number (15-60)',
153+
},
154+
org_features: {
155+
enabled: 'boolean (default false)',
156+
groups_enabled: 'boolean (default false)',
157+
user_needs_team: 'boolean (default false)',
158+
max_teams_per_org: 'number (default 100, max 1000)',
159+
max_groups_per_org: 'number (default 20, max 200)',
160+
max_members_per_org: 'number (default 1000, max 10000)',
161+
max_members_per_team: 'number (default 200, max 5000)',
162+
max_members_per_group: 'number (default 500, max 5000)',
163+
max_team_memberships_per_user: 'number (default 50, max 200)',
164+
org_roles:
165+
'string[] (default ["owner", "admin", "member"]). Must include "owner".',
166+
},
167+
access_requests: {
168+
enabled: 'boolean (default false)',
169+
target_org_id: 'string (required when enabled=true)',
170+
target_team_id: 'string (required when enabled=true)',
171+
auto_grant_domains: 'string[]',
172+
notify_org_roles: 'string[] (default ["owner", "admin"])',
173+
admin_review_url: 'absolute URL',
174+
},
175+
},
176+
example_payload: configExample,
177+
};
178+
179+
export const configVerificationEndpointDocumentation = {
180+
path: '/config/verify',
181+
method: 'POST',
182+
description:
183+
'Debug endpoint that validates raw config JSON, a signed config JWT, or a config_url fetch target. It reports schema problems separately from signature, audience, and config_url/domain issues.',
184+
body: {
185+
config:
186+
'object (optional) — raw config payload to schema-validate directly. This skips JWT signature checking unless config_jwt or config_url is also supplied instead.',
187+
config_jwt:
188+
'string (optional) — signed config JWT to decode, inspect, schema-validate, and optionally verify with shared_secret.',
189+
config_url:
190+
'string (optional) — URL that should return the signed config JWT. The endpoint fetches it and then runs the same checks.',
191+
shared_secret:
192+
'string (optional) — candidate secret used to verify config_jwt or the JWT fetched from config_url. If it is wrong, the response explicitly reports the shared secret/signature check failure.',
193+
auth_service_identifier:
194+
'string (optional) — expected JWT aud. Defaults to this auth service environment when omitted.',
195+
},
196+
source_priority: ['config', 'config_jwt', 'config_url'],
197+
response: {
198+
ok: 'boolean — true when every executed check passed',
199+
schema_valid: 'boolean',
200+
jwt_signature_valid:
201+
'boolean | null — null when signature checking was skipped',
202+
audience_valid: 'boolean | null — null when no audience check was possible',
203+
domain_match: 'boolean | null — null when config_url was not part of the request or schema parsing failed',
204+
checks:
205+
'object — per-stage results for source, fetch, decode, signature, audience, schema, and domain_match',
206+
issues: 'array — structured stage-specific failures and warnings',
207+
config_summary:
208+
'object | null — safe summary of the parsed config when schema validation succeeds',
209+
},
210+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { FastifyInstance } from 'fastify';
2+
3+
import {
4+
parseVerifyConfigRequest,
5+
verifyClientConfig,
6+
} from '../../services/config-debug.service.js';
7+
8+
export function registerConfigVerifyRoute(app: FastifyInstance): void {
9+
app.post('/config/verify', async (request) => {
10+
const body = parseVerifyConfigRequest(request.body);
11+
return await verifyClientConfig(body);
12+
});
13+
}

API/src/routes/root/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { fileURLToPath } from 'node:url';
44

55
import type { FastifyInstance } from 'fastify';
66

7+
import { configJwtDocumentation, configVerificationEndpointDocumentation } from './config-docs.js';
8+
import { registerConfigVerifyRoute } from './config-verify.js';
79
import { registerLlmRoute } from './llm.js';
810
import { endpoints } from './schema.js';
911

@@ -21,6 +23,7 @@ try {
2123

2224
export function registerRootRoute(app: FastifyInstance): void {
2325
registerLlmRoute(app);
26+
registerConfigVerifyRoute(app);
2427

2528
app.get('/', async () => {
2629
return {
@@ -30,6 +33,8 @@ export function registerRootRoute(app: FastifyInstance): void {
3033
version,
3134
repository: 'https://github.com/UnlikeOtherAI/UnlikeOtherAuthenticator',
3235
docs: '/llm',
36+
config_jwt: configJwtDocumentation,
37+
config_verification: configVerificationEndpointDocumentation,
3338
endpoints,
3439
};
3540
});

0 commit comments

Comments
 (0)