diff --git a/packages/api/drizzle/0017_settings_defaults.sql b/packages/api/drizzle/0017_settings_defaults.sql new file mode 100644 index 0000000..6bab493 --- /dev/null +++ b/packages/api/drizzle/0017_settings_defaults.sql @@ -0,0 +1,82 @@ +INSERT INTO "settings" ("key", "name", "type", "category", "description", "tags", "default_value", "value", "secure", "updated_at") VALUES +('issuer', 'Issuer', 'string', 'Core', 'Issuer URL used in OIDC discovery and tokens', ARRAY['core']::text[], '"http://localhost:9080"'::jsonb, '"http://localhost:9080"'::jsonb, false, now()), +('users.self_registration_enabled', 'Self Registration Enabled', 'boolean', 'Users', 'Allow new users to sign up without invitation', ARRAY['users']::text[], 'false'::jsonb, 'false'::jsonb, false, now()), +('users.require_email_verification', 'Require Email Verification', 'boolean', 'Users', 'Require users to verify their email before login completes', ARRAY['users', 'email']::text[], 'false'::jsonb, 'false'::jsonb, false, now()), +('public_origin', 'Public Origin', 'string', 'Core', 'Public base origin for redirects and links', ARRAY['core']::text[], '"http://localhost:9080"'::jsonb, '"http://localhost:9080"'::jsonb, false, now()), +('rp_id', 'RP ID', 'string', 'Core', 'WebAuthn relying party ID', ARRAY['core', 'webauthn']::text[], '"localhost"'::jsonb, '"localhost"'::jsonb, false, now()), +('zk_delivery.fragment_param', 'Fragment Param', 'string', 'ZK / Delivery', 'URL fragment parameter name for DRK JWE', ARRAY['zk']::text[], '"drk_jwe"'::jsonb, '"drk_jwe"'::jsonb, false, now()), +('zk_delivery.jwe_alg', 'JWE Alg', 'string', 'ZK / Delivery', 'Default JWE algorithm for DRK delivery', ARRAY['zk']::text[], '"ECDH-ES"'::jsonb, '"ECDH-ES"'::jsonb, false, now()), +('zk_delivery.jwe_enc', 'JWE Enc', 'string', 'ZK / Delivery', 'Default JWE encryption method for DRK delivery', ARRAY['zk']::text[], '"A256GCM"'::jsonb, '"A256GCM"'::jsonb, false, now()), +('zk_delivery.hash_alg', 'Hash Alg', 'string', 'ZK / Delivery', 'Hash algorithm used for fragment verification', ARRAY['zk']::text[], '"SHA-256"'::jsonb, '"SHA-256"'::jsonb, false, now()), +('opaque.kdf', 'OPAQUE KDF', 'string', 'Security / OPAQUE', 'OPAQUE KDF suite', ARRAY['opaque']::text[], '"ristretto255"'::jsonb, '"ristretto255"'::jsonb, false, now()), +('opaque.envelope_mode', 'Envelope Mode', 'string', 'Security / OPAQUE', 'OPAQUE envelope mode', ARRAY['opaque']::text[], '"base"'::jsonb, '"base"'::jsonb, false, now()), +('security_headers.enabled', 'Security Headers Enabled', 'boolean', 'Security / Headers', 'Enable security headers middleware', ARRAY['security']::text[], 'true'::jsonb, 'true'::jsonb, false, now()), +('security_headers.csp', 'CSP', 'string', 'Security / Headers', 'Content Security Policy applied to responses', ARRAY['security']::text[], '"default-src ''self''; script-src ''self''; style-src ''self''; img-src ''self'' data:; connect-src ''self'' https://darkauth.com https://release.darkauth.com; frame-ancestors ''self''; base-uri ''none''; form-action ''self''; object-src ''none''; require-trusted-types-for ''script''"'::jsonb, '"default-src ''self''; script-src ''self''; style-src ''self''; img-src ''self'' data:; connect-src ''self'' https://darkauth.com https://release.darkauth.com; frame-ancestors ''self''; base-uri ''none''; form-action ''self''; object-src ''none''; require-trusted-types-for ''script''"'::jsonb, false, now()), +('rate_limits.general.enabled', 'General Enabled', 'boolean', 'Security / Rate Limits / General', 'Enable general rate limiting', ARRAY['ratelimit']::text[], 'true'::jsonb, 'true'::jsonb, false, now()), +('rate_limits.general.window_minutes', 'General Window (min)', 'number', 'Security / Rate Limits / General', 'Window length in minutes', ARRAY['ratelimit']::text[], '1'::jsonb, '1'::jsonb, false, now()), +('rate_limits.general.max_requests', 'General Max Requests', 'number', 'Security / Rate Limits / General', 'Maximum requests per window', ARRAY['ratelimit']::text[], '1000'::jsonb, '1000'::jsonb, false, now()), +('rate_limits.auth.enabled', 'Auth Enabled', 'boolean', 'Security / Rate Limits / Auth', 'Enable rate limits for auth endpoints', ARRAY['ratelimit']::text[], 'true'::jsonb, 'true'::jsonb, false, now()), +('rate_limits.auth.window_minutes', 'Auth Window (min)', 'number', 'Security / Rate Limits / Auth', 'Window length in minutes', ARRAY['ratelimit']::text[], '1'::jsonb, '1'::jsonb, false, now()), +('rate_limits.auth.max_requests', 'Auth Max Requests', 'number', 'Security / Rate Limits / Auth', 'Maximum requests per window', ARRAY['ratelimit']::text[], '1000'::jsonb, '1000'::jsonb, false, now()), +('rate_limits.opaque.enabled', 'OPAQUE Enabled', 'boolean', 'Security / Rate Limits / OPAQUE', 'Enable rate limits for OPAQUE endpoints', ARRAY['ratelimit']::text[], 'true'::jsonb, 'true'::jsonb, false, now()), +('rate_limits.opaque.window_minutes', 'OPAQUE Window (min)', 'number', 'Security / Rate Limits / OPAQUE', NULL, ARRAY['ratelimit']::text[], '1'::jsonb, '1'::jsonb, false, now()), +('rate_limits.opaque.max_requests', 'OPAQUE Max Requests', 'number', 'Security / Rate Limits / OPAQUE', NULL, ARRAY['ratelimit']::text[], '1000'::jsonb, '1000'::jsonb, false, now()), +('rate_limits.token.enabled', 'Token Enabled', 'boolean', 'Security / Rate Limits / Token', NULL, ARRAY['ratelimit']::text[], 'true'::jsonb, 'true'::jsonb, false, now()), +('rate_limits.token.window_minutes', 'Token Window (min)', 'number', 'Security / Rate Limits / Token', NULL, ARRAY['ratelimit']::text[], '1'::jsonb, '1'::jsonb, false, now()), +('rate_limits.token.max_requests', 'Token Max Requests', 'number', 'Security / Rate Limits / Token', NULL, ARRAY['ratelimit']::text[], '1000'::jsonb, '1000'::jsonb, false, now()), +('rate_limits.otp.enabled', 'OTP Enabled', 'boolean', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], 'true'::jsonb, 'true'::jsonb, false, now()), +('rate_limits.otp.window_minutes', 'OTP Window (min)', 'number', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], '15'::jsonb, '15'::jsonb, false, now()), +('rate_limits.otp.max_requests', 'OTP Max Requests', 'number', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], '1000'::jsonb, '1000'::jsonb, false, now()), +('rate_limits.otp_setup.enabled', 'OTP Setup Enabled', 'boolean', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], 'true'::jsonb, 'true'::jsonb, false, now()), +('rate_limits.otp_setup.window_minutes', 'OTP Setup Window (min)', 'number', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], '60'::jsonb, '60'::jsonb, false, now()), +('rate_limits.otp_setup.max_requests', 'OTP Setup Max Requests', 'number', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], '1000'::jsonb, '1000'::jsonb, false, now()), +('rate_limits.otp_verify.enabled', 'OTP Verify Enabled', 'boolean', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], 'true'::jsonb, 'true'::jsonb, false, now()), +('rate_limits.otp_verify.window_minutes', 'OTP Verify Window (min)', 'number', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], '60'::jsonb, '60'::jsonb, false, now()), +('rate_limits.otp_verify.max_requests', 'OTP Verify Max Requests', 'number', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], '1000'::jsonb, '1000'::jsonb, false, now()), +('rate_limits.otp_disable.enabled', 'OTP Disable Enabled', 'boolean', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], 'true'::jsonb, 'true'::jsonb, false, now()), +('rate_limits.otp_disable.window_minutes', 'OTP Disable Window (min)', 'number', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], '60'::jsonb, '60'::jsonb, false, now()), +('rate_limits.otp_disable.max_requests', 'OTP Disable Max Requests', 'number', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], '1000'::jsonb, '1000'::jsonb, false, now()), +('rate_limits.otp_regenerate.enabled', 'OTP Regenerate Enabled', 'boolean', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], 'true'::jsonb, 'true'::jsonb, false, now()), +('rate_limits.otp_regenerate.window_minutes', 'OTP Regenerate Window (min)', 'number', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], '60'::jsonb, '60'::jsonb, false, now()), +('rate_limits.otp_regenerate.max_requests', 'OTP Regenerate Max Requests', 'number', 'Security / Rate Limits / OTP', NULL, ARRAY['ratelimit']::text[], '1000'::jsonb, '1000'::jsonb, false, now()), +('rate_limits.admin.enabled', 'Admin Enabled', 'boolean', 'Security / Rate Limits / Admin', NULL, ARRAY['ratelimit']::text[], 'true'::jsonb, 'true'::jsonb, false, now()), +('rate_limits.admin.window_minutes', 'Admin Window (min)', 'number', 'Security / Rate Limits / Admin', NULL, ARRAY['ratelimit']::text[], '1'::jsonb, '1'::jsonb, false, now()), +('rate_limits.admin.max_requests', 'Admin Max Requests', 'number', 'Security / Rate Limits / Admin', NULL, ARRAY['ratelimit']::text[], '1000'::jsonb, '1000'::jsonb, false, now()), +('rate_limits.install.enabled', 'Install Enabled', 'boolean', 'Security / Rate Limits / Install', NULL, ARRAY['ratelimit']::text[], 'true'::jsonb, 'true'::jsonb, false, now()), +('rate_limits.install.window_minutes', 'Install Window (min)', 'number', 'Security / Rate Limits / Install', NULL, ARRAY['ratelimit']::text[], '60'::jsonb, '60'::jsonb, false, now()), +('rate_limits.install.max_requests', 'Install Max Requests', 'number', 'Security / Rate Limits / Install', NULL, ARRAY['ratelimit']::text[], '1000'::jsonb, '1000'::jsonb, false, now()), +('user_keys.enc_public_visible_to_authenticated_users', 'Enc Public Visible To Authenticated Users', 'boolean', 'Security / User Keys', 'Expose users'' encryption public keys to authenticated users', ARRAY['keys']::text[], 'true'::jsonb, 'true'::jsonb, false, now()), +('admin_session.lifetime_seconds', 'Admin Session TTL (s)', 'number', 'Admin / Session', 'Admin session lifetime in seconds', ARRAY['session']::text[], '900'::jsonb, '900'::jsonb, false, now()), +('otp', 'OTP Settings', 'json', 'Security / OTP', 'TOTP configuration and policy', ARRAY['otp', 'security']::text[], '{"enabled":true,"issuer":"DarkAuth","algorithm":"SHA1","digits":6,"period":30,"window":1,"backup_codes_count":8,"max_failures":5,"lockout_duration_minutes":15,"require_for_admin":true,"require_for_users":false}'::jsonb, '{"enabled":true,"issuer":"DarkAuth","algorithm":"SHA1","digits":6,"period":30,"window":1,"backup_codes_count":8,"max_failures":5,"lockout_duration_minutes":15,"require_for_admin":true,"require_for_users":false}'::jsonb, false, now()), +('email.transport', 'Email Transport', 'string', 'Email / SMTP', 'Email transport provider', ARRAY['email', 'smtp']::text[], '"smtp"'::jsonb, '"smtp"'::jsonb, false, now()), +('email.from', 'From Address', 'string', 'Email / SMTP', 'From address for outgoing emails', ARRAY['email', 'smtp']::text[], '""'::jsonb, '""'::jsonb, false, now()), +('email.smtp.host', 'SMTP Host', 'string', 'Email / SMTP', 'SMTP host', ARRAY['email', 'smtp']::text[], '""'::jsonb, '""'::jsonb, false, now()), +('email.smtp.port', 'SMTP Port', 'number', 'Email / SMTP', 'SMTP port', ARRAY['email', 'smtp']::text[], '587'::jsonb, '587'::jsonb, false, now()), +('email.smtp.user', 'SMTP User', 'string', 'Email / SMTP', 'SMTP username', ARRAY['email', 'smtp']::text[], '""'::jsonb, '""'::jsonb, false, now()), +('email.smtp.password', 'SMTP Password', 'string', 'Email / SMTP', 'SMTP password', ARRAY['email', 'smtp']::text[], '""'::jsonb, '""'::jsonb, true, now()), +('email.smtp.enabled', 'SMTP Enabled', 'boolean', 'Email / SMTP', 'Enable SMTP for outgoing email', ARRAY['email', 'smtp']::text[], 'false'::jsonb, 'false'::jsonb, false, now()), +('email.verification.token_ttl_minutes', 'Verification Token TTL (minutes)', 'number', 'Email / Verification', 'Minutes until email verification links expire', ARRAY['email', 'verification']::text[], '1440'::jsonb, '1440'::jsonb, false, now()), +('branding.identity', 'Brand Identity', 'object', 'Branding/Identity', 'Product name and tagline used across user pages', ARRAY[]::text[], '{"title":"DarkAuth","tagline":"Secure Zero-Knowledge Authentication"}'::jsonb, '{"title":"DarkAuth","tagline":"Secure Zero-Knowledge Authentication"}'::jsonb, false, now()), +('branding.logo', 'Logo Image', 'object', 'Branding/Identity', 'Base64 logo image and MIME type', ARRAY[]::text[], '{"data":null,"mimeType":null}'::jsonb, '{"data":null,"mimeType":null}'::jsonb, false, now()), +('branding.logo_dark', 'Logo Image (Dark)', 'object', 'Branding/Identity', 'Base64 dark mode logo image and MIME type', ARRAY[]::text[], '{"data":null,"mimeType":null}'::jsonb, '{"data":null,"mimeType":null}'::jsonb, false, now()), +('branding.favicon', 'Favicon', 'object', 'Branding/Identity', 'Base64 favicon and MIME type', ARRAY[]::text[], '{"data":null,"mimeType":null}'::jsonb, '{"data":null,"mimeType":null}'::jsonb, false, now()), +('branding.favicon_dark', 'Favicon (Dark)', 'object', 'Branding/Identity', 'Base64 dark mode favicon and MIME type', ARRAY[]::text[], '{"data":null,"mimeType":null}'::jsonb, '{"data":null,"mimeType":null}'::jsonb, false, now()), +('branding.colors', 'Color Scheme', 'object', 'Branding/Appearance', 'Color palette for user login and consent screens', ARRAY[]::text[], '{"backgroundGradientStart":"#f3f4f6","backgroundGradientEnd":"#eff6ff","backgroundAngle":"135deg","primary":"#6600cc","primaryHover":"#2563eb","primaryLight":"#dbeafe","primaryDark":"#1d4ed8","secondary":"#6b7280","secondaryHover":"#4b5563","success":"#10b981","error":"#ef4444","warning":"#f59e0b","info":"#6600cc","text":"#111827","textSecondary":"#6b7280","textMuted":"#9ca3af","border":"#e5e7eb","cardBackground":"#ffffff","cardShadow":"rgba(0,0,0,0.1)","inputBackground":"#ffffff","inputBorder":"#d1d5db","inputFocus":"#6600cc"}'::jsonb, '{"backgroundGradientStart":"#f3f4f6","backgroundGradientEnd":"#eff6ff","backgroundAngle":"135deg","primary":"#6600cc","primaryHover":"#2563eb","primaryLight":"#dbeafe","primaryDark":"#1d4ed8","secondary":"#6b7280","secondaryHover":"#4b5563","success":"#10b981","error":"#ef4444","warning":"#f59e0b","info":"#6600cc","text":"#111827","textSecondary":"#6b7280","textMuted":"#9ca3af","border":"#e5e7eb","cardBackground":"#ffffff","cardShadow":"rgba(0,0,0,0.1)","inputBackground":"#ffffff","inputBorder":"#d1d5db","inputFocus":"#6600cc"}'::jsonb, false, now()), +('branding.colors_dark', 'Color Scheme (Dark)', 'object', 'Branding/Appearance', 'Dark mode color palette for user login and consent screens', ARRAY[]::text[], '{"backgroundGradientStart":"#1f2937","backgroundGradientEnd":"#111827","backgroundAngle":"135deg","primary":"#aec1e0","primaryHover":"#9eb3d6","primaryLight":"#374151","primaryDark":"#c5d3e8","secondary":"#9ca3af","secondaryHover":"#d1d5db","success":"#10b981","error":"#ef4444","warning":"#f59e0b","info":"#aec1e0","text":"#f9fafb","textSecondary":"#d1d5db","textMuted":"#9ca3af","border":"#374151","cardBackground":"#1f2937","cardShadow":"rgba(0,0,0,0.3)","inputBackground":"#111827","inputBorder":"#4b5563","inputFocus":"#aec1e0"}'::jsonb, '{"backgroundGradientStart":"#1f2937","backgroundGradientEnd":"#111827","backgroundAngle":"135deg","primary":"#aec1e0","primaryHover":"#9eb3d6","primaryLight":"#374151","primaryDark":"#c5d3e8","secondary":"#9ca3af","secondaryHover":"#d1d5db","success":"#10b981","error":"#ef4444","warning":"#f59e0b","info":"#aec1e0","text":"#f9fafb","textSecondary":"#d1d5db","textMuted":"#9ca3af","border":"#374151","cardBackground":"#1f2937","cardShadow":"rgba(0,0,0,0.3)","inputBackground":"#111827","inputBorder":"#4b5563","inputFocus":"#aec1e0"}'::jsonb, false, now()), +('branding.wording', 'UI Text', 'object', 'Branding/Text', 'Text labels used in user flows', ARRAY[]::text[], '{"welcomeBack":"Welcome back","createAccount":"Create your account","email":"Email","emailPlaceholder":"Enter your email","password":"Password","passwordPlaceholder":"Enter your password","confirmPassword":"Confirm Password","confirmPasswordPlaceholder":"Confirm your password","signin":"Continue","signingIn":"Signing in...","signup":"Sign up","signingUp":"Creating account...","signout":"Sign Out","changePassword":"Change Password","cancel":"Cancel","authorize":"Authorize","deny":"Deny","noAccount":"Don''t have an account?","hasAccount":"Already have an account?","forgotPassword":"Forgot your password?","signedInAs":"Signed in as","successAuth":"Successfully authenticated","errorGeneral":"An error occurred. Please try again.","errorNetwork":"Network error. Please check your connection.","errorInvalidCreds":"Invalid email or password.","authorizeTitle":"Authorize Application","authorizeDescription":"{app} would like to:","scopeProfile":"Access your profile information","scopeEmail":"Access your email address","scopeOpenid":"Authenticate you"}'::jsonb, '{"welcomeBack":"Welcome back","createAccount":"Create your account","email":"Email","emailPlaceholder":"Enter your email","password":"Password","passwordPlaceholder":"Enter your password","confirmPassword":"Confirm Password","confirmPasswordPlaceholder":"Confirm your password","signin":"Continue","signingIn":"Signing in...","signup":"Sign up","signingUp":"Creating account...","signout":"Sign Out","changePassword":"Change Password","cancel":"Cancel","authorize":"Authorize","deny":"Deny","noAccount":"Don''t have an account?","hasAccount":"Already have an account?","forgotPassword":"Forgot your password?","signedInAs":"Signed in as","successAuth":"Successfully authenticated","errorGeneral":"An error occurred. Please try again.","errorNetwork":"Network error. Please check your connection.","errorInvalidCreds":"Invalid email or password.","authorizeTitle":"Authorize Application","authorizeDescription":"{app} would like to:","scopeProfile":"Access your profile information","scopeEmail":"Access your email address","scopeOpenid":"Authenticate you"}'::jsonb, false, now()), +('branding.font', 'Typography', 'object', 'Branding/Appearance', 'Font family, base size, and weights', ARRAY[]::text[], '{"family":"system-ui, -apple-system, BlinkMacSystemFont, ''Segoe UI'', Roboto, sans-serif","size":"16px","weight":{"normal":"400","medium":"500","bold":"700"}}'::jsonb, '{"family":"system-ui, -apple-system, BlinkMacSystemFont, ''Segoe UI'', Roboto, sans-serif","size":"16px","weight":{"normal":"400","medium":"500","bold":"700"}}'::jsonb, false, now()), +('branding.custom_css', 'Custom CSS', 'string', 'Branding/Advanced', 'Additional CSS injected into login and consent pages', ARRAY[]::text[], '""'::jsonb, '""'::jsonb, false, now()), +('email.templates.signup_verification', 'signup_verification Template', 'object', 'Email / Templates', 'Template for signup_verification', ARRAY['email', 'templates']::text[], '{"subject":"Verify your email","text":"Hello {{name}},\n\nPlease verify your email by opening this link:\n{{verification_link}}\n\nIf you did not create this account, ignore this email.","html":"

Hello {{name}},

Please verify your email by opening this link:

Verify email

If you did not create this account, ignore this email.

"}'::jsonb, '{"subject":"Verify your email","text":"Hello {{name}},\n\nPlease verify your email by opening this link:\n{{verification_link}}\n\nIf you did not create this account, ignore this email.","html":"

Hello {{name}},

Please verify your email by opening this link:

Verify email

If you did not create this account, ignore this email.

"}'::jsonb, false, now()), +('email.templates.verification_resend_confirmation', 'verification_resend_confirmation Template', 'object', 'Email / Templates', 'Template for verification_resend_confirmation', ARRAY['email', 'templates']::text[], '{"subject":"A new verification link has been sent","text":"Hello {{name}},\n\nA new verification link has been requested for this account.\n\nIf this was you, use the newest email in your inbox.","html":"

Hello {{name}},

A new verification link has been requested for this account.

If this was you, use the newest email in your inbox.

"}'::jsonb, '{"subject":"A new verification link has been sent","text":"Hello {{name}},\n\nA new verification link has been requested for this account.\n\nIf this was you, use the newest email in your inbox.","html":"

Hello {{name}},

A new verification link has been requested for this account.

If this was you, use the newest email in your inbox.

"}'::jsonb, false, now()), +('email.templates.email_change_verification', 'email_change_verification Template', 'object', 'Email / Templates', 'Template for email_change_verification', ARRAY['email', 'templates']::text[], '{"subject":"Verify your new email address","text":"Hello {{name}},\n\nPlease verify your new email address by opening this link:\n{{verification_link}}\n\nYour current email remains active until verification completes.","html":"

Hello {{name}},

Please verify your new email address by opening this link:

Verify new email

Your current email remains active until verification completes.

"}'::jsonb, '{"subject":"Verify your new email address","text":"Hello {{name}},\n\nPlease verify your new email address by opening this link:\n{{verification_link}}\n\nYour current email remains active until verification completes.","html":"

Hello {{name}},

Please verify your new email address by opening this link:

Verify new email

Your current email remains active until verification completes.

"}'::jsonb, false, now()), +('email.templates.password_recovery', 'password_recovery Template', 'object', 'Email / Templates', 'Template for password_recovery', ARRAY['email', 'templates']::text[], '{"subject":"Password recovery","text":"Hello {{name}},\n\nUse this link to recover access to your account:\n{{recovery_link}}","html":"

Hello {{name}},

Use this link to recover access to your account:

Recover account

"}'::jsonb, '{"subject":"Password recovery","text":"Hello {{name}},\n\nUse this link to recover access to your account:\n{{recovery_link}}","html":"

Hello {{name}},

Use this link to recover access to your account:

Recover account

"}'::jsonb, false, now()), +('email.templates.admin_test_email', 'admin_test_email Template', 'object', 'Email / Templates', 'Template for admin_test_email', ARRAY['email', 'templates']::text[], '{"subject":"DarkAuth SMTP test","text":"This is a test email from DarkAuth.\n\nSent at: {{sent_at}}","html":"

This is a test email from DarkAuth.

Sent at: {{sent_at}}

"}'::jsonb, '{"subject":"DarkAuth SMTP test","text":"This is a test email from DarkAuth.\n\nSent at: {{sent_at}}","html":"

This is a test email from DarkAuth.

Sent at: {{sent_at}}

"}'::jsonb, false, now()) +ON CONFLICT ("key") DO UPDATE SET +"name" = excluded."name", +"type" = excluded."type", +"category" = excluded."category", +"description" = excluded."description", +"tags" = excluded."tags", +"default_value" = excluded."default_value", +"secure" = excluded."secure", +"updated_at" = now(); \ No newline at end of file diff --git a/packages/api/drizzle/0018_default_clients_and_org_rbac.sql b/packages/api/drizzle/0018_default_clients_and_org_rbac.sql new file mode 100644 index 0000000..471ae70 --- /dev/null +++ b/packages/api/drizzle/0018_default_clients_and_org_rbac.sql @@ -0,0 +1,135 @@ +INSERT INTO "permissions" ("key", "description") +VALUES + ('darkauth.users:read', 'Allows searching and reading users from the user directory endpoints'), + ('darkauth.org:manage', 'Allows management of organization members, roles, and invites') +ON CONFLICT ("key") DO UPDATE +SET + "description" = EXCLUDED."description"; +--> statement-breakpoint +INSERT INTO "roles" ("key", "name", "description", "system", "created_at", "updated_at") +VALUES + ('org_admin', 'Organization Admin', 'Can manage organization members, roles, and invitations', true, now(), now()), + ('member', 'Member', 'Default organization member role', true, now(), now()) +ON CONFLICT ("key") DO UPDATE +SET + "name" = EXCLUDED."name", + "description" = EXCLUDED."description", + "system" = EXCLUDED."system", + "updated_at" = now(); +--> statement-breakpoint +INSERT INTO "role_permissions" ("role_id", "permission_key") +SELECT r."id", p."key" +FROM "roles" r +JOIN "permissions" p ON p."key" IN ('darkauth.org:manage', 'darkauth.users:read') +WHERE r."key" = 'org_admin' +ON CONFLICT DO NOTHING; +--> statement-breakpoint +INSERT INTO "organization_member_roles" ("organization_member_id", "role_id") +SELECT om."id", r."id" +FROM "organization_members" om +JOIN "roles" r ON r."key" = 'member' +LEFT JOIN "organization_member_roles" omr ON omr."organization_member_id" = om."id" +WHERE omr."organization_member_id" IS NULL +ON CONFLICT DO NOTHING; +--> statement-breakpoint +INSERT INTO "clients" ( + "client_id", + "name", + "type", + "token_endpoint_auth_method", + "client_secret_enc", + "require_pkce", + "zk_delivery", + "zk_required", + "allowed_jwe_algs", + "allowed_jwe_encs", + "redirect_uris", + "post_logout_redirect_uris", + "grant_types", + "response_types", + "scopes", + "allowed_zk_origins", + "created_at", + "updated_at" +) +VALUES + ( + 'user', + 'User Portal', + 'public', + 'none', + NULL, + true, + 'none', + false, + ARRAY[]::text[], + ARRAY[]::text[], + ARRAY['http://localhost:9080/callback']::text[], + ARRAY['http://localhost:9080']::text[], + ARRAY['authorization_code', 'refresh_token']::text[], + ARRAY['code']::text[], + ARRAY[ + '{"key":"openid","description":"Authenticate you"}', + '{"key":"profile","description":"Access your profile information"}', + '{"key":"email","description":"Access your email address"}' + ]::text[], + ARRAY['http://localhost:9080']::text[], + now(), + now() + ), + ( + 'demo-public-client', + 'Demo Public Client', + 'public', + 'none', + NULL, + true, + 'fragment-jwe', + true, + ARRAY['ECDH-ES']::text[], + ARRAY['A256GCM']::text[], + ARRAY[ + 'http://localhost:9092/', + 'http://localhost:9092/callback', + 'http://localhost:3000/', + 'http://localhost:3000/callback', + 'https://app.example.com/', + 'https://app.example.com/callback' + ]::text[], + ARRAY['http://localhost:9092/', 'http://localhost:3000', 'https://app.example.com']::text[], + ARRAY['authorization_code', 'refresh_token']::text[], + ARRAY['code']::text[], + ARRAY[ + '{"key":"openid","description":"Authenticate you"}', + '{"key":"profile","description":"Access your profile information"}', + '{"key":"email","description":"Access your email address"}' + ]::text[], + ARRAY['http://localhost:9092', 'http://localhost:3000', 'https://app.example.com']::text[], + now(), + now() + ), + ( + 'demo-confidential-client', + 'Demo Confidential Client', + 'confidential', + 'client_secret_basic', + NULL, + false, + 'none', + false, + ARRAY[]::text[], + ARRAY[]::text[], + ARRAY['http://localhost:4000/callback', 'https://support.example.com/callback']::text[], + ARRAY['http://localhost:4000', 'https://support.example.com']::text[], + ARRAY['authorization_code', 'refresh_token', 'client_credentials']::text[], + ARRAY['code']::text[], + ARRAY[ + '{"key":"openid","description":"Authenticate you"}', + '{"key":"profile","description":"Access your profile information"}', + '{"key":"darkauth.users:read","description":"Search and read users from the directory"}' + ]::text[], + ARRAY[]::text[], + now(), + now() + ) +ON CONFLICT ("client_id") DO NOTHING; diff --git a/packages/api/drizzle/meta/_journal.json b/packages/api/drizzle/meta/_journal.json index ce93c43..226d61b 100644 --- a/packages/api/drizzle/meta/_journal.json +++ b/packages/api/drizzle/meta/_journal.json @@ -106,6 +106,20 @@ "when": 1772445600000, "tag": "0016_email_verification", "breakpoints": true + }, + { + "idx": 15, + "version": "7", + "when": 1772447400000, + "tag": "0017_settings_defaults", + "breakpoints": true + }, + { + "idx": 16, + "version": "7", + "when": 1772449200000, + "tag": "0018_default_clients_and_org_rbac", + "breakpoints": true } ] } diff --git a/packages/api/scripts/install.ts b/packages/api/scripts/install.ts index d8f11ef..65a0104 100644 --- a/packages/api/scripts/install.ts +++ b/packages/api/scripts/install.ts @@ -1,12 +1,17 @@ #!/usr/bin/env node +/** + * Seeding policy: + * - Production/default bootstrap data (for example settings, RBAC, default clients) must be owned by database migrations. + * - Runtime install logic should only write install-specific values that are unique to this instance. + * - Code-based seeding is reserved for test/dev/sample/demo data. + */ +import { eq } from "drizzle-orm"; import { createContext } from "../src/context/createContext.ts"; -import { adminUsers, settings } from "../src/db/schema.ts"; -import { seedDefaultOrganizationRbac } from "../src/models/install.ts"; +import { adminUsers, clients, settings } from "../src/db/schema.ts"; import { generateEdDSAKeyPair, storeKeyPair } from "../src/services/jwks.ts"; import { createKekService, generateKdfParams } from "../src/services/kek.ts"; -import { isSystemInitialized, markSystemInitialized, seedDefaultSettings } from "../src/services/settings.ts"; -import { seedDefaultClients } from "../src/models/install.ts"; +import { isSystemInitialized, markSystemInitialized, setSetting } from "../src/services/settings.ts"; import type { Config, KdfParams } from "../src/types.ts"; import { generateRandomString } from "../src/utils/crypto.ts"; import fs from "node:fs"; @@ -135,13 +140,9 @@ async function install() { updatedAt: new Date(), }); - console.log("3. Seeding default settings..."); - await seedDefaultSettings( - context, - config.issuer, - config.publicOrigin, - config.rpId, - ); + await setSetting(context, "issuer", config.issuer); + await setSetting(context, "public_origin", config.publicOrigin); + await setSetting(context, "rp_id", config.rpId); await context.db.insert(settings).values({ key: "ui_user", @@ -289,13 +290,28 @@ async function install() { Buffer.from(demoConfidentialClientSecret), ); } - - await seedDefaultClients(context, demoConfidentialSecretEnc, config.publicOrigin); - - console.log("6. Seeding default group..."); - await seedDefaultOrganizationRbac(context); - - console.log("7. Creating default admin user..."); + // Seeding is migration-owned. Install only writes runtime-specific values. + if (demoConfidentialSecretEnc) { + await context.db + .update(clients) + .set({ + clientSecretEnc: demoConfidentialSecretEnc, + updatedAt: new Date(), + }) + .where(eq(clients.clientId, "demo-confidential-client")); + } + const normalizedOrigin = config.publicOrigin.replace(/\/+$/, ""); + await context.db + .update(clients) + .set({ + redirectUris: [`${normalizedOrigin}/callback`], + postLogoutRedirectUris: [normalizedOrigin], + allowedZkOrigins: [normalizedOrigin], + updatedAt: new Date(), + }) + .where(eq(clients.clientId, "user")); + + console.log("6. Creating default admin user..."); const adminEmail = "admin@example.com"; const adminName = "System Administrator"; @@ -306,7 +322,7 @@ async function install() { createdAt: new Date(), }); - console.log("8. Marking system as initialized..."); + console.log("7. Marking system as initialized..."); await markSystemInitialized(context); console.log( diff --git a/packages/api/src/controllers/install/postInstallComplete.ts b/packages/api/src/controllers/install/postInstallComplete.ts index 271486c..fbdc560 100644 --- a/packages/api/src/controllers/install/postInstallComplete.ts +++ b/packages/api/src/controllers/install/postInstallComplete.ts @@ -1,6 +1,8 @@ import type { IncomingMessage, ServerResponse } from "node:http"; +import { eq } from "drizzle-orm"; import { z } from "zod/v4"; +import { clients } from "../../db/schema.ts"; import { AlreadyInitializedError, ExpiredInstallTokenError, @@ -10,12 +12,7 @@ import { import { genericErrors } from "../../http/openapi-helpers.ts"; import { generateEdDSAKeyPair, storeKeyPair } from "../../services/jwks.ts"; import { ensureKekService, generateKdfParams } from "../../services/kek.ts"; -import { - isSystemInitialized, - markSystemInitialized, - seedDefaultSettings, - setSetting, -} from "../../services/settings.ts"; +import { isSystemInitialized, markSystemInitialized, setSetting } from "../../services/settings.ts"; import type { Context, ControllerSchema } from "../../types.ts"; import { withAudit } from "../../utils/auditWrapper.ts"; import { generateRandomString } from "../../utils/crypto.ts"; @@ -113,15 +110,11 @@ async function _postInstallComplete( throw new Error("No database available for installation"); } - context.logger.info("[install:post] Seeding default settings"); - const tempContextDb = { ...context, db } as Context; - await seedDefaultSettings( - tempContextDb, - context.config.issuer, - context.config.publicOrigin, - context.config.rpId - ); const installCtx = { ...context, db } as Context; + const tempContextDb = { ...context, db } as Context; + await setSetting(installCtx, "issuer", context.config.issuer); + await setSetting(installCtx, "public_origin", context.config.publicOrigin); + await setSetting(installCtx, "rp_id", context.config.rpId); await setSetting( installCtx, "users.self_registration_enabled", @@ -174,13 +167,24 @@ async function _postInstallComplete( const demoConfidentialSecretEnc = await kekService.encrypt( Buffer.from(demoConfidentialClientSecret) ); - await (await import("../../models/install.ts")).seedDefaultClients( - installCtx, - demoConfidentialSecretEnc, - context.config.publicOrigin - ); + await installCtx.db + .update(clients) + .set({ + clientSecretEnc: demoConfidentialSecretEnc, + updatedAt: new Date(), + }) + .where(eq(clients.clientId, "demo-confidential-client")); + const normalizedOrigin = context.config.publicOrigin.replace(/\/+$/, ""); + await installCtx.db + .update(clients) + .set({ + redirectUris: [`${normalizedOrigin}/callback`], + postLogoutRedirectUris: [normalizedOrigin], + allowedZkOrigins: [normalizedOrigin], + updatedAt: new Date(), + }) + .where(eq(clients.clientId, "user")); await (await import("../../models/install.ts")).ensureDefaultOrganizationAndSchema(installCtx); - await (await import("../../models/install.ts")).seedDefaultOrganizationRbac(installCtx); context.logger.debug( "[install:post] verifying admin user was created during OPAQUE registration" diff --git a/packages/api/src/models/install.test.ts b/packages/api/src/models/install.test.ts index 94bcab2..98d421f 100644 --- a/packages/api/src/models/install.test.ts +++ b/packages/api/src/models/install.test.ts @@ -1,33 +1,19 @@ import assert from "node:assert/strict"; import { test } from "node:test"; -import { parseClientScopeDefinitions } from "../utils/clientScopes.ts"; -import { buildDefaultClientSeeds } from "./install.ts"; +import { + parseClientScopeDefinitions, + serializeClientScopeDefinitions, +} from "../utils/clientScopes.ts"; -test("buildDefaultClientSeeds includes structured scope definitions", () => { - const demoSecret = Buffer.from("demo-secret"); - const seeds = buildDefaultClientSeeds(demoSecret, "https://auth.example.com"); - - assert.equal(seeds.length, 3); - assert.equal(seeds[0]?.clientId, "user"); - assert.equal(seeds[1]?.clientId, "demo-public-client"); - assert.equal(seeds[2]?.clientId, "demo-confidential-client"); - assert.equal(seeds[2]?.clientSecretEnc, demoSecret); - - const userScopes = parseClientScopeDefinitions(seeds[0]?.scopes ?? []); - const publicScopes = parseClientScopeDefinitions(seeds[1]?.scopes ?? []); - const confidentialScopes = parseClientScopeDefinitions(seeds[2]?.scopes ?? []); - - assert.deepEqual(userScopes, [ - { key: "openid", description: "Authenticate you" }, - { key: "profile", description: "Access your profile information" }, - { key: "email", description: "Access your email address" }, - ]); - assert.deepEqual(publicScopes, [ +test("client scope serialization preserves structured entries", () => { + const scopes = serializeClientScopeDefinitions([ { key: "openid", description: "Authenticate you" }, { key: "profile", description: "Access your profile information" }, - { key: "email", description: "Access your email address" }, + { key: "darkauth.users:read", description: "Search and read users from the directory" }, ]); - assert.deepEqual(confidentialScopes, [ + const parsedScopes = parseClientScopeDefinitions(scopes); + + assert.deepEqual(parsedScopes, [ { key: "openid", description: "Authenticate you" }, { key: "profile", description: "Access your profile information" }, { key: "darkauth.users:read", description: "Search and read users from the directory" }, diff --git a/packages/api/src/models/install.ts b/packages/api/src/models/install.ts index 888bb46..2e01d24 100644 --- a/packages/api/src/models/install.ts +++ b/packages/api/src/models/install.ts @@ -2,18 +2,14 @@ import { eq } from "drizzle-orm"; import { adminOpaqueRecords, adminUsers, - clients, organizationMemberRoles, organizationMembers, organizations, - permissions, - rolePermissions, roles, settings, } from "../db/schema.ts"; import { ConflictError, NotFoundError } from "../errors.ts"; import type { Context } from "../types.ts"; -import { serializeClientScopeDefinitions } from "../utils/clientScopes.ts"; export async function storeOpaqueAdmin( context: Context, @@ -63,181 +59,6 @@ export async function writeKdfSetting(context: Context, kdfParams: unknown) { .values({ key: "kek_kdf", value: kdfParams, secure: true, updatedAt: new Date() }); } -export function buildDefaultClientSeeds( - demoConfidentialSecretEnc: Buffer | null, - publicOrigin: string -) { - const normalizedOrigin = publicOrigin.replace(/\/+$/, ""); - return [ - { - clientId: "user", - name: "User Portal", - type: "public" as const, - tokenEndpointAuthMethod: "none" as const, - clientSecretEnc: null, - requirePkce: true, - zkDelivery: "none" as const, - zkRequired: false, - allowedJweAlgs: [], - allowedJweEncs: [], - redirectUris: [`${normalizedOrigin}/callback`], - postLogoutRedirectUris: [normalizedOrigin], - grantTypes: ["authorization_code", "refresh_token"], - responseTypes: ["code"], - scopes: serializeClientScopeDefinitions([ - { key: "openid", description: "Authenticate you" }, - { key: "profile", description: "Access your profile information" }, - { key: "email", description: "Access your email address" }, - ]), - allowedZkOrigins: [normalizedOrigin], - createdAt: new Date(), - updatedAt: new Date(), - }, - { - clientId: "demo-public-client", - name: "Demo Public Client", - type: "public" as const, - tokenEndpointAuthMethod: "none" as const, - clientSecretEnc: null, - requirePkce: true, - zkDelivery: "fragment-jwe" as const, - zkRequired: true, - allowedJweAlgs: ["ECDH-ES"], - allowedJweEncs: ["A256GCM"], - redirectUris: [ - "http://localhost:9092/", - "http://localhost:9092/callback", - "http://localhost:3000/", - "http://localhost:3000/callback", - "https://app.example.com/", - "https://app.example.com/callback", - ], - postLogoutRedirectUris: [ - "http://localhost:9092/", - "http://localhost:3000", - "https://app.example.com", - ], - grantTypes: ["authorization_code", "refresh_token"], - responseTypes: ["code"], - scopes: serializeClientScopeDefinitions([ - { key: "openid", description: "Authenticate you" }, - { key: "profile", description: "Access your profile information" }, - { key: "email", description: "Access your email address" }, - ]), - allowedZkOrigins: [ - "http://localhost:9092", - "http://localhost:3000", - "https://app.example.com", - ], - createdAt: new Date(), - updatedAt: new Date(), - }, - { - clientId: "demo-confidential-client", - name: "Demo Confidential Client", - type: "confidential" as const, - tokenEndpointAuthMethod: "client_secret_basic" as const, - clientSecretEnc: demoConfidentialSecretEnc, - requirePkce: false, - zkDelivery: "none" as const, - zkRequired: false, - allowedJweAlgs: [], - allowedJweEncs: [], - redirectUris: ["http://localhost:4000/callback", "https://support.example.com/callback"], - postLogoutRedirectUris: ["http://localhost:4000", "https://support.example.com"], - grantTypes: ["authorization_code", "refresh_token", "client_credentials"], - responseTypes: ["code"], - scopes: serializeClientScopeDefinitions([ - { key: "openid", description: "Authenticate you" }, - { key: "profile", description: "Access your profile information" }, - { - key: "darkauth.users:read", - description: "Search and read users from the directory", - }, - ]), - allowedZkOrigins: [], - createdAt: new Date(), - updatedAt: new Date(), - }, - ]; -} - -export async function seedDefaultClients( - context: Context, - demoConfidentialSecretEnc: Buffer | null, - publicOrigin: string -) { - await context.db - .insert(clients) - .values(buildDefaultClientSeeds(demoConfidentialSecretEnc, publicOrigin)); -} - -export async function seedDefaultOrganizationRbac(context: Context) { - const usersReadPermission = await context.db.query.permissions.findFirst({ - where: eq(permissions.key, "darkauth.users:read"), - }); - if (!usersReadPermission) { - await context.db.insert(permissions).values({ - key: "darkauth.users:read", - description: "Allows searching and reading users from the user directory endpoints", - }); - } - const orgManagePermission = await context.db.query.permissions.findFirst({ - where: eq(permissions.key, "darkauth.org:manage"), - }); - if (!orgManagePermission) { - await context.db.insert(permissions).values({ - key: "darkauth.org:manage", - description: "Allows management of organization members, roles, and invites", - }); - } - - const existingOrg = await context.db.query.organizations.findFirst({ - where: eq(organizations.slug, "default"), - }); - if (!existingOrg) { - await context.db.insert(organizations).values({ - slug: "default", - name: "Default", - forceOtp: false, - createdAt: new Date(), - updatedAt: new Date(), - }); - } - - const defaultOrg = await context.db.query.organizations.findFirst({ - where: eq(organizations.slug, "default"), - }); - if (!defaultOrg) return; - - await context.db - .insert(roles) - .values([ - { key: "member", name: "Member", system: true, createdAt: new Date(), updatedAt: new Date() }, - { - key: "org_admin", - name: "Organization Admin", - system: true, - createdAt: new Date(), - updatedAt: new Date(), - }, - ]) - .onConflictDoNothing(); - - const orgAdminRole = await context.db.query.roles.findFirst({ - where: eq(roles.key, "org_admin"), - }); - if (orgAdminRole) { - await context.db - .insert(rolePermissions) - .values([ - { roleId: orgAdminRole.id, permissionKey: "darkauth.org:manage" }, - { roleId: orgAdminRole.id, permissionKey: "darkauth.users:read" }, - ]) - .onConflictDoNothing(); - } -} - export async function ensureDefaultOrganizationAndSchema(context: Context) { try { await context.db.execute( @@ -319,10 +140,6 @@ export async function ensureDefaultOrganizationAndSchema(context: Context) { ); } catch {} - try { - await seedDefaultOrganizationRbac(context); - } catch {} - try { const defaultOrg = await context.db.query.organizations.findFirst({ where: eq(organizations.slug, "default"), diff --git a/packages/api/src/models/organizations.test.ts b/packages/api/src/models/organizations.test.ts index f75a301..b7213ce 100644 --- a/packages/api/src/models/organizations.test.ts +++ b/packages/api/src/models/organizations.test.ts @@ -55,7 +55,10 @@ test("createOrganizationInvite rejects unknown or non-assignable role ids", asyn .returning(); assert.ok(managerRole); - await db.insert(permissions).values({ key: "darkauth.org:manage", description: "Manage org" }); + await db + .insert(permissions) + .values({ key: "darkauth.org:manage", description: "Manage org" }) + .onConflictDoNothing(); await db .insert(rolePermissions) .values({ roleId: managerRole.id, permissionKey: "darkauth.org:manage" }); diff --git a/packages/api/src/services/settings.ts b/packages/api/src/services/settings.ts index 340f470..38da298 100644 --- a/packages/api/src/services/settings.ts +++ b/packages/api/src/services/settings.ts @@ -126,853 +126,6 @@ export async function pruneDeprecatedSettings(context: Context): Promise { .where(eq(settings.key, "admin_session")); } -export async function seedDefaultSettings( - context: Context, - issuer: string, - publicOrigin: string, - rpId: string -): Promise { - const items: Array<{ - key: string; - name: string; - type: string; - category: string; - description?: string; - tags?: string[]; - defaultValue: unknown; - value: unknown; - secure?: boolean; - }> = [ - { - key: "issuer", - name: "Issuer", - type: "string", - category: "Core", - description: "Issuer URL used in OIDC discovery and tokens", - tags: ["core"], - defaultValue: issuer, - value: issuer, - }, - - { - key: "users.self_registration_enabled", - name: "Self Registration Enabled", - type: "boolean", - category: "Users", - description: "Allow new users to sign up without invitation", - tags: ["users"], - defaultValue: false, - value: false, - }, - { - key: "users.require_email_verification", - name: "Require Email Verification", - type: "boolean", - category: "Users", - description: "Require users to verify their email before login completes", - tags: ["users", "email"], - defaultValue: false, - value: false, - }, - { - key: "public_origin", - name: "Public Origin", - type: "string", - category: "Core", - description: "Public base origin for redirects and links", - tags: ["core"], - defaultValue: publicOrigin, - value: publicOrigin, - }, - { - key: "rp_id", - name: "RP ID", - type: "string", - category: "Core", - description: "WebAuthn relying party ID", - tags: ["core", "webauthn"], - defaultValue: rpId, - value: rpId, - }, - - { - key: "zk_delivery.fragment_param", - name: "Fragment Param", - type: "string", - category: "ZK / Delivery", - description: "URL fragment parameter name for DRK JWE", - tags: ["zk"], - defaultValue: "drk_jwe", - value: "drk_jwe", - }, - { - key: "zk_delivery.jwe_alg", - name: "JWE Alg", - type: "string", - category: "ZK / Delivery", - description: "Default JWE algorithm for DRK delivery", - tags: ["zk"], - defaultValue: "ECDH-ES", - value: "ECDH-ES", - }, - { - key: "zk_delivery.jwe_enc", - name: "JWE Enc", - type: "string", - category: "ZK / Delivery", - description: "Default JWE encryption method for DRK delivery", - tags: ["zk"], - defaultValue: "A256GCM", - value: "A256GCM", - }, - { - key: "zk_delivery.hash_alg", - name: "Hash Alg", - type: "string", - category: "ZK / Delivery", - description: "Hash algorithm used for fragment verification", - tags: ["zk"], - defaultValue: "SHA-256", - value: "SHA-256", - }, - - { - key: "opaque.kdf", - name: "OPAQUE KDF", - type: "string", - category: "Security / OPAQUE", - description: "OPAQUE KDF suite", - tags: ["opaque"], - defaultValue: "ristretto255", - value: "ristretto255", - }, - { - key: "opaque.envelope_mode", - name: "Envelope Mode", - type: "string", - category: "Security / OPAQUE", - description: "OPAQUE envelope mode", - tags: ["opaque"], - defaultValue: "base", - value: "base", - }, - - { - key: "security_headers.enabled", - name: "Security Headers Enabled", - type: "boolean", - category: "Security / Headers", - description: "Enable security headers middleware", - tags: ["security"], - defaultValue: true, - value: true, - }, - { - key: "security_headers.csp", - name: "CSP", - type: "string", - category: "Security / Headers", - description: "Content Security Policy applied to responses", - tags: ["security"], - defaultValue: - "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https://darkauth.com https://release.darkauth.com; frame-ancestors 'self'; base-uri 'none'; form-action 'self'; object-src 'none'; require-trusted-types-for 'script'", - value: - "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https://darkauth.com https://release.darkauth.com; frame-ancestors 'self'; base-uri 'none'; form-action 'self'; object-src 'none'; require-trusted-types-for 'script'", - }, - - { - key: "rate_limits.general.enabled", - name: "General Enabled", - type: "boolean", - category: "Security / Rate Limits / General", - description: "Enable general rate limiting", - tags: ["ratelimit"], - defaultValue: true, - value: true, - }, - { - key: "rate_limits.general.window_minutes", - name: "General Window (min)", - type: "number", - category: "Security / Rate Limits / General", - description: "Window length in minutes", - tags: ["ratelimit"], - defaultValue: 1, - value: 1, - }, - { - key: "rate_limits.general.max_requests", - name: "General Max Requests", - type: "number", - category: "Security / Rate Limits / General", - description: "Maximum requests per window", - tags: ["ratelimit"], - defaultValue: 1000, - value: 1000, - }, - - { - key: "rate_limits.auth.enabled", - name: "Auth Enabled", - type: "boolean", - category: "Security / Rate Limits / Auth", - description: "Enable rate limits for auth endpoints", - tags: ["ratelimit"], - defaultValue: true, - value: true, - }, - { - key: "rate_limits.auth.window_minutes", - name: "Auth Window (min)", - type: "number", - category: "Security / Rate Limits / Auth", - description: "Window length in minutes", - tags: ["ratelimit"], - defaultValue: 1, - value: 1, - }, - { - key: "rate_limits.auth.max_requests", - name: "Auth Max Requests", - type: "number", - category: "Security / Rate Limits / Auth", - description: "Maximum requests per window", - tags: ["ratelimit"], - defaultValue: 1000, - value: 1000, - }, - - { - key: "rate_limits.opaque.enabled", - name: "OPAQUE Enabled", - type: "boolean", - category: "Security / Rate Limits / OPAQUE", - description: "Enable rate limits for OPAQUE endpoints", - tags: ["ratelimit"], - defaultValue: true, - value: true, - }, - { - key: "rate_limits.opaque.window_minutes", - name: "OPAQUE Window (min)", - type: "number", - category: "Security / Rate Limits / OPAQUE", - tags: ["ratelimit"], - defaultValue: 1, - value: 1, - }, - { - key: "rate_limits.opaque.max_requests", - name: "OPAQUE Max Requests", - type: "number", - category: "Security / Rate Limits / OPAQUE", - tags: ["ratelimit"], - defaultValue: 1000, - value: 1000, - }, - - { - key: "rate_limits.token.enabled", - name: "Token Enabled", - type: "boolean", - category: "Security / Rate Limits / Token", - tags: ["ratelimit"], - defaultValue: true, - value: true, - }, - { - key: "rate_limits.token.window_minutes", - name: "Token Window (min)", - type: "number", - category: "Security / Rate Limits / Token", - tags: ["ratelimit"], - defaultValue: 1, - value: 1, - }, - { - key: "rate_limits.token.max_requests", - name: "Token Max Requests", - type: "number", - category: "Security / Rate Limits / Token", - tags: ["ratelimit"], - defaultValue: 1000, - value: 1000, - }, - - { - key: "rate_limits.otp.enabled", - name: "OTP Enabled", - type: "boolean", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: true, - value: true, - }, - { - key: "rate_limits.otp.window_minutes", - name: "OTP Window (min)", - type: "number", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: 15, - value: 15, - }, - { - key: "rate_limits.otp.max_requests", - name: "OTP Max Requests", - type: "number", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: 1000, - value: 1000, - }, - - { - key: "rate_limits.otp_setup.enabled", - name: "OTP Setup Enabled", - type: "boolean", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: true, - value: true, - }, - { - key: "rate_limits.otp_setup.window_minutes", - name: "OTP Setup Window (min)", - type: "number", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: 60, - value: 60, - }, - { - key: "rate_limits.otp_setup.max_requests", - name: "OTP Setup Max Requests", - type: "number", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: 1000, - value: 1000, - }, - - { - key: "rate_limits.otp_verify.enabled", - name: "OTP Verify Enabled", - type: "boolean", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: true, - value: true, - }, - { - key: "rate_limits.otp_verify.window_minutes", - name: "OTP Verify Window (min)", - type: "number", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: 60, - value: 60, - }, - { - key: "rate_limits.otp_verify.max_requests", - name: "OTP Verify Max Requests", - type: "number", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: 1000, - value: 1000, - }, - - { - key: "rate_limits.otp_disable.enabled", - name: "OTP Disable Enabled", - type: "boolean", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: true, - value: true, - }, - { - key: "rate_limits.otp_disable.window_minutes", - name: "OTP Disable Window (min)", - type: "number", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: 60, - value: 60, - }, - { - key: "rate_limits.otp_disable.max_requests", - name: "OTP Disable Max Requests", - type: "number", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: 1000, - value: 1000, - }, - - { - key: "rate_limits.otp_regenerate.enabled", - name: "OTP Regenerate Enabled", - type: "boolean", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: true, - value: true, - }, - { - key: "rate_limits.otp_regenerate.window_minutes", - name: "OTP Regenerate Window (min)", - type: "number", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: 60, - value: 60, - }, - { - key: "rate_limits.otp_regenerate.max_requests", - name: "OTP Regenerate Max Requests", - type: "number", - category: "Security / Rate Limits / OTP", - tags: ["ratelimit"], - defaultValue: 1000, - value: 1000, - }, - - { - key: "rate_limits.admin.enabled", - name: "Admin Enabled", - type: "boolean", - category: "Security / Rate Limits / Admin", - tags: ["ratelimit"], - defaultValue: true, - value: true, - }, - { - key: "rate_limits.admin.window_minutes", - name: "Admin Window (min)", - type: "number", - category: "Security / Rate Limits / Admin", - tags: ["ratelimit"], - defaultValue: 1, - value: 1, - }, - { - key: "rate_limits.admin.max_requests", - name: "Admin Max Requests", - type: "number", - category: "Security / Rate Limits / Admin", - tags: ["ratelimit"], - defaultValue: 1000, - value: 1000, - }, - - { - key: "rate_limits.install.enabled", - name: "Install Enabled", - type: "boolean", - category: "Security / Rate Limits / Install", - tags: ["ratelimit"], - defaultValue: true, - value: true, - }, - { - key: "rate_limits.install.window_minutes", - name: "Install Window (min)", - type: "number", - category: "Security / Rate Limits / Install", - tags: ["ratelimit"], - defaultValue: 60, - value: 60, - }, - { - key: "rate_limits.install.max_requests", - name: "Install Max Requests", - type: "number", - category: "Security / Rate Limits / Install", - tags: ["ratelimit"], - defaultValue: 1000, - value: 1000, - }, - - { - key: "user_keys.enc_public_visible_to_authenticated_users", - name: "Enc Public Visible To Authenticated Users", - type: "boolean", - category: "Security / User Keys", - description: "Expose users' encryption public keys to authenticated users", - tags: ["keys"], - defaultValue: true, - value: true, - }, - - { - key: "admin_session.lifetime_seconds", - name: "Admin Session TTL (s)", - type: "number", - category: "Admin / Session", - description: "Admin session lifetime in seconds", - tags: ["session"], - defaultValue: 15 * 60, - value: 15 * 60, - }, - - { - key: "otp", - name: "OTP Settings", - type: "json", - category: "Security / OTP", - description: "TOTP configuration and policy", - tags: ["otp", "security"], - defaultValue: { - enabled: true, - issuer: "DarkAuth", - algorithm: "SHA1", - digits: 6, - period: 30, - window: 1, - backup_codes_count: 8, - max_failures: 5, - lockout_duration_minutes: 15, - require_for_admin: true, - require_for_users: false, - }, - value: { - enabled: true, - issuer: "DarkAuth", - algorithm: "SHA1", - digits: 6, - period: 30, - window: 1, - backup_codes_count: 8, - max_failures: 5, - lockout_duration_minutes: 15, - require_for_admin: true, - require_for_users: false, - }, - }, - { - key: "email.transport", - name: "Email Transport", - type: "string", - category: "Email / SMTP", - description: "Email transport provider", - tags: ["email", "smtp"], - defaultValue: "smtp", - value: "smtp", - }, - { - key: "email.from", - name: "From Address", - type: "string", - category: "Email / SMTP", - description: "From address for outgoing emails", - tags: ["email", "smtp"], - defaultValue: "", - value: "", - }, - { - key: "email.smtp.host", - name: "SMTP Host", - type: "string", - category: "Email / SMTP", - description: "SMTP host", - tags: ["email", "smtp"], - defaultValue: "", - value: "", - }, - { - key: "email.smtp.port", - name: "SMTP Port", - type: "number", - category: "Email / SMTP", - description: "SMTP port", - tags: ["email", "smtp"], - defaultValue: 587, - value: 587, - }, - { - key: "email.smtp.user", - name: "SMTP User", - type: "string", - category: "Email / SMTP", - description: "SMTP username", - tags: ["email", "smtp"], - defaultValue: "", - value: "", - }, - { - key: "email.smtp.password", - name: "SMTP Password", - type: "string", - category: "Email / SMTP", - description: "SMTP password", - tags: ["email", "smtp"], - defaultValue: "", - value: "", - secure: true, - }, - { - key: "email.smtp.enabled", - name: "SMTP Enabled", - type: "boolean", - category: "Email / SMTP", - description: "Enable SMTP for outgoing email", - tags: ["email", "smtp"], - defaultValue: false, - value: false, - }, - { - key: "email.verification.token_ttl_minutes", - name: "Verification Token TTL (minutes)", - type: "number", - category: "Email / Verification", - description: "Minutes until email verification links expire", - tags: ["email", "verification"], - defaultValue: 1440, - value: 1440, - }, - ]; - - const brandingDefaults = { - identity: { title: "DarkAuth", tagline: "Secure Zero-Knowledge Authentication" }, - logo: { data: null, mimeType: null }, - favicon: { data: null, mimeType: null }, - colors: { - backgroundGradientStart: "#f3f4f6", - backgroundGradientEnd: "#eff6ff", - backgroundAngle: "135deg", - primary: "#6600cc", - primaryHover: "#2563eb", - primaryLight: "#dbeafe", - primaryDark: "#1d4ed8", - secondary: "#6b7280", - secondaryHover: "#4b5563", - success: "#10b981", - error: "#ef4444", - warning: "#f59e0b", - info: "#6600cc", - text: "#111827", - textSecondary: "#6b7280", - textMuted: "#9ca3af", - border: "#e5e7eb", - cardBackground: "#ffffff", - cardShadow: "rgba(0,0,0,0.1)", - inputBackground: "#ffffff", - inputBorder: "#d1d5db", - inputFocus: "#6600cc", - }, - colorsDark: { - backgroundGradientStart: "#1f2937", - backgroundGradientEnd: "#111827", - backgroundAngle: "135deg", - primary: "#aec1e0", - primaryHover: "#9eb3d6", - primaryLight: "#374151", - primaryDark: "#c5d3e8", - secondary: "#9ca3af", - secondaryHover: "#d1d5db", - success: "#10b981", - error: "#ef4444", - warning: "#f59e0b", - info: "#aec1e0", - text: "#f9fafb", - textSecondary: "#d1d5db", - textMuted: "#9ca3af", - border: "#374151", - cardBackground: "#1f2937", - cardShadow: "rgba(0,0,0,0.3)", - inputBackground: "#111827", - inputBorder: "#4b5563", - inputFocus: "#aec1e0", - }, - wording: { - welcomeBack: "Welcome back", - createAccount: "Create your account", - email: "Email", - emailPlaceholder: "Enter your email", - password: "Password", - passwordPlaceholder: "Enter your password", - confirmPassword: "Confirm Password", - confirmPasswordPlaceholder: "Confirm your password", - signin: "Continue", - signingIn: "Signing in...", - signup: "Sign up", - signingUp: "Creating account...", - signout: "Sign Out", - changePassword: "Change Password", - cancel: "Cancel", - authorize: "Authorize", - deny: "Deny", - noAccount: "Don't have an account?", - hasAccount: "Already have an account?", - forgotPassword: "Forgot your password?", - signedInAs: "Signed in as", - successAuth: "Successfully authenticated", - errorGeneral: "An error occurred. Please try again.", - errorNetwork: "Network error. Please check your connection.", - errorInvalidCreds: "Invalid email or password.", - authorizeTitle: "Authorize Application", - authorizeDescription: "{app} would like to:", - scopeProfile: "Access your profile information", - scopeEmail: "Access your email address", - scopeOpenid: "Authenticate you", - }, - font: { - family: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", - size: "16px", - weight: { normal: "400", medium: "500", bold: "700" }, - }, - customCSS: "", - }; - - const brandingItems: typeof items = [ - { - key: "branding.identity", - name: "Brand Identity", - type: "object", - category: "Branding/Identity", - description: "Product name and tagline used across user pages", - defaultValue: brandingDefaults.identity, - value: brandingDefaults.identity, - }, - { - key: "branding.logo", - name: "Logo Image", - type: "object", - category: "Branding/Identity", - description: "Base64 logo image and MIME type", - defaultValue: brandingDefaults.logo, - value: brandingDefaults.logo, - }, - { - key: "branding.logo_dark", - name: "Logo Image (Dark)", - type: "object", - category: "Branding/Identity", - description: "Base64 dark mode logo image and MIME type", - defaultValue: brandingDefaults.logo, - value: brandingDefaults.logo, - }, - { - key: "branding.favicon", - name: "Favicon", - type: "object", - category: "Branding/Identity", - description: "Base64 favicon and MIME type", - defaultValue: brandingDefaults.favicon, - value: brandingDefaults.favicon, - }, - { - key: "branding.favicon_dark", - name: "Favicon (Dark)", - type: "object", - category: "Branding/Identity", - description: "Base64 dark mode favicon and MIME type", - defaultValue: brandingDefaults.favicon, - value: brandingDefaults.favicon, - }, - { - key: "branding.colors", - name: "Color Scheme", - type: "object", - category: "Branding/Appearance", - description: "Color palette for user login and consent screens", - defaultValue: brandingDefaults.colors, - value: brandingDefaults.colors, - }, - { - key: "branding.colors_dark", - name: "Color Scheme (Dark)", - type: "object", - category: "Branding/Appearance", - description: "Dark mode color palette for user login and consent screens", - defaultValue: brandingDefaults.colorsDark, - value: brandingDefaults.colorsDark, - }, - { - key: "branding.wording", - name: "UI Text", - type: "object", - category: "Branding/Text", - description: "Text labels used in user flows", - defaultValue: brandingDefaults.wording, - value: brandingDefaults.wording, - }, - { - key: "branding.font", - name: "Typography", - type: "object", - category: "Branding/Appearance", - description: "Font family, base size, and weights", - defaultValue: brandingDefaults.font, - value: brandingDefaults.font, - }, - { - key: "branding.custom_css", - name: "Custom CSS", - type: "string", - category: "Branding/Advanced", - description: "Additional CSS injected into login and consent pages", - defaultValue: brandingDefaults.customCSS, - value: brandingDefaults.customCSS, - }, - ]; - - items.push(...brandingItems); - const { listTemplateDefaults } = await import("./emailTemplates.ts"); - const templateDefaults = listTemplateDefaults(); - for (const [templateKey, template] of Object.entries(templateDefaults)) { - items.push({ - key: `email.templates.${templateKey}`, - name: `${templateKey} Template`, - type: "object", - category: "Email / Templates", - description: `Template for ${templateKey}`, - tags: ["email", "templates"], - defaultValue: template, - value: template, - }); - } - - for (const s of items) { - await context.db - .insert(settings) - .values({ - key: s.key, - name: s.name, - type: s.type, - category: s.category, - description: s.description, - tags: s.tags || [], - defaultValue: s.defaultValue, - value: s.value, - secure: s.secure === true, - updatedAt: new Date(), - }) - .onConflictDoUpdate({ - target: settings.key, - set: { - name: s.name, - type: s.type, - category: s.category, - description: s.description, - tags: s.tags || [], - defaultValue: s.defaultValue, - value: s.value, - secure: s.secure === true, - updatedAt: new Date(), - }, - }); - } - - await pruneDeprecatedSettings(context); -} - -/** - * Load runtime configuration from database settings - * As specified in CORE.md: "all settings in Postgres" - */ export async function loadRuntimeConfig(context: Context): Promise<{ issuer: string; publicOrigin: string;