From 3d33a1c52dc598156125fdeb965ce45a3eef26f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 00:11:25 +0000 Subject: [PATCH 1/7] Initial plan From ebe6288b711b7a5a6ec68f60e290108bf06a5b2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 00:17:41 +0000 Subject: [PATCH 2/7] Add better-auth authentication plugin specification Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../system/AccountLinkingConfig.mdx | 12 + .../references/system/BetterAuthConfig.mdx | 32 + .../system/BetterAuthPluginConfig.mdx | 12 + .../references/system/BetterAuthProvider.mdx | 11 + .../references/system/BetterAuthStrategy.mdx | 13 + content/docs/references/system/CSRFConfig.mdx | 13 + .../references/system/DatabaseAdapter.mdx | 13 + .../references/system/EmailPasswordConfig.mdx | 15 + .../references/system/MagicLinkConfig.mdx | 11 + .../docs/references/system/OAuthProvider.mdx | 17 + .../docs/references/system/PasskeyConfig.mdx | 15 + .../references/system/RateLimitConfig.mdx | 14 + .../docs/references/system/SessionConfig.mdx | 17 + .../references/system/TwoFactorConfig.mdx | 13 + .../references/system/UserFieldMapping.mdx | 16 + .../json-schema/AccountLinkingConfig.json | 27 + .../spec/json-schema/BetterAuthConfig.json | 601 ++++++++++++++ .../json-schema/BetterAuthPluginConfig.json | 28 + .../spec/json-schema/BetterAuthProvider.json | 617 ++++++++++++++ .../spec/json-schema/BetterAuthStrategy.json | 17 + packages/spec/json-schema/CSRFConfig.json | 31 + .../spec/json-schema/DatabaseAdapter.json | 38 + .../spec/json-schema/EmailPasswordConfig.json | 43 + .../spec/json-schema/MagicLinkConfig.json | 21 + packages/spec/json-schema/OAuthProvider.json | 66 ++ packages/spec/json-schema/PasskeyConfig.json | 54 ++ .../spec/json-schema/RateLimitConfig.json | 36 + packages/spec/json-schema/SessionConfig.json | 56 ++ .../spec/json-schema/TwoFactorConfig.json | 40 + .../spec/json-schema/UserFieldMapping.json | 47 ++ packages/spec/src/index.ts | 1 + packages/spec/src/system/auth-better.test.ts | 752 ++++++++++++++++++ packages/spec/src/system/auth-better.zod.ts | 484 +++++++++++ 33 files changed, 3183 insertions(+) create mode 100644 content/docs/references/system/AccountLinkingConfig.mdx create mode 100644 content/docs/references/system/BetterAuthConfig.mdx create mode 100644 content/docs/references/system/BetterAuthPluginConfig.mdx create mode 100644 content/docs/references/system/BetterAuthProvider.mdx create mode 100644 content/docs/references/system/BetterAuthStrategy.mdx create mode 100644 content/docs/references/system/CSRFConfig.mdx create mode 100644 content/docs/references/system/DatabaseAdapter.mdx create mode 100644 content/docs/references/system/EmailPasswordConfig.mdx create mode 100644 content/docs/references/system/MagicLinkConfig.mdx create mode 100644 content/docs/references/system/OAuthProvider.mdx create mode 100644 content/docs/references/system/PasskeyConfig.mdx create mode 100644 content/docs/references/system/RateLimitConfig.mdx create mode 100644 content/docs/references/system/SessionConfig.mdx create mode 100644 content/docs/references/system/TwoFactorConfig.mdx create mode 100644 content/docs/references/system/UserFieldMapping.mdx create mode 100644 packages/spec/json-schema/AccountLinkingConfig.json create mode 100644 packages/spec/json-schema/BetterAuthConfig.json create mode 100644 packages/spec/json-schema/BetterAuthPluginConfig.json create mode 100644 packages/spec/json-schema/BetterAuthProvider.json create mode 100644 packages/spec/json-schema/BetterAuthStrategy.json create mode 100644 packages/spec/json-schema/CSRFConfig.json create mode 100644 packages/spec/json-schema/DatabaseAdapter.json create mode 100644 packages/spec/json-schema/EmailPasswordConfig.json create mode 100644 packages/spec/json-schema/MagicLinkConfig.json create mode 100644 packages/spec/json-schema/OAuthProvider.json create mode 100644 packages/spec/json-schema/PasskeyConfig.json create mode 100644 packages/spec/json-schema/RateLimitConfig.json create mode 100644 packages/spec/json-schema/SessionConfig.json create mode 100644 packages/spec/json-schema/TwoFactorConfig.json create mode 100644 packages/spec/json-schema/UserFieldMapping.json create mode 100644 packages/spec/src/system/auth-better.test.ts create mode 100644 packages/spec/src/system/auth-better.zod.ts diff --git a/content/docs/references/system/AccountLinkingConfig.mdx b/content/docs/references/system/AccountLinkingConfig.mdx new file mode 100644 index 000000000..df6148efd --- /dev/null +++ b/content/docs/references/system/AccountLinkingConfig.mdx @@ -0,0 +1,12 @@ +--- +title: AccountLinkingConfig +description: AccountLinkingConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | Allow account linking | +| **autoLink** | `boolean` | optional | Automatically link accounts with same email | +| **requireVerification** | `boolean` | optional | Require email verification before linking | diff --git a/content/docs/references/system/BetterAuthConfig.mdx b/content/docs/references/system/BetterAuthConfig.mdx new file mode 100644 index 000000000..4757d17d9 --- /dev/null +++ b/content/docs/references/system/BetterAuthConfig.mdx @@ -0,0 +1,32 @@ +--- +title: BetterAuthConfig +description: BetterAuthConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Configuration name (snake_case) | +| **label** | `string` | ✅ | Display label | +| **strategies** | `Enum<'email_password' \| 'magic_link' \| 'oauth' \| 'passkey' \| 'otp' \| 'anonymous'>[]` | ✅ | Enabled authentication strategies | +| **baseUrl** | `string` | ✅ | Application base URL | +| **secret** | `string` | ✅ | Secret key for signing (min 32 chars) | +| **emailPassword** | `object` | optional | | +| **magicLink** | `object` | optional | | +| **passkey** | `object` | optional | | +| **oauth** | `object` | optional | | +| **session** | `object` | optional | | +| **rateLimit** | `object` | optional | | +| **csrf** | `object` | optional | | +| **accountLinking** | `object` | optional | | +| **twoFactor** | `object` | optional | | +| **userFieldMapping** | `object` | optional | | +| **database** | `object` | optional | | +| **plugins** | `object[]` | optional | | +| **hooks** | `object` | optional | Authentication lifecycle hooks | +| **security** | `object` | optional | Advanced security settings | +| **email** | `object` | optional | Email configuration | +| **ui** | `object` | optional | UI customization | +| **active** | `boolean` | optional | Whether this provider is active | +| **allowRegistration** | `boolean` | optional | Allow new user registration | diff --git a/content/docs/references/system/BetterAuthPluginConfig.mdx b/content/docs/references/system/BetterAuthPluginConfig.mdx new file mode 100644 index 000000000..976dda763 --- /dev/null +++ b/content/docs/references/system/BetterAuthPluginConfig.mdx @@ -0,0 +1,12 @@ +--- +title: BetterAuthPluginConfig +description: BetterAuthPluginConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Plugin name | +| **enabled** | `boolean` | optional | | +| **options** | `Record` | optional | Plugin-specific options | diff --git a/content/docs/references/system/BetterAuthProvider.mdx b/content/docs/references/system/BetterAuthProvider.mdx new file mode 100644 index 000000000..2673e536d --- /dev/null +++ b/content/docs/references/system/BetterAuthProvider.mdx @@ -0,0 +1,11 @@ +--- +title: BetterAuthProvider +description: BetterAuthProvider Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Provider type identifier | +| **config** | `object` | ✅ | Better-auth configuration | diff --git a/content/docs/references/system/BetterAuthStrategy.mdx b/content/docs/references/system/BetterAuthStrategy.mdx new file mode 100644 index 000000000..834e2d0af --- /dev/null +++ b/content/docs/references/system/BetterAuthStrategy.mdx @@ -0,0 +1,13 @@ +--- +title: BetterAuthStrategy +description: BetterAuthStrategy Schema Reference +--- + +## Allowed Values + +* `email_password` +* `magic_link` +* `oauth` +* `passkey` +* `otp` +* `anonymous` \ No newline at end of file diff --git a/content/docs/references/system/CSRFConfig.mdx b/content/docs/references/system/CSRFConfig.mdx new file mode 100644 index 000000000..eba2dddf6 --- /dev/null +++ b/content/docs/references/system/CSRFConfig.mdx @@ -0,0 +1,13 @@ +--- +title: CSRFConfig +description: CSRFConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **tokenLength** | `number` | optional | CSRF token length | +| **cookieName** | `string` | optional | CSRF cookie name | +| **headerName** | `string` | optional | CSRF header name | diff --git a/content/docs/references/system/DatabaseAdapter.mdx b/content/docs/references/system/DatabaseAdapter.mdx new file mode 100644 index 000000000..804f75b03 --- /dev/null +++ b/content/docs/references/system/DatabaseAdapter.mdx @@ -0,0 +1,13 @@ +--- +title: DatabaseAdapter +description: DatabaseAdapter Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'prisma' \| 'drizzle' \| 'kysely' \| 'custom'>` | ✅ | Database adapter type | +| **connectionString** | `string` | optional | Database connection string | +| **tablePrefix** | `string` | optional | Prefix for auth tables | +| **schema** | `string` | optional | Database schema name | diff --git a/content/docs/references/system/EmailPasswordConfig.mdx b/content/docs/references/system/EmailPasswordConfig.mdx new file mode 100644 index 000000000..ed35be72f --- /dev/null +++ b/content/docs/references/system/EmailPasswordConfig.mdx @@ -0,0 +1,15 @@ +--- +title: EmailPasswordConfig +description: EmailPasswordConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **requireEmailVerification** | `boolean` | optional | Require email verification before login | +| **minPasswordLength** | `number` | optional | Minimum password length | +| **requirePasswordComplexity** | `boolean` | optional | Require uppercase, lowercase, numbers, symbols | +| **allowPasswordReset** | `boolean` | optional | Enable password reset functionality | +| **passwordResetExpiry** | `number` | optional | Password reset token expiry in seconds | diff --git a/content/docs/references/system/MagicLinkConfig.mdx b/content/docs/references/system/MagicLinkConfig.mdx new file mode 100644 index 000000000..2971b2862 --- /dev/null +++ b/content/docs/references/system/MagicLinkConfig.mdx @@ -0,0 +1,11 @@ +--- +title: MagicLinkConfig +description: MagicLinkConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **expiryTime** | `number` | optional | Magic link expiry time in seconds (default 15 min) | diff --git a/content/docs/references/system/OAuthProvider.mdx b/content/docs/references/system/OAuthProvider.mdx new file mode 100644 index 000000000..32ff86e83 --- /dev/null +++ b/content/docs/references/system/OAuthProvider.mdx @@ -0,0 +1,17 @@ +--- +title: OAuthProvider +description: OAuthProvider Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `Enum<'google' \| 'github' \| 'facebook' \| 'twitter' \| 'linkedin' \| 'microsoft' \| 'apple' \| 'discord' \| 'gitlab' \| 'custom'>` | ✅ | OAuth provider type | +| **clientId** | `string` | ✅ | OAuth client ID | +| **clientSecret** | `string` | ✅ | OAuth client secret (typically from ENV) | +| **scopes** | `string[]` | optional | Requested OAuth scopes | +| **redirectUri** | `string` | optional | OAuth callback URL | +| **enabled** | `boolean` | optional | Whether this provider is enabled | +| **displayName** | `string` | optional | Display name for the provider button | +| **icon** | `string` | optional | Icon URL or identifier | diff --git a/content/docs/references/system/PasskeyConfig.mdx b/content/docs/references/system/PasskeyConfig.mdx new file mode 100644 index 000000000..9e305ed4c --- /dev/null +++ b/content/docs/references/system/PasskeyConfig.mdx @@ -0,0 +1,15 @@ +--- +title: PasskeyConfig +description: PasskeyConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **rpName** | `string` | ✅ | Relying Party name | +| **rpId** | `string` | optional | Relying Party ID (defaults to domain) | +| **allowedOrigins** | `string[]` | optional | Allowed origins for WebAuthn | +| **userVerification** | `Enum<'required' \| 'preferred' \| 'discouraged'>` | optional | | +| **attestation** | `Enum<'none' \| 'indirect' \| 'direct' \| 'enterprise'>` | optional | | diff --git a/content/docs/references/system/RateLimitConfig.mdx b/content/docs/references/system/RateLimitConfig.mdx new file mode 100644 index 000000000..6682505c5 --- /dev/null +++ b/content/docs/references/system/RateLimitConfig.mdx @@ -0,0 +1,14 @@ +--- +title: RateLimitConfig +description: RateLimitConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **maxAttempts** | `number` | optional | Maximum login attempts | +| **windowMs** | `number` | optional | Time window in milliseconds (default 15 min) | +| **blockDuration** | `number` | optional | Block duration after max attempts in ms | +| **skipSuccessfulRequests** | `boolean` | optional | Only count failed requests | diff --git a/content/docs/references/system/SessionConfig.mdx b/content/docs/references/system/SessionConfig.mdx new file mode 100644 index 000000000..5fb4b1c25 --- /dev/null +++ b/content/docs/references/system/SessionConfig.mdx @@ -0,0 +1,17 @@ +--- +title: SessionConfig +description: SessionConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **expiresIn** | `number` | optional | Session expiry in seconds (default 7 days) | +| **updateAge** | `number` | optional | Session update interval in seconds (default 1 day) | +| **cookieName** | `string` | optional | Session cookie name | +| **cookieSecure** | `boolean` | optional | Use secure cookies (HTTPS only) | +| **cookieSameSite** | `Enum<'strict' \| 'lax' \| 'none'>` | optional | SameSite cookie attribute | +| **cookieDomain** | `string` | optional | Cookie domain | +| **cookiePath** | `string` | optional | Cookie path | +| **cookieHttpOnly** | `boolean` | optional | HttpOnly cookie attribute | diff --git a/content/docs/references/system/TwoFactorConfig.mdx b/content/docs/references/system/TwoFactorConfig.mdx new file mode 100644 index 000000000..70add73db --- /dev/null +++ b/content/docs/references/system/TwoFactorConfig.mdx @@ -0,0 +1,13 @@ +--- +title: TwoFactorConfig +description: TwoFactorConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **issuer** | `string` | optional | TOTP issuer name | +| **qrCodeSize** | `number` | optional | QR code size in pixels | +| **backupCodes** | `object` | optional | | diff --git a/content/docs/references/system/UserFieldMapping.mdx b/content/docs/references/system/UserFieldMapping.mdx new file mode 100644 index 000000000..bfbf94d14 --- /dev/null +++ b/content/docs/references/system/UserFieldMapping.mdx @@ -0,0 +1,16 @@ +--- +title: UserFieldMapping +description: UserFieldMapping Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | optional | User ID field | +| **email** | `string` | optional | Email field | +| **name** | `string` | optional | Name field | +| **image** | `string` | optional | Profile image field | +| **emailVerified** | `string` | optional | Email verification status field | +| **createdAt** | `string` | optional | Created timestamp field | +| **updatedAt** | `string` | optional | Updated timestamp field | diff --git a/packages/spec/json-schema/AccountLinkingConfig.json b/packages/spec/json-schema/AccountLinkingConfig.json new file mode 100644 index 000000000..f635ac7cc --- /dev/null +++ b/packages/spec/json-schema/AccountLinkingConfig.json @@ -0,0 +1,27 @@ +{ + "$ref": "#/definitions/AccountLinkingConfig", + "definitions": { + "AccountLinkingConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow account linking" + }, + "autoLink": { + "type": "boolean", + "default": false, + "description": "Automatically link accounts with same email" + }, + "requireVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before linking" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/BetterAuthConfig.json b/packages/spec/json-schema/BetterAuthConfig.json new file mode 100644 index 000000000..c486939eb --- /dev/null +++ b/packages/spec/json-schema/BetterAuthConfig.json @@ -0,0 +1,601 @@ +{ + "$ref": "#/definitions/BetterAuthConfig", + "definitions": { + "BetterAuthConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Configuration name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label" + }, + "strategies": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email_password", + "magic_link", + "oauth", + "passkey", + "otp", + "anonymous" + ] + }, + "minItems": 1, + "description": "Enabled authentication strategies" + }, + "baseUrl": { + "type": "string", + "format": "uri", + "description": "Application base URL" + }, + "secret": { + "type": "string", + "minLength": 32, + "description": "Secret key for signing (min 32 chars)" + }, + "emailPassword": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "requireEmailVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before login" + }, + "minPasswordLength": { + "type": "number", + "minimum": 6, + "maximum": 128, + "default": 8, + "description": "Minimum password length" + }, + "requirePasswordComplexity": { + "type": "boolean", + "default": true, + "description": "Require uppercase, lowercase, numbers, symbols" + }, + "allowPasswordReset": { + "type": "boolean", + "default": true, + "description": "Enable password reset functionality" + }, + "passwordResetExpiry": { + "type": "number", + "default": 3600, + "description": "Password reset token expiry in seconds" + } + }, + "additionalProperties": false + }, + "magicLink": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "expiryTime": { + "type": "number", + "default": 900, + "description": "Magic link expiry time in seconds (default 15 min)" + } + }, + "additionalProperties": false + }, + "passkey": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "rpName": { + "type": "string", + "description": "Relying Party name" + }, + "rpId": { + "type": "string", + "description": "Relying Party ID (defaults to domain)" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Allowed origins for WebAuthn" + }, + "userVerification": { + "type": "string", + "enum": [ + "required", + "preferred", + "discouraged" + ], + "default": "preferred" + }, + "attestation": { + "type": "string", + "enum": [ + "none", + "indirect", + "direct", + "enterprise" + ], + "default": "none" + } + }, + "required": [ + "rpName" + ], + "additionalProperties": false + }, + "oauth": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google", + "github", + "facebook", + "twitter", + "linkedin", + "microsoft", + "apple", + "discord", + "gitlab", + "custom" + ], + "description": "OAuth provider type" + }, + "clientId": { + "type": "string", + "description": "OAuth client ID" + }, + "clientSecret": { + "type": "string", + "description": "OAuth client secret (typically from ENV)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Requested OAuth scopes" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "description": "OAuth callback URL" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this provider is enabled" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "provider", + "clientId", + "clientSecret" + ], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": [ + "providers" + ], + "additionalProperties": false + }, + "session": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number", + "default": 604800, + "description": "Session expiry in seconds (default 7 days)" + }, + "updateAge": { + "type": "number", + "default": 86400, + "description": "Session update interval in seconds (default 1 day)" + }, + "cookieName": { + "type": "string", + "default": "better-auth.session_token", + "description": "Session cookie name" + }, + "cookieSecure": { + "type": "boolean", + "default": true, + "description": "Use secure cookies (HTTPS only)" + }, + "cookieSameSite": { + "type": "string", + "enum": [ + "strict", + "lax", + "none" + ], + "default": "lax", + "description": "SameSite cookie attribute" + }, + "cookieDomain": { + "type": "string", + "description": "Cookie domain" + }, + "cookiePath": { + "type": "string", + "default": "/", + "description": "Cookie path" + }, + "cookieHttpOnly": { + "type": "boolean", + "default": true, + "description": "HttpOnly cookie attribute" + } + }, + "additionalProperties": false, + "default": {} + }, + "rateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "maxAttempts": { + "type": "number", + "default": 5, + "description": "Maximum login attempts" + }, + "windowMs": { + "type": "number", + "default": 900000, + "description": "Time window in milliseconds (default 15 min)" + }, + "blockDuration": { + "type": "number", + "default": 900000, + "description": "Block duration after max attempts in ms" + }, + "skipSuccessfulRequests": { + "type": "boolean", + "default": false, + "description": "Only count failed requests" + } + }, + "additionalProperties": false, + "default": {} + }, + "csrf": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "tokenLength": { + "type": "number", + "default": 32, + "description": "CSRF token length" + }, + "cookieName": { + "type": "string", + "default": "better-auth.csrf_token", + "description": "CSRF cookie name" + }, + "headerName": { + "type": "string", + "default": "X-CSRF-Token", + "description": "CSRF header name" + } + }, + "additionalProperties": false, + "default": {} + }, + "accountLinking": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow account linking" + }, + "autoLink": { + "type": "boolean", + "default": false, + "description": "Automatically link accounts with same email" + }, + "requireVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before linking" + } + }, + "additionalProperties": false, + "default": {} + }, + "twoFactor": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "description": "TOTP issuer name" + }, + "qrCodeSize": { + "type": "number", + "default": 200, + "description": "QR code size in pixels" + }, + "backupCodes": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "count": { + "type": "number", + "default": 10, + "description": "Number of backup codes to generate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "userFieldMapping": { + "type": "object", + "properties": { + "id": { + "type": "string", + "default": "id", + "description": "User ID field" + }, + "email": { + "type": "string", + "default": "email", + "description": "Email field" + }, + "name": { + "type": "string", + "default": "name", + "description": "Name field" + }, + "image": { + "type": "string", + "default": "image", + "description": "Profile image field" + }, + "emailVerified": { + "type": "string", + "default": "email_verified", + "description": "Email verification status field" + }, + "createdAt": { + "type": "string", + "default": "created_at", + "description": "Created timestamp field" + }, + "updatedAt": { + "type": "string", + "default": "updated_at", + "description": "Updated timestamp field" + } + }, + "additionalProperties": false, + "default": {} + }, + "database": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "prisma", + "drizzle", + "kysely", + "custom" + ], + "description": "Database adapter type" + }, + "connectionString": { + "type": "string", + "description": "Database connection string" + }, + "tablePrefix": { + "type": "string", + "default": "auth_", + "description": "Prefix for auth tables" + }, + "schema": { + "type": "string", + "description": "Database schema name" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "plugins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Plugin name" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "additionalProperties": {}, + "description": "Plugin-specific options" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "default": [] + }, + "hooks": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "description": "Authentication lifecycle hooks" + }, + "security": { + "type": "object", + "properties": { + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed origins" + }, + "trustProxy": { + "type": "boolean", + "default": false, + "description": "Trust proxy headers" + }, + "ipRateLimiting": { + "type": "boolean", + "default": true, + "description": "Enable IP-based rate limiting" + }, + "sessionFingerprinting": { + "type": "boolean", + "default": true, + "description": "Enable session fingerprinting" + }, + "maxSessions": { + "type": "number", + "default": 5, + "description": "Maximum concurrent sessions per user" + } + }, + "additionalProperties": false, + "description": "Advanced security settings" + }, + "email": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "email", + "description": "From email address" + }, + "fromName": { + "type": "string", + "description": "From name" + }, + "provider": { + "type": "string", + "enum": [ + "smtp", + "sendgrid", + "mailgun", + "ses", + "resend", + "custom" + ], + "description": "Email provider" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific configuration" + } + }, + "required": [ + "from", + "provider" + ], + "additionalProperties": false, + "description": "Email configuration" + }, + "ui": { + "type": "object", + "properties": { + "brandName": { + "type": "string", + "description": "Brand name displayed in auth UI" + }, + "logo": { + "type": "string", + "description": "Logo URL" + }, + "primaryColor": { + "type": "string", + "description": "Primary brand color (hex)" + }, + "customCss": { + "type": "string", + "description": "Custom CSS for auth pages" + } + }, + "additionalProperties": false, + "description": "UI customization" + }, + "active": { + "type": "boolean", + "default": true, + "description": "Whether this provider is active" + }, + "allowRegistration": { + "type": "boolean", + "default": true, + "description": "Allow new user registration" + } + }, + "required": [ + "name", + "label", + "strategies", + "baseUrl", + "secret" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/BetterAuthPluginConfig.json b/packages/spec/json-schema/BetterAuthPluginConfig.json new file mode 100644 index 000000000..20aed5a85 --- /dev/null +++ b/packages/spec/json-schema/BetterAuthPluginConfig.json @@ -0,0 +1,28 @@ +{ + "$ref": "#/definitions/BetterAuthPluginConfig", + "definitions": { + "BetterAuthPluginConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Plugin name" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "additionalProperties": {}, + "description": "Plugin-specific options" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/BetterAuthProvider.json b/packages/spec/json-schema/BetterAuthProvider.json new file mode 100644 index 000000000..3d32dfbd5 --- /dev/null +++ b/packages/spec/json-schema/BetterAuthProvider.json @@ -0,0 +1,617 @@ +{ + "$ref": "#/definitions/BetterAuthProvider", + "definitions": { + "BetterAuthProvider": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "better_auth", + "description": "Provider type identifier" + }, + "config": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Configuration name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label" + }, + "strategies": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email_password", + "magic_link", + "oauth", + "passkey", + "otp", + "anonymous" + ] + }, + "minItems": 1, + "description": "Enabled authentication strategies" + }, + "baseUrl": { + "type": "string", + "format": "uri", + "description": "Application base URL" + }, + "secret": { + "type": "string", + "minLength": 32, + "description": "Secret key for signing (min 32 chars)" + }, + "emailPassword": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "requireEmailVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before login" + }, + "minPasswordLength": { + "type": "number", + "minimum": 6, + "maximum": 128, + "default": 8, + "description": "Minimum password length" + }, + "requirePasswordComplexity": { + "type": "boolean", + "default": true, + "description": "Require uppercase, lowercase, numbers, symbols" + }, + "allowPasswordReset": { + "type": "boolean", + "default": true, + "description": "Enable password reset functionality" + }, + "passwordResetExpiry": { + "type": "number", + "default": 3600, + "description": "Password reset token expiry in seconds" + } + }, + "additionalProperties": false + }, + "magicLink": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "expiryTime": { + "type": "number", + "default": 900, + "description": "Magic link expiry time in seconds (default 15 min)" + } + }, + "additionalProperties": false + }, + "passkey": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "rpName": { + "type": "string", + "description": "Relying Party name" + }, + "rpId": { + "type": "string", + "description": "Relying Party ID (defaults to domain)" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Allowed origins for WebAuthn" + }, + "userVerification": { + "type": "string", + "enum": [ + "required", + "preferred", + "discouraged" + ], + "default": "preferred" + }, + "attestation": { + "type": "string", + "enum": [ + "none", + "indirect", + "direct", + "enterprise" + ], + "default": "none" + } + }, + "required": [ + "rpName" + ], + "additionalProperties": false + }, + "oauth": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google", + "github", + "facebook", + "twitter", + "linkedin", + "microsoft", + "apple", + "discord", + "gitlab", + "custom" + ], + "description": "OAuth provider type" + }, + "clientId": { + "type": "string", + "description": "OAuth client ID" + }, + "clientSecret": { + "type": "string", + "description": "OAuth client secret (typically from ENV)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Requested OAuth scopes" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "description": "OAuth callback URL" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this provider is enabled" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "provider", + "clientId", + "clientSecret" + ], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": [ + "providers" + ], + "additionalProperties": false + }, + "session": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number", + "default": 604800, + "description": "Session expiry in seconds (default 7 days)" + }, + "updateAge": { + "type": "number", + "default": 86400, + "description": "Session update interval in seconds (default 1 day)" + }, + "cookieName": { + "type": "string", + "default": "better-auth.session_token", + "description": "Session cookie name" + }, + "cookieSecure": { + "type": "boolean", + "default": true, + "description": "Use secure cookies (HTTPS only)" + }, + "cookieSameSite": { + "type": "string", + "enum": [ + "strict", + "lax", + "none" + ], + "default": "lax", + "description": "SameSite cookie attribute" + }, + "cookieDomain": { + "type": "string", + "description": "Cookie domain" + }, + "cookiePath": { + "type": "string", + "default": "/", + "description": "Cookie path" + }, + "cookieHttpOnly": { + "type": "boolean", + "default": true, + "description": "HttpOnly cookie attribute" + } + }, + "additionalProperties": false, + "default": {} + }, + "rateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "maxAttempts": { + "type": "number", + "default": 5, + "description": "Maximum login attempts" + }, + "windowMs": { + "type": "number", + "default": 900000, + "description": "Time window in milliseconds (default 15 min)" + }, + "blockDuration": { + "type": "number", + "default": 900000, + "description": "Block duration after max attempts in ms" + }, + "skipSuccessfulRequests": { + "type": "boolean", + "default": false, + "description": "Only count failed requests" + } + }, + "additionalProperties": false, + "default": {} + }, + "csrf": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "tokenLength": { + "type": "number", + "default": 32, + "description": "CSRF token length" + }, + "cookieName": { + "type": "string", + "default": "better-auth.csrf_token", + "description": "CSRF cookie name" + }, + "headerName": { + "type": "string", + "default": "X-CSRF-Token", + "description": "CSRF header name" + } + }, + "additionalProperties": false, + "default": {} + }, + "accountLinking": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow account linking" + }, + "autoLink": { + "type": "boolean", + "default": false, + "description": "Automatically link accounts with same email" + }, + "requireVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before linking" + } + }, + "additionalProperties": false, + "default": {} + }, + "twoFactor": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "description": "TOTP issuer name" + }, + "qrCodeSize": { + "type": "number", + "default": 200, + "description": "QR code size in pixels" + }, + "backupCodes": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "count": { + "type": "number", + "default": 10, + "description": "Number of backup codes to generate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "userFieldMapping": { + "type": "object", + "properties": { + "id": { + "type": "string", + "default": "id", + "description": "User ID field" + }, + "email": { + "type": "string", + "default": "email", + "description": "Email field" + }, + "name": { + "type": "string", + "default": "name", + "description": "Name field" + }, + "image": { + "type": "string", + "default": "image", + "description": "Profile image field" + }, + "emailVerified": { + "type": "string", + "default": "email_verified", + "description": "Email verification status field" + }, + "createdAt": { + "type": "string", + "default": "created_at", + "description": "Created timestamp field" + }, + "updatedAt": { + "type": "string", + "default": "updated_at", + "description": "Updated timestamp field" + } + }, + "additionalProperties": false, + "default": {} + }, + "database": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "prisma", + "drizzle", + "kysely", + "custom" + ], + "description": "Database adapter type" + }, + "connectionString": { + "type": "string", + "description": "Database connection string" + }, + "tablePrefix": { + "type": "string", + "default": "auth_", + "description": "Prefix for auth tables" + }, + "schema": { + "type": "string", + "description": "Database schema name" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "plugins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Plugin name" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "additionalProperties": {}, + "description": "Plugin-specific options" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "default": [] + }, + "hooks": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "description": "Authentication lifecycle hooks" + }, + "security": { + "type": "object", + "properties": { + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed origins" + }, + "trustProxy": { + "type": "boolean", + "default": false, + "description": "Trust proxy headers" + }, + "ipRateLimiting": { + "type": "boolean", + "default": true, + "description": "Enable IP-based rate limiting" + }, + "sessionFingerprinting": { + "type": "boolean", + "default": true, + "description": "Enable session fingerprinting" + }, + "maxSessions": { + "type": "number", + "default": 5, + "description": "Maximum concurrent sessions per user" + } + }, + "additionalProperties": false, + "description": "Advanced security settings" + }, + "email": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "email", + "description": "From email address" + }, + "fromName": { + "type": "string", + "description": "From name" + }, + "provider": { + "type": "string", + "enum": [ + "smtp", + "sendgrid", + "mailgun", + "ses", + "resend", + "custom" + ], + "description": "Email provider" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific configuration" + } + }, + "required": [ + "from", + "provider" + ], + "additionalProperties": false, + "description": "Email configuration" + }, + "ui": { + "type": "object", + "properties": { + "brandName": { + "type": "string", + "description": "Brand name displayed in auth UI" + }, + "logo": { + "type": "string", + "description": "Logo URL" + }, + "primaryColor": { + "type": "string", + "description": "Primary brand color (hex)" + }, + "customCss": { + "type": "string", + "description": "Custom CSS for auth pages" + } + }, + "additionalProperties": false, + "description": "UI customization" + }, + "active": { + "type": "boolean", + "default": true, + "description": "Whether this provider is active" + }, + "allowRegistration": { + "type": "boolean", + "default": true, + "description": "Allow new user registration" + } + }, + "required": [ + "name", + "label", + "strategies", + "baseUrl", + "secret" + ], + "additionalProperties": false, + "description": "Better-auth configuration" + } + }, + "required": [ + "type", + "config" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/BetterAuthStrategy.json b/packages/spec/json-schema/BetterAuthStrategy.json new file mode 100644 index 000000000..593ac2235 --- /dev/null +++ b/packages/spec/json-schema/BetterAuthStrategy.json @@ -0,0 +1,17 @@ +{ + "$ref": "#/definitions/BetterAuthStrategy", + "definitions": { + "BetterAuthStrategy": { + "type": "string", + "enum": [ + "email_password", + "magic_link", + "oauth", + "passkey", + "otp", + "anonymous" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/CSRFConfig.json b/packages/spec/json-schema/CSRFConfig.json new file mode 100644 index 000000000..eaed24aff --- /dev/null +++ b/packages/spec/json-schema/CSRFConfig.json @@ -0,0 +1,31 @@ +{ + "$ref": "#/definitions/CSRFConfig", + "definitions": { + "CSRFConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "tokenLength": { + "type": "number", + "default": 32, + "description": "CSRF token length" + }, + "cookieName": { + "type": "string", + "default": "better-auth.csrf_token", + "description": "CSRF cookie name" + }, + "headerName": { + "type": "string", + "default": "X-CSRF-Token", + "description": "CSRF header name" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/DatabaseAdapter.json b/packages/spec/json-schema/DatabaseAdapter.json new file mode 100644 index 000000000..4695c1a23 --- /dev/null +++ b/packages/spec/json-schema/DatabaseAdapter.json @@ -0,0 +1,38 @@ +{ + "$ref": "#/definitions/DatabaseAdapter", + "definitions": { + "DatabaseAdapter": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "prisma", + "drizzle", + "kysely", + "custom" + ], + "description": "Database adapter type" + }, + "connectionString": { + "type": "string", + "description": "Database connection string" + }, + "tablePrefix": { + "type": "string", + "default": "auth_", + "description": "Prefix for auth tables" + }, + "schema": { + "type": "string", + "description": "Database schema name" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/EmailPasswordConfig.json b/packages/spec/json-schema/EmailPasswordConfig.json new file mode 100644 index 000000000..9bb0c155b --- /dev/null +++ b/packages/spec/json-schema/EmailPasswordConfig.json @@ -0,0 +1,43 @@ +{ + "$ref": "#/definitions/EmailPasswordConfig", + "definitions": { + "EmailPasswordConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "requireEmailVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before login" + }, + "minPasswordLength": { + "type": "number", + "minimum": 6, + "maximum": 128, + "default": 8, + "description": "Minimum password length" + }, + "requirePasswordComplexity": { + "type": "boolean", + "default": true, + "description": "Require uppercase, lowercase, numbers, symbols" + }, + "allowPasswordReset": { + "type": "boolean", + "default": true, + "description": "Enable password reset functionality" + }, + "passwordResetExpiry": { + "type": "number", + "default": 3600, + "description": "Password reset token expiry in seconds" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/MagicLinkConfig.json b/packages/spec/json-schema/MagicLinkConfig.json new file mode 100644 index 000000000..937680a51 --- /dev/null +++ b/packages/spec/json-schema/MagicLinkConfig.json @@ -0,0 +1,21 @@ +{ + "$ref": "#/definitions/MagicLinkConfig", + "definitions": { + "MagicLinkConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "expiryTime": { + "type": "number", + "default": 900, + "description": "Magic link expiry time in seconds (default 15 min)" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/OAuthProvider.json b/packages/spec/json-schema/OAuthProvider.json new file mode 100644 index 000000000..738d846ad --- /dev/null +++ b/packages/spec/json-schema/OAuthProvider.json @@ -0,0 +1,66 @@ +{ + "$ref": "#/definitions/OAuthProvider", + "definitions": { + "OAuthProvider": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google", + "github", + "facebook", + "twitter", + "linkedin", + "microsoft", + "apple", + "discord", + "gitlab", + "custom" + ], + "description": "OAuth provider type" + }, + "clientId": { + "type": "string", + "description": "OAuth client ID" + }, + "clientSecret": { + "type": "string", + "description": "OAuth client secret (typically from ENV)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Requested OAuth scopes" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "description": "OAuth callback URL" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this provider is enabled" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "provider", + "clientId", + "clientSecret" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/PasskeyConfig.json b/packages/spec/json-schema/PasskeyConfig.json new file mode 100644 index 000000000..335a54344 --- /dev/null +++ b/packages/spec/json-schema/PasskeyConfig.json @@ -0,0 +1,54 @@ +{ + "$ref": "#/definitions/PasskeyConfig", + "definitions": { + "PasskeyConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "rpName": { + "type": "string", + "description": "Relying Party name" + }, + "rpId": { + "type": "string", + "description": "Relying Party ID (defaults to domain)" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Allowed origins for WebAuthn" + }, + "userVerification": { + "type": "string", + "enum": [ + "required", + "preferred", + "discouraged" + ], + "default": "preferred" + }, + "attestation": { + "type": "string", + "enum": [ + "none", + "indirect", + "direct", + "enterprise" + ], + "default": "none" + } + }, + "required": [ + "rpName" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/RateLimitConfig.json b/packages/spec/json-schema/RateLimitConfig.json new file mode 100644 index 000000000..d8620aadb --- /dev/null +++ b/packages/spec/json-schema/RateLimitConfig.json @@ -0,0 +1,36 @@ +{ + "$ref": "#/definitions/RateLimitConfig", + "definitions": { + "RateLimitConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "maxAttempts": { + "type": "number", + "default": 5, + "description": "Maximum login attempts" + }, + "windowMs": { + "type": "number", + "default": 900000, + "description": "Time window in milliseconds (default 15 min)" + }, + "blockDuration": { + "type": "number", + "default": 900000, + "description": "Block duration after max attempts in ms" + }, + "skipSuccessfulRequests": { + "type": "boolean", + "default": false, + "description": "Only count failed requests" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/SessionConfig.json b/packages/spec/json-schema/SessionConfig.json new file mode 100644 index 000000000..8aea856bf --- /dev/null +++ b/packages/spec/json-schema/SessionConfig.json @@ -0,0 +1,56 @@ +{ + "$ref": "#/definitions/SessionConfig", + "definitions": { + "SessionConfig": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number", + "default": 604800, + "description": "Session expiry in seconds (default 7 days)" + }, + "updateAge": { + "type": "number", + "default": 86400, + "description": "Session update interval in seconds (default 1 day)" + }, + "cookieName": { + "type": "string", + "default": "better-auth.session_token", + "description": "Session cookie name" + }, + "cookieSecure": { + "type": "boolean", + "default": true, + "description": "Use secure cookies (HTTPS only)" + }, + "cookieSameSite": { + "type": "string", + "enum": [ + "strict", + "lax", + "none" + ], + "default": "lax", + "description": "SameSite cookie attribute" + }, + "cookieDomain": { + "type": "string", + "description": "Cookie domain" + }, + "cookiePath": { + "type": "string", + "default": "/", + "description": "Cookie path" + }, + "cookieHttpOnly": { + "type": "boolean", + "default": true, + "description": "HttpOnly cookie attribute" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/TwoFactorConfig.json b/packages/spec/json-schema/TwoFactorConfig.json new file mode 100644 index 000000000..488c80a3b --- /dev/null +++ b/packages/spec/json-schema/TwoFactorConfig.json @@ -0,0 +1,40 @@ +{ + "$ref": "#/definitions/TwoFactorConfig", + "definitions": { + "TwoFactorConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "description": "TOTP issuer name" + }, + "qrCodeSize": { + "type": "number", + "default": 200, + "description": "QR code size in pixels" + }, + "backupCodes": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "count": { + "type": "number", + "default": 10, + "description": "Number of backup codes to generate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/UserFieldMapping.json b/packages/spec/json-schema/UserFieldMapping.json new file mode 100644 index 000000000..a07a8458c --- /dev/null +++ b/packages/spec/json-schema/UserFieldMapping.json @@ -0,0 +1,47 @@ +{ + "$ref": "#/definitions/UserFieldMapping", + "definitions": { + "UserFieldMapping": { + "type": "object", + "properties": { + "id": { + "type": "string", + "default": "id", + "description": "User ID field" + }, + "email": { + "type": "string", + "default": "email", + "description": "Email field" + }, + "name": { + "type": "string", + "default": "name", + "description": "Name field" + }, + "image": { + "type": "string", + "default": "image", + "description": "Profile image field" + }, + "emailVerified": { + "type": "string", + "default": "email_verified", + "description": "Email verification status field" + }, + "createdAt": { + "type": "string", + "default": "created_at", + "description": "Created timestamp field" + }, + "updatedAt": { + "type": "string", + "default": "updated_at", + "description": "Updated timestamp field" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/src/index.ts b/packages/spec/src/index.ts index 80f8750d1..e5c29adba 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -41,6 +41,7 @@ export * from './system/manifest.zod'; export * from './system/datasource.zod'; export * from './system/api.zod'; export * from './system/identity.zod'; +export * from './system/auth-better.zod'; export * from './system/policy.zod'; export * from './system/role.zod'; export * from './system/territory.zod'; diff --git a/packages/spec/src/system/auth-better.test.ts b/packages/spec/src/system/auth-better.test.ts new file mode 100644 index 000000000..52321a5b1 --- /dev/null +++ b/packages/spec/src/system/auth-better.test.ts @@ -0,0 +1,752 @@ +import { describe, it, expect } from 'vitest'; +import { + BetterAuthStrategy, + OAuthProviderSchema, + EmailPasswordConfigSchema, + MagicLinkConfigSchema, + PasskeyConfigSchema, + SessionConfigSchema, + RateLimitConfigSchema, + CSRFConfigSchema, + AccountLinkingConfigSchema, + TwoFactorConfigSchema, + UserFieldMappingSchema, + DatabaseAdapterSchema, + BetterAuthPluginConfigSchema, + BetterAuthConfigSchema, + BetterAuthProviderSchema, + type BetterAuthConfig, + type BetterAuthProvider, + type OAuthProvider, +} from './auth-better.zod'; + +describe('BetterAuthStrategy', () => { + it('should accept valid authentication strategies', () => { + const strategies = [ + 'email_password', + 'magic_link', + 'oauth', + 'passkey', + 'otp', + 'anonymous', + ]; + + strategies.forEach((strategy) => { + expect(() => BetterAuthStrategy.parse(strategy)).not.toThrow(); + }); + }); + + it('should reject invalid strategies', () => { + expect(() => BetterAuthStrategy.parse('invalid')).toThrow(); + }); +}); + +describe('OAuthProviderSchema', () => { + it('should accept valid OAuth provider configuration', () => { + const provider: OAuthProvider = { + provider: 'google', + clientId: 'test-client-id', + clientSecret: 'test-client-secret', + scopes: ['openid', 'profile', 'email'], + enabled: true, + }; + + expect(() => OAuthProviderSchema.parse(provider)).not.toThrow(); + }); + + it('should accept minimal OAuth provider configuration', () => { + const provider = { + provider: 'github', + clientId: 'test-client-id', + clientSecret: 'test-client-secret', + }; + + const result = OAuthProviderSchema.parse(provider); + expect(result.enabled).toBe(true); // default value + }); + + it('should accept all supported OAuth providers', () => { + const providers = [ + 'google', + 'github', + 'facebook', + 'twitter', + 'linkedin', + 'microsoft', + 'apple', + 'discord', + 'gitlab', + 'custom', + ]; + + providers.forEach((provider) => { + const config = { + provider, + clientId: 'test-id', + clientSecret: 'test-secret', + }; + expect(() => OAuthProviderSchema.parse(config)).not.toThrow(); + }); + }); + + it('should validate redirect URI is a valid URL', () => { + const provider = { + provider: 'google', + clientId: 'test-id', + clientSecret: 'test-secret', + redirectUri: 'not-a-url', + }; + + expect(() => OAuthProviderSchema.parse(provider)).toThrow(); + }); +}); + +describe('EmailPasswordConfigSchema', () => { + it('should accept valid email password configuration', () => { + const config = { + enabled: true, + requireEmailVerification: true, + minPasswordLength: 12, + requirePasswordComplexity: true, + allowPasswordReset: true, + passwordResetExpiry: 3600, + }; + + expect(() => EmailPasswordConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = EmailPasswordConfigSchema.parse(config); + + expect(result.enabled).toBe(true); + expect(result.minPasswordLength).toBe(8); + expect(result.requireEmailVerification).toBe(true); + }); + + it('should enforce password length constraints', () => { + const tooShort = { minPasswordLength: 5 }; + expect(() => EmailPasswordConfigSchema.parse(tooShort)).toThrow(); + + const tooLong = { minPasswordLength: 200 }; + expect(() => EmailPasswordConfigSchema.parse(tooLong)).toThrow(); + + const justRight = { minPasswordLength: 10 }; + expect(() => EmailPasswordConfigSchema.parse(justRight)).not.toThrow(); + }); +}); + +describe('MagicLinkConfigSchema', () => { + it('should accept valid magic link configuration', () => { + const config = { + enabled: true, + expiryTime: 1800, // 30 minutes + }; + + expect(() => MagicLinkConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default expiry time', () => { + const config = { enabled: true }; + const result = MagicLinkConfigSchema.parse(config); + + expect(result.expiryTime).toBe(900); // 15 minutes default + }); +}); + +describe('PasskeyConfigSchema', () => { + it('should accept valid passkey configuration', () => { + const config = { + enabled: true, + rpName: 'ObjectStack', + rpId: 'objectstack.com', + userVerification: 'required' as const, + attestation: 'direct' as const, + }; + + expect(() => PasskeyConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = { + rpName: 'Test App', + }; + const result = PasskeyConfigSchema.parse(config); + + expect(result.enabled).toBe(false); // disabled by default + expect(result.userVerification).toBe('preferred'); + expect(result.attestation).toBe('none'); + }); + + it('should validate allowed origins are URLs', () => { + const config = { + rpName: 'Test', + allowedOrigins: ['https://example.com', 'https://app.example.com'], + }; + + expect(() => PasskeyConfigSchema.parse(config)).not.toThrow(); + }); +}); + +describe('SessionConfigSchema', () => { + it('should accept valid session configuration', () => { + const config = { + expiresIn: 604800, // 7 days + updateAge: 86400, // 1 day + cookieName: 'my-session', + cookieSecure: true, + cookieSameSite: 'strict' as const, + cookieHttpOnly: true, + }; + + expect(() => SessionConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = SessionConfigSchema.parse(config); + + expect(result.expiresIn).toBe(86400 * 7); // 7 days + expect(result.cookieName).toBe('better-auth.session_token'); + expect(result.cookieSecure).toBe(true); + expect(result.cookieSameSite).toBe('lax'); + }); + + it('should accept all SameSite options', () => { + const options = ['strict', 'lax', 'none'] as const; + + options.forEach((sameSite) => { + const config = { cookieSameSite: sameSite }; + expect(() => SessionConfigSchema.parse(config)).not.toThrow(); + }); + }); +}); + +describe('RateLimitConfigSchema', () => { + it('should accept valid rate limit configuration', () => { + const config = { + enabled: true, + maxAttempts: 10, + windowMs: 1800000, // 30 minutes + blockDuration: 3600000, // 1 hour + skipSuccessfulRequests: true, + }; + + expect(() => RateLimitConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = RateLimitConfigSchema.parse(config); + + expect(result.enabled).toBe(true); + expect(result.maxAttempts).toBe(5); + expect(result.windowMs).toBe(900000); // 15 minutes + }); +}); + +describe('CSRFConfigSchema', () => { + it('should accept valid CSRF configuration', () => { + const config = { + enabled: true, + tokenLength: 64, + cookieName: 'csrf-token', + headerName: 'X-CSRF-Token', + }; + + expect(() => CSRFConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = CSRFConfigSchema.parse(config); + + expect(result.enabled).toBe(true); + expect(result.tokenLength).toBe(32); + expect(result.cookieName).toBe('better-auth.csrf_token'); + }); +}); + +describe('AccountLinkingConfigSchema', () => { + it('should accept valid account linking configuration', () => { + const config = { + enabled: true, + autoLink: true, + requireVerification: false, + }; + + expect(() => AccountLinkingConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = AccountLinkingConfigSchema.parse(config); + + expect(result.enabled).toBe(true); + expect(result.autoLink).toBe(false); + expect(result.requireVerification).toBe(true); + }); +}); + +describe('TwoFactorConfigSchema', () => { + it('should accept valid 2FA configuration', () => { + const config = { + enabled: true, + issuer: 'ObjectStack', + qrCodeSize: 256, + backupCodes: { + enabled: true, + count: 8, + }, + }; + + expect(() => TwoFactorConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = { enabled: false }; + const result = TwoFactorConfigSchema.parse(config); + + expect(result.qrCodeSize).toBe(200); + }); +}); + +describe('UserFieldMappingSchema', () => { + it('should accept custom field mappings', () => { + const config = { + id: 'user_id', + email: 'user_email', + name: 'full_name', + emailVerified: 'is_email_verified', + }; + + expect(() => UserFieldMappingSchema.parse(config)).not.toThrow(); + }); + + it('should use default field names', () => { + const config = {}; + const result = UserFieldMappingSchema.parse(config); + + expect(result.id).toBe('id'); + expect(result.email).toBe('email'); + expect(result.name).toBe('name'); + expect(result.emailVerified).toBe('email_verified'); + }); +}); + +describe('DatabaseAdapterSchema', () => { + it('should accept valid database adapter configuration', () => { + const config = { + type: 'prisma' as const, + connectionString: 'postgresql://user:pass@localhost:5432/db', + tablePrefix: 'auth_', + schema: 'public', + }; + + expect(() => DatabaseAdapterSchema.parse(config)).not.toThrow(); + }); + + it('should accept all supported adapter types', () => { + const types = ['prisma', 'drizzle', 'kysely', 'custom'] as const; + + types.forEach((type) => { + const config = { type }; + expect(() => DatabaseAdapterSchema.parse(config)).not.toThrow(); + }); + }); + + it('should use default table prefix', () => { + const config = { type: 'drizzle' as const }; + const result = DatabaseAdapterSchema.parse(config); + + expect(result.tablePrefix).toBe('auth_'); + }); +}); + +describe('BetterAuthPluginConfigSchema', () => { + it('should accept valid plugin configuration', () => { + const config = { + name: 'organization', + enabled: true, + options: { + maxOrganizations: 5, + allowInvites: true, + }, + }; + + expect(() => BetterAuthPluginConfigSchema.parse(config)).not.toThrow(); + }); +}); + +describe('BetterAuthConfigSchema', () => { + it('should accept minimal valid configuration', () => { + const config: BetterAuthConfig = { + name: 'better_auth', + label: 'Better Auth', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), // 32 character secret + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => BetterAuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept comprehensive configuration', () => { + const config: BetterAuthConfig = { + name: 'main_auth', + label: 'Main Authentication', + strategies: ['email_password', 'oauth', 'magic_link'], + baseUrl: 'https://app.example.com', + secret: 'super-secret-key-with-at-least-32-characters', + + emailPassword: { + enabled: true, + requireEmailVerification: true, + minPasswordLength: 10, + requirePasswordComplexity: true, + allowPasswordReset: true, + passwordResetExpiry: 7200, + }, + + magicLink: { + enabled: true, + expiryTime: 900, + }, + + oauth: { + providers: [ + { + provider: 'google', + clientId: 'google-client-id', + clientSecret: 'google-client-secret', + scopes: ['openid', 'profile', 'email'], + enabled: true, + }, + { + provider: 'github', + clientId: 'github-client-id', + clientSecret: 'github-client-secret', + enabled: true, + }, + ], + }, + + session: { + expiresIn: 604800, + cookieName: 'app-session', + cookieSecure: true, + cookieSameSite: 'lax', + }, + + rateLimit: { + enabled: true, + maxAttempts: 5, + windowMs: 900000, + }, + + csrf: { + enabled: true, + tokenLength: 32, + }, + + accountLinking: { + enabled: true, + autoLink: false, + requireVerification: true, + }, + + twoFactor: { + enabled: true, + issuer: 'My App', + }, + + userFieldMapping: {}, + + database: { + type: 'prisma', + tablePrefix: 'auth_', + }, + + plugins: [], + + active: true, + allowRegistration: true, + }; + + expect(() => BetterAuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should enforce snake_case for name field', () => { + const invalidConfig = { + name: 'betterAuth', // camelCase - invalid + label: 'Better Auth', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => BetterAuthConfigSchema.parse(invalidConfig)).toThrow(); + + const validConfig = { + ...invalidConfig, + name: 'better_auth', // snake_case - valid + }; + + expect(() => BetterAuthConfigSchema.parse(validConfig)).not.toThrow(); + }); + + it('should require at least one strategy', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: [], // empty - invalid + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => BetterAuthConfigSchema.parse(config)).toThrow(); + }); + + it('should require secret to be at least 32 characters', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'short', // too short + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => BetterAuthConfigSchema.parse(config)).toThrow(); + }); + + it('should validate baseUrl is a valid URL', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'not-a-url', // invalid URL + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => BetterAuthConfigSchema.parse(config)).toThrow(); + }); + + it('should accept configuration with hooks', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + hooks: { + beforeSignIn: async ({ email }: { email: string }) => { + console.log('Before sign in:', email); + }, + afterSignIn: async ({ user, session }: { user: any; session: any }) => { + console.log('After sign in:', user.id); + }, + }, + }; + + expect(() => BetterAuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept configuration with security settings', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + security: { + allowedOrigins: ['https://app.example.com'], + trustProxy: true, + ipRateLimiting: true, + sessionFingerprinting: true, + maxSessions: 3, + }, + }; + + expect(() => BetterAuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept configuration with email settings', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + email: { + from: 'noreply@example.com', + fromName: 'My App', + provider: 'sendgrid' as const, + config: { + apiKey: 'sendgrid-api-key', + }, + }, + }; + + expect(() => BetterAuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept configuration with UI customization', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + ui: { + brandName: 'My Brand', + logo: 'https://example.com/logo.png', + primaryColor: '#007bff', + customCss: '.button { color: blue; }', + }, + }; + + expect(() => BetterAuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values for optional fields', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + }; + + const result = BetterAuthConfigSchema.parse(config); + + expect(result.active).toBe(true); + expect(result.allowRegistration).toBe(true); + expect(result.plugins).toEqual([]); + }); +}); + +describe('BetterAuthProviderSchema', () => { + it('should accept valid better-auth provider', () => { + const provider: BetterAuthProvider = { + type: 'better_auth', + config: { + name: 'main_auth', + label: 'Main Auth', + strategies: ['email_password', 'oauth'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + oauth: { + providers: [ + { + provider: 'google', + clientId: 'test-id', + clientSecret: 'test-secret', + }, + ], + }, + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }, + }; + + expect(() => BetterAuthProviderSchema.parse(provider)).not.toThrow(); + }); + + it('should require type to be "better_auth"', () => { + const provider = { + type: 'other_auth', // invalid + config: { + name: 'test', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }, + }; + + expect(() => BetterAuthProviderSchema.parse(provider)).toThrow(); + }); +}); + +describe('Type inference', () => { + it('should correctly infer BetterAuthConfig type', () => { + const config: BetterAuthConfig = { + name: 'test_auth', + label: 'Test Auth', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + // This test passes if TypeScript compiles without errors + expect(config.name).toBe('test_auth'); + expect(config.strategies).toContain('email_password'); + }); + + it('should correctly infer BetterAuthProvider type', () => { + const provider: BetterAuthProvider = { + type: 'better_auth', + config: { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }, + }; + + // This test passes if TypeScript compiles without errors + expect(provider.type).toBe('better_auth'); + expect(provider.config.name).toBe('test_auth'); + }); +}); diff --git a/packages/spec/src/system/auth-better.zod.ts b/packages/spec/src/system/auth-better.zod.ts new file mode 100644 index 000000000..aa6d6c271 --- /dev/null +++ b/packages/spec/src/system/auth-better.zod.ts @@ -0,0 +1,484 @@ +import { z } from 'zod'; + +/** + * Better-Auth Authentication Protocol + * + * Defines the schema for integrating better-auth (https://better-auth.com) + * as an authentication provider in the ObjectStack ecosystem. + * + * Better-auth is a modern, framework-agnostic authentication library that provides: + * - Multiple authentication strategies (email/password, OAuth, magic links, passkeys) + * - Session management + * - Security features (rate limiting, CSRF protection, etc.) + * - Plugin architecture + */ + +/** + * Supported authentication strategies in better-auth + */ +export const BetterAuthStrategy = z.enum([ + 'email_password', // Traditional email & password authentication + 'magic_link', // Passwordless email magic link + 'oauth', // OAuth2 providers (Google, GitHub, etc.) + 'passkey', // WebAuthn / FIDO2 passkeys + 'otp', // One-time password (SMS, Email) + 'anonymous', // Anonymous/guest sessions +]); + +export type BetterAuthStrategy = z.infer; + +/** + * OAuth Provider Configuration + * Supports popular OAuth2 providers + */ +export const OAuthProviderSchema = z.object({ + provider: z.enum([ + 'google', + 'github', + 'facebook', + 'twitter', + 'linkedin', + 'microsoft', + 'apple', + 'discord', + 'gitlab', + 'custom', + ]).describe('OAuth provider type'), + + clientId: z.string().describe('OAuth client ID'), + clientSecret: z.string().describe('OAuth client secret (typically from ENV)'), + + scopes: z.array(z.string()).optional().describe('Requested OAuth scopes'), + + redirectUri: z.string().url().optional().describe('OAuth callback URL'), + + enabled: z.boolean().default(true).describe('Whether this provider is enabled'), + + displayName: z.string().optional().describe('Display name for the provider button'), + + icon: z.string().optional().describe('Icon URL or identifier'), +}); + +export type OAuthProvider = z.infer; + +/** + * Email & Password Strategy Configuration + */ +export const EmailPasswordConfigSchema = z.object({ + enabled: z.boolean().default(true), + + requireEmailVerification: z.boolean().default(true).describe('Require email verification before login'), + + minPasswordLength: z.number().min(6).max(128).default(8).describe('Minimum password length'), + + requirePasswordComplexity: z.boolean().default(true).describe('Require uppercase, lowercase, numbers, symbols'), + + allowPasswordReset: z.boolean().default(true).describe('Enable password reset functionality'), + + passwordResetExpiry: z.number().default(3600).describe('Password reset token expiry in seconds'), +}); + +export type EmailPasswordConfig = z.infer; + +/** + * Magic Link Strategy Configuration + */ +export const MagicLinkConfigSchema = z.object({ + enabled: z.boolean().default(true), + + expiryTime: z.number().default(900).describe('Magic link expiry time in seconds (default 15 min)'), + + sendEmail: z.function() + .args(z.object({ + to: z.string().email(), + link: z.string().url(), + token: z.string(), + })) + .returns(z.promise(z.void())) + .optional() + .describe('Custom email sending function'), +}); + +export type MagicLinkConfig = z.infer; + +/** + * Passkey (WebAuthn) Strategy Configuration + */ +export const PasskeyConfigSchema = z.object({ + enabled: z.boolean().default(false), + + rpName: z.string().describe('Relying Party name'), + + rpId: z.string().optional().describe('Relying Party ID (defaults to domain)'), + + allowedOrigins: z.array(z.string().url()).optional().describe('Allowed origins for WebAuthn'), + + userVerification: z.enum(['required', 'preferred', 'discouraged']).default('preferred'), + + attestation: z.enum(['none', 'indirect', 'direct', 'enterprise']).default('none'), +}); + +export type PasskeyConfig = z.infer; + +/** + * Session Configuration + */ +export const SessionConfigSchema = z.object({ + expiresIn: z.number().default(86400 * 7).describe('Session expiry in seconds (default 7 days)'), + + updateAge: z.number().default(86400).describe('Session update interval in seconds (default 1 day)'), + + cookieName: z.string().default('better-auth.session_token').describe('Session cookie name'), + + cookieSecure: z.boolean().default(true).describe('Use secure cookies (HTTPS only)'), + + cookieSameSite: z.enum(['strict', 'lax', 'none']).default('lax').describe('SameSite cookie attribute'), + + cookieDomain: z.string().optional().describe('Cookie domain'), + + cookiePath: z.string().default('/').describe('Cookie path'), + + cookieHttpOnly: z.boolean().default(true).describe('HttpOnly cookie attribute'), +}); + +export type SessionConfig = z.infer; + +/** + * Rate Limiting Configuration + */ +export const RateLimitConfigSchema = z.object({ + enabled: z.boolean().default(true), + + maxAttempts: z.number().default(5).describe('Maximum login attempts'), + + windowMs: z.number().default(900000).describe('Time window in milliseconds (default 15 min)'), + + blockDuration: z.number().default(900000).describe('Block duration after max attempts in ms'), + + skipSuccessfulRequests: z.boolean().default(false).describe('Only count failed requests'), +}); + +export type RateLimitConfig = z.infer; + +/** + * CSRF Protection Configuration + */ +export const CSRFConfigSchema = z.object({ + enabled: z.boolean().default(true), + + tokenLength: z.number().default(32).describe('CSRF token length'), + + cookieName: z.string().default('better-auth.csrf_token').describe('CSRF cookie name'), + + headerName: z.string().default('X-CSRF-Token').describe('CSRF header name'), +}); + +export type CSRFConfig = z.infer; + +/** + * Account Linking Configuration + * Allows linking multiple auth methods to a single user account + */ +export const AccountLinkingConfigSchema = z.object({ + enabled: z.boolean().default(true).describe('Allow account linking'), + + autoLink: z.boolean().default(false).describe('Automatically link accounts with same email'), + + requireVerification: z.boolean().default(true).describe('Require email verification before linking'), +}); + +export type AccountLinkingConfig = z.infer; + +/** + * Two-Factor Authentication (2FA) Configuration + */ +export const TwoFactorConfigSchema = z.object({ + enabled: z.boolean().default(false), + + issuer: z.string().optional().describe('TOTP issuer name'), + + qrCodeSize: z.number().default(200).describe('QR code size in pixels'), + + backupCodes: z.object({ + enabled: z.boolean().default(true), + count: z.number().default(10).describe('Number of backup codes to generate'), + }).optional(), +}); + +export type TwoFactorConfig = z.infer; + +/** + * User Field Mapping Configuration + * Maps better-auth user fields to ObjectStack user object fields + */ +export const UserFieldMappingSchema = z.object({ + id: z.string().default('id').describe('User ID field'), + email: z.string().default('email').describe('Email field'), + name: z.string().default('name').describe('Name field'), + image: z.string().default('image').optional().describe('Profile image field'), + emailVerified: z.string().default('email_verified').describe('Email verification status field'), + createdAt: z.string().default('created_at').describe('Created timestamp field'), + updatedAt: z.string().default('updated_at').describe('Updated timestamp field'), +}); + +export type UserFieldMapping = z.infer; + +/** + * Database Adapter Configuration + */ +export const DatabaseAdapterSchema = z.object({ + type: z.enum(['prisma', 'drizzle', 'kysely', 'custom']).describe('Database adapter type'), + + connectionString: z.string().optional().describe('Database connection string'), + + tablePrefix: z.string().default('auth_').describe('Prefix for auth tables'), + + schema: z.string().optional().describe('Database schema name'), +}); + +export type DatabaseAdapter = z.infer; + +/** + * Better-Auth Plugin Configuration + * Extends better-auth with additional features + */ +export const BetterAuthPluginConfigSchema = z.object({ + name: z.string().describe('Plugin name'), + + enabled: z.boolean().default(true), + + options: z.record(z.any()).optional().describe('Plugin-specific options'), +}); + +export type BetterAuthPluginConfig = z.infer; + +/** + * Complete Better-Auth Configuration Schema + * + * This is the main configuration object for integrating better-auth + * into an ObjectStack application. + * + * @example + * ```typescript + * const authConfig: BetterAuthConfig = { + * name: 'better_auth', + * label: 'Better Auth', + * strategies: ['email_password', 'oauth'], + * emailPassword: { + * enabled: true, + * minPasswordLength: 8, + * }, + * oauth: { + * providers: [{ + * provider: 'google', + * clientId: process.env.GOOGLE_CLIENT_ID, + * clientSecret: process.env.GOOGLE_CLIENT_SECRET, + * }], + * }, + * session: { + * expiresIn: 604800, // 7 days + * }, + * }; + * ``` + */ +export const BetterAuthConfigSchema = z.object({ + /** + * Unique identifier for this auth configuration + * Must be in snake_case following ObjectStack conventions + */ + name: z.string() + .regex(/^[a-z_][a-z0-9_]*$/) + .describe('Configuration name (snake_case)'), + + /** + * Human-readable label + */ + label: z.string().describe('Display label'), + + /** + * Enabled authentication strategies + */ + strategies: z.array(BetterAuthStrategy).min(1).describe('Enabled authentication strategies'), + + /** + * Base URL for the application + */ + baseUrl: z.string().url().describe('Application base URL'), + + /** + * Secret key for signing tokens and cookies + * Should be loaded from environment variables + */ + secret: z.string().min(32).describe('Secret key for signing (min 32 chars)'), + + /** + * Email & Password configuration + */ + emailPassword: EmailPasswordConfigSchema.optional(), + + /** + * Magic Link configuration + */ + magicLink: MagicLinkConfigSchema.optional(), + + /** + * Passkey (WebAuthn) configuration + */ + passkey: PasskeyConfigSchema.optional(), + + /** + * OAuth configuration + */ + oauth: z.object({ + providers: z.array(OAuthProviderSchema).min(1), + }).optional(), + + /** + * Session configuration + */ + session: SessionConfigSchema.default({}), + + /** + * Rate limiting configuration + */ + rateLimit: RateLimitConfigSchema.default({}), + + /** + * CSRF protection configuration + */ + csrf: CSRFConfigSchema.default({}), + + /** + * Account linking configuration + */ + accountLinking: AccountLinkingConfigSchema.default({}), + + /** + * Two-factor authentication configuration + */ + twoFactor: TwoFactorConfigSchema.optional(), + + /** + * User field mapping + */ + userFieldMapping: UserFieldMappingSchema.default({}), + + /** + * Database adapter configuration + */ + database: DatabaseAdapterSchema.optional(), + + /** + * Additional better-auth plugins + */ + plugins: z.array(BetterAuthPluginConfigSchema).default([]), + + /** + * Custom hooks for authentication events + */ + hooks: z.object({ + beforeSignIn: z.function() + .args(z.object({ email: z.string() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called before user sign in'), + + afterSignIn: z.function() + .args(z.object({ user: z.any(), session: z.any() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called after user sign in'), + + beforeSignUp: z.function() + .args(z.object({ email: z.string(), name: z.string().optional() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called before user registration'), + + afterSignUp: z.function() + .args(z.object({ user: z.any() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called after user registration'), + + beforeSignOut: z.function() + .args(z.object({ sessionId: z.string() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called before user sign out'), + + afterSignOut: z.function() + .args(z.object({ sessionId: z.string() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called after user sign out'), + }).optional().describe('Authentication lifecycle hooks'), + + /** + * Advanced security settings + */ + security: z.object({ + allowedOrigins: z.array(z.string()).optional().describe('CORS allowed origins'), + + trustProxy: z.boolean().default(false).describe('Trust proxy headers'), + + ipRateLimiting: z.boolean().default(true).describe('Enable IP-based rate limiting'), + + sessionFingerprinting: z.boolean().default(true).describe('Enable session fingerprinting'), + + maxSessions: z.number().default(5).describe('Maximum concurrent sessions per user'), + }).optional().describe('Advanced security settings'), + + /** + * Email configuration for transactional emails + */ + email: z.object({ + from: z.string().email().describe('From email address'), + + fromName: z.string().optional().describe('From name'), + + provider: z.enum(['smtp', 'sendgrid', 'mailgun', 'ses', 'resend', 'custom']).describe('Email provider'), + + config: z.record(z.any()).optional().describe('Provider-specific configuration'), + }).optional().describe('Email configuration'), + + /** + * UI customization options + */ + ui: z.object({ + brandName: z.string().optional().describe('Brand name displayed in auth UI'), + + logo: z.string().optional().describe('Logo URL'), + + primaryColor: z.string().optional().describe('Primary brand color (hex)'), + + customCss: z.string().optional().describe('Custom CSS for auth pages'), + }).optional().describe('UI customization'), + + /** + * Whether this auth provider is active + */ + active: z.boolean().default(true).describe('Whether this provider is active'), + + /** + * Whether to allow new user registration + */ + allowRegistration: z.boolean().default(true).describe('Allow new user registration'), +}); + +/** + * TypeScript type inferred from BetterAuthConfigSchema + */ +export type BetterAuthConfig = z.infer; + +/** + * Better-Auth Provider Schema + * Wraps the configuration for use in the identity system + */ +export const BetterAuthProviderSchema = z.object({ + type: z.literal('better_auth').describe('Provider type identifier'), + + config: BetterAuthConfigSchema.describe('Better-auth configuration'), +}); + +export type BetterAuthProvider = z.infer; From 24f46fae559c98eeba35379c283b47d6e1666d39 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 00:21:21 +0000 Subject: [PATCH 3/7] Add documentation and examples for better-auth plugin Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- docs/BETTER_AUTH_PLUGIN.md | 364 +++++++++++++++++++++++++++++ examples/auth-better-examples.ts | 377 +++++++++++++++++++++++++++++++ 2 files changed, 741 insertions(+) create mode 100644 docs/BETTER_AUTH_PLUGIN.md create mode 100644 examples/auth-better-examples.ts diff --git a/docs/BETTER_AUTH_PLUGIN.md b/docs/BETTER_AUTH_PLUGIN.md new file mode 100644 index 000000000..4299ccf9d --- /dev/null +++ b/docs/BETTER_AUTH_PLUGIN.md @@ -0,0 +1,364 @@ +# Better-Auth Authentication Plugin + +A comprehensive authentication specification for integrating [better-auth](https://better-auth.com) into the ObjectStack ecosystem. + +## Overview + +This specification defines the schema and types for configuring better-auth as an authentication provider in ObjectStack applications. Better-auth is a modern, framework-agnostic authentication library that provides multiple authentication strategies, session management, and comprehensive security features. + +## Features + +### Authentication Strategies + +- **Email/Password**: Traditional email and password authentication with customizable password policies +- **Magic Link**: Passwordless email-based authentication +- **OAuth**: Social login with popular providers (Google, GitHub, Facebook, Twitter, LinkedIn, Microsoft, Apple, Discord, GitLab) +- **Passkey**: WebAuthn/FIDO2 biometric authentication +- **OTP**: One-time password authentication (SMS, Email) +- **Anonymous**: Guest/anonymous session support + +### Security Features + +- **Rate Limiting**: Configurable rate limiting to prevent brute-force attacks +- **CSRF Protection**: Built-in CSRF token validation +- **Session Fingerprinting**: Enhanced session security with device fingerprinting +- **Two-Factor Authentication (2FA)**: TOTP-based 2FA with backup codes +- **Account Linking**: Link multiple authentication methods to a single user account +- **IP-based Rate Limiting**: Prevent attacks from specific IP addresses + +### Session Management + +- Customizable session expiry and renewal +- Secure cookie configuration (HttpOnly, Secure, SameSite) +- Maximum concurrent sessions per user +- Session update intervals + +### Developer Features + +- **Lifecycle Hooks**: `beforeSignIn`, `afterSignIn`, `beforeSignUp`, `afterSignUp`, `beforeSignOut`, `afterSignOut` +- **Database Adapters**: Support for Prisma, Drizzle, Kysely, and custom adapters +- **Email Providers**: Integration with SendGrid, Mailgun, AWS SES, Resend, SMTP +- **Field Mapping**: Map better-auth user fields to your custom user object +- **Plugin System**: Extend better-auth with custom plugins + +## Installation + +```bash +pnpm add @objectstack/spec +``` + +## Usage + +### Basic Example + +```typescript +import type { BetterAuthConfig } from '@objectstack/spec'; + +const authConfig: BetterAuthConfig = { + name: 'main_auth', + label: 'Main Authentication', + strategies: ['email_password'], + baseUrl: 'https://app.example.com', + secret: process.env.AUTH_SECRET!, + + emailPassword: { + enabled: true, + requireEmailVerification: true, + minPasswordLength: 8, + }, + + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, +}; +``` + +### OAuth Example + +```typescript +const oauthConfig: BetterAuthConfig = { + name: 'social_auth', + label: 'Social Login', + strategies: ['oauth'], + baseUrl: 'https://app.example.com', + secret: process.env.AUTH_SECRET!, + + oauth: { + providers: [ + { + provider: 'google', + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + scopes: ['openid', 'profile', 'email'], + }, + ], + }, + + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, +}; +``` + +### Multi-Strategy Example + +```typescript +const multiAuthConfig: BetterAuthConfig = { + name: 'multi_auth', + label: 'Multi-Strategy Auth', + strategies: ['email_password', 'oauth', 'magic_link'], + baseUrl: 'https://app.example.com', + secret: process.env.AUTH_SECRET!, + + emailPassword: { + enabled: true, + minPasswordLength: 10, + }, + + oauth: { + providers: [ + { provider: 'google', clientId: '...', clientSecret: '...' }, + { provider: 'github', clientId: '...', clientSecret: '...' }, + ], + }, + + magicLink: { + enabled: true, + expiryTime: 900, + }, + + session: { + expiresIn: 604800, // 7 days + }, + + rateLimit: { + enabled: true, + maxAttempts: 5, + }, + + csrf: { + enabled: true, + }, + + accountLinking: { + enabled: true, + autoLink: false, + }, +}; +``` + +## Configuration Reference + +### Core Configuration + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `name` | `string` | ✅ | Configuration identifier (snake_case) | +| `label` | `string` | ✅ | Human-readable label | +| `strategies` | `BetterAuthStrategy[]` | ✅ | Enabled authentication strategies | +| `baseUrl` | `string` | ✅ | Application base URL | +| `secret` | `string` | ✅ | Secret key for signing (min 32 chars) | + +### Strategy Configuration + +#### Email/Password + +```typescript +emailPassword: { + enabled: boolean; // Enable email/password auth + requireEmailVerification: boolean; // Require email verification + minPasswordLength: number; // Minimum password length (6-128) + requirePasswordComplexity: boolean; // Require uppercase, lowercase, numbers, symbols + allowPasswordReset: boolean; // Enable password reset + passwordResetExpiry: number; // Reset token expiry in seconds +} +``` + +#### Magic Link + +```typescript +magicLink: { + enabled: boolean; // Enable magic link auth + expiryTime: number; // Link expiry in seconds (default: 900) + sendEmail?: Function; // Custom email sending function +} +``` + +#### Passkey (WebAuthn) + +```typescript +passkey: { + enabled: boolean; // Enable passkey auth + rpName: string; // Relying Party name + rpId?: string; // Relying Party ID (defaults to domain) + allowedOrigins?: string[]; // Allowed origins + userVerification?: 'required' | 'preferred' | 'discouraged'; + attestation?: 'none' | 'indirect' | 'direct' | 'enterprise'; +} +``` + +#### OAuth + +```typescript +oauth: { + providers: [{ + provider: 'google' | 'github' | 'facebook' | ...; // OAuth provider + clientId: string; // OAuth client ID + clientSecret: string; // OAuth client secret + scopes?: string[]; // Requested scopes + redirectUri?: string; // Callback URL + enabled?: boolean; // Enable/disable provider + displayName?: string; // Button label + icon?: string; // Icon URL + }] +} +``` + +### Session Configuration + +```typescript +session: { + expiresIn: number; // Session expiry in seconds (default: 604800) + updateAge: number; // Update interval in seconds (default: 86400) + cookieName: string; // Cookie name + cookieSecure: boolean; // Use secure cookies (HTTPS only) + cookieSameSite: 'strict' | 'lax' | 'none'; // SameSite attribute + cookieDomain?: string; // Cookie domain + cookiePath: string; // Cookie path (default: '/') + cookieHttpOnly: boolean; // HttpOnly attribute +} +``` + +### Security Configuration + +```typescript +rateLimit: { + enabled: boolean; // Enable rate limiting + maxAttempts: number; // Max login attempts (default: 5) + windowMs: number; // Time window in ms (default: 900000) + blockDuration: number; // Block duration in ms + skipSuccessfulRequests: boolean; // Only count failed requests +} + +csrf: { + enabled: boolean; // Enable CSRF protection + tokenLength: number; // CSRF token length (default: 32) + cookieName: string; // CSRF cookie name + headerName: string; // CSRF header name +} + +security: { + allowedOrigins?: string[]; // CORS allowed origins + trustProxy: boolean; // Trust proxy headers + ipRateLimiting: boolean; // Enable IP-based rate limiting + sessionFingerprinting: boolean; // Enable session fingerprinting + maxSessions: number; // Max concurrent sessions (default: 5) +} +``` + +### Two-Factor Authentication + +```typescript +twoFactor: { + enabled: boolean; // Enable 2FA + issuer?: string; // TOTP issuer name + qrCodeSize: number; // QR code size in pixels (default: 200) + backupCodes?: { + enabled: boolean; // Enable backup codes + count: number; // Number of backup codes (default: 10) + } +} +``` + +### Lifecycle Hooks + +```typescript +hooks: { + beforeSignIn?: ({ email }) => Promise; + afterSignIn?: ({ user, session }) => Promise; + beforeSignUp?: ({ email, name? }) => Promise; + afterSignUp?: ({ user }) => Promise; + beforeSignOut?: ({ sessionId }) => Promise; + afterSignOut?: ({ sessionId }) => Promise; +} +``` + +## Supported OAuth Providers + +- Google (`google`) +- GitHub (`github`) +- Facebook (`facebook`) +- Twitter (`twitter`) +- LinkedIn (`linkedin`) +- Microsoft (`microsoft`) +- Apple (`apple`) +- Discord (`discord`) +- GitLab (`gitlab`) +- Custom OAuth2 (`custom`) + +## Database Adapters + +- Prisma (`prisma`) +- Drizzle (`drizzle`) +- Kysely (`kysely`) +- Custom (`custom`) + +## Email Providers + +- SMTP (`smtp`) +- SendGrid (`sendgrid`) +- Mailgun (`mailgun`) +- AWS SES (`ses`) +- Resend (`resend`) +- Custom (`custom`) + +## Examples + +See [examples/auth-better-examples.ts](../examples/auth-better-examples.ts) for comprehensive usage examples including: + +- Basic email/password authentication +- OAuth with Google and GitHub +- Multi-strategy authentication +- Production-ready configuration +- Plugin manifest integration + +## Schema Files + +- **Zod Schema**: `packages/spec/src/system/auth-better.zod.ts` +- **Tests**: `packages/spec/src/system/auth-better.test.ts` +- **JSON Schema**: `packages/spec/json-schema/BetterAuthConfig.json` +- **Documentation**: `content/docs/references/system/BetterAuthConfig.mdx` + +## Type Safety + +All schemas are defined using Zod and TypeScript types are inferred automatically: + +```typescript +import type { + BetterAuthConfig, + BetterAuthProvider, + BetterAuthStrategy, + OAuthProvider, + SessionConfig, + // ... and more +} from '@objectstack/spec'; +``` + +## Naming Conventions + +Following ObjectStack conventions: + +- **Configuration Keys** (TypeScript properties): `camelCase` (e.g., `maxAttempts`, `emailPassword`) +- **Machine Names** (Data values): `snake_case` (e.g., `name: 'better_auth'`, `strategy: 'email_password'`) + +## Resources + +- [Better-Auth Documentation](https://better-auth.com) +- [ObjectStack Documentation](https://objectstack.ai) +- [JSON Schema Reference](../packages/spec/json-schema/BetterAuthConfig.json) + +## License + +Apache 2.0 diff --git a/examples/auth-better-examples.ts b/examples/auth-better-examples.ts new file mode 100644 index 000000000..81fd184ba --- /dev/null +++ b/examples/auth-better-examples.ts @@ -0,0 +1,377 @@ +/** + * Better-Auth Plugin Example + * + * This example demonstrates how to configure a better-auth authentication provider + * for an ObjectStack application. + * + * @see https://better-auth.com for more information about better-auth + */ + +import type { BetterAuthConfig, BetterAuthProvider } from '@objectstack/spec'; + +/** + * Example 1: Basic Email/Password Authentication + * + * Simplest configuration with just email and password login. + */ +export const basicEmailAuth: BetterAuthConfig = { + name: 'basic_auth', + label: 'Email Login', + strategies: ['email_password'], + baseUrl: 'https://app.example.com', + secret: process.env.AUTH_SECRET || 'your-secret-key-min-32-characters-long', + + emailPassword: { + enabled: true, + requireEmailVerification: true, + minPasswordLength: 8, + requirePasswordComplexity: true, + }, + + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, +}; + +/** + * Example 2: OAuth with Google and GitHub + * + * Allows users to sign in with Google or GitHub accounts. + */ +export const oauthAuth: BetterAuthConfig = { + name: 'social_auth', + label: 'Social Login', + strategies: ['oauth'], + baseUrl: 'https://app.example.com', + secret: process.env.AUTH_SECRET!, + + oauth: { + providers: [ + { + provider: 'google', + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + scopes: ['openid', 'profile', 'email'], + enabled: true, + displayName: 'Sign in with Google', + }, + { + provider: 'github', + clientId: process.env.GITHUB_CLIENT_ID!, + clientSecret: process.env.GITHUB_CLIENT_SECRET!, + scopes: ['user:email'], + enabled: true, + displayName: 'Sign in with GitHub', + }, + ], + }, + + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: { + enabled: true, + autoLink: true, + requireVerification: false, + }, +}; + +/** + * Example 3: Multi-Strategy Authentication + * + * Comprehensive configuration supporting multiple authentication methods. + */ +export const comprehensiveAuth: BetterAuthConfig = { + name: 'main_auth', + label: 'Main Authentication', + strategies: ['email_password', 'oauth', 'magic_link', 'passkey'], + baseUrl: 'https://app.example.com', + secret: process.env.AUTH_SECRET!, + + // Email & Password Configuration + emailPassword: { + enabled: true, + requireEmailVerification: true, + minPasswordLength: 10, + requirePasswordComplexity: true, + allowPasswordReset: true, + passwordResetExpiry: 3600, // 1 hour + }, + + // Magic Link Configuration + magicLink: { + enabled: true, + expiryTime: 900, // 15 minutes + }, + + // Passkey (WebAuthn) Configuration + passkey: { + enabled: true, + rpName: 'My App', + rpId: 'app.example.com', + userVerification: 'preferred', + attestation: 'none', + }, + + // OAuth Providers + oauth: { + providers: [ + { + provider: 'google', + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + enabled: true, + }, + { + provider: 'github', + clientId: process.env.GITHUB_CLIENT_ID!, + clientSecret: process.env.GITHUB_CLIENT_SECRET!, + enabled: true, + }, + ], + }, + + // Session Configuration + session: { + expiresIn: 604800, // 7 days + updateAge: 86400, // 1 day + cookieName: 'app_session', + cookieSecure: true, + cookieSameSite: 'lax', + }, + + // Rate Limiting + rateLimit: { + enabled: true, + maxAttempts: 5, + windowMs: 900000, // 15 minutes + blockDuration: 900000, // 15 minutes + }, + + // CSRF Protection + csrf: { + enabled: true, + tokenLength: 32, + }, + + // Account Linking + accountLinking: { + enabled: true, + autoLink: false, + requireVerification: true, + }, + + // Two-Factor Authentication + twoFactor: { + enabled: true, + issuer: 'My App', + qrCodeSize: 256, + backupCodes: { + enabled: true, + count: 10, + }, + }, + + // Database Configuration + database: { + type: 'prisma', + tablePrefix: 'auth_', + }, + + // Lifecycle Hooks + hooks: { + beforeSignIn: async ({ email }) => { + console.log(`User ${email} attempting to sign in`); + }, + + afterSignIn: async ({ user, session }) => { + console.log(`User ${user.id} signed in successfully`); + // You could log this event, update last login time, etc. + }, + + afterSignUp: async ({ user }) => { + console.log(`New user registered: ${user.email}`); + // You could send a welcome email, create default data, etc. + }, + }, + + // Security Settings + security: { + allowedOrigins: ['https://app.example.com', 'https://admin.example.com'], + trustProxy: true, + ipRateLimiting: true, + sessionFingerprinting: true, + maxSessions: 5, + }, + + // Email Configuration + email: { + from: 'noreply@example.com', + fromName: 'My App', + provider: 'sendgrid', + config: { + apiKey: process.env.SENDGRID_API_KEY, + }, + }, + + // UI Customization + ui: { + brandName: 'My App', + logo: 'https://app.example.com/logo.png', + primaryColor: '#007bff', + }, + + active: true, + allowRegistration: true, +}; + +/** + * Example 4: Production-Ready Configuration + * + * Enterprise-grade authentication with all security features enabled. + */ +export const productionAuth: BetterAuthConfig = { + name: 'production_auth', + label: 'Production Authentication', + strategies: ['email_password', 'oauth'], + baseUrl: process.env.APP_URL!, + secret: process.env.AUTH_SECRET!, + + emailPassword: { + enabled: true, + requireEmailVerification: true, + minPasswordLength: 12, + requirePasswordComplexity: true, + allowPasswordReset: true, + passwordResetExpiry: 3600, + }, + + oauth: { + providers: [ + { + provider: 'google', + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + scopes: ['openid', 'profile', 'email'], + enabled: true, + }, + ], + }, + + session: { + expiresIn: 86400 * 30, // 30 days + updateAge: 86400, // 1 day + cookieSecure: true, + cookieSameSite: 'strict', + cookieHttpOnly: true, + }, + + rateLimit: { + enabled: true, + maxAttempts: 3, + windowMs: 600000, // 10 minutes + blockDuration: 1800000, // 30 minutes + }, + + csrf: { + enabled: true, + tokenLength: 64, + }, + + accountLinking: { + enabled: true, + autoLink: false, + requireVerification: true, + }, + + twoFactor: { + enabled: true, + issuer: process.env.APP_NAME!, + backupCodes: { + enabled: true, + count: 10, + }, + }, + + security: { + allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') || [], + trustProxy: true, + ipRateLimiting: true, + sessionFingerprinting: true, + maxSessions: 3, + }, + + database: { + type: 'prisma', + connectionString: process.env.DATABASE_URL, + tablePrefix: 'auth_', + }, + + email: { + from: process.env.EMAIL_FROM!, + fromName: process.env.APP_NAME!, + provider: 'sendgrid', + config: { + apiKey: process.env.SENDGRID_API_KEY, + }, + }, + + hooks: { + beforeSignIn: async ({ email }) => { + // Log authentication attempts + console.log(`[AUTH] Sign in attempt: ${email}`); + }, + + afterSignIn: async ({ user, session }) => { + // Audit logging + console.log(`[AUTH] User ${user.id} authenticated`); + }, + + afterSignUp: async ({ user }) => { + // Send welcome email, create default records, etc. + console.log(`[AUTH] New user registered: ${user.id}`); + }, + }, + + active: true, + allowRegistration: false, // Registration disabled for security +}; + +/** + * Example 5: Better-Auth Provider Wrapper + * + * Wrapping the configuration in the provider schema for use in identity system. + */ +export const betterAuthProvider: BetterAuthProvider = { + type: 'better_auth', + config: comprehensiveAuth, +}; + +/** + * Example 6: Plugin Configuration for ObjectStack Manifest + * + * How to include better-auth in an ObjectStack plugin manifest. + */ +export const authPluginManifest = { + id: 'com.example.auth', + name: 'Authentication Plugin', + version: '1.0.0', + type: 'plugin' as const, + description: 'Better-auth based authentication for ObjectStack', + + configuration: { + title: 'Authentication Settings', + properties: { + authConfig: { + type: 'object' as const, + description: 'Better-auth configuration', + required: true, + }, + }, + }, + + contributes: { + events: ['auth.signin', 'auth.signout', 'auth.signup'], + }, +}; From e8f0d88719caaea3953c34f83e8598cda736a72f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 00:41:12 +0000 Subject: [PATCH 4/7] Restructure as plugin: Move better-auth from core spec to plugin package Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../system/AccountLinkingConfig.mdx | 12 - .../references/system/BetterAuthConfig.mdx | 32 - .../system/BetterAuthPluginConfig.mdx | 12 - .../references/system/BetterAuthProvider.mdx | 11 - .../references/system/BetterAuthStrategy.mdx | 13 - content/docs/references/system/CSRFConfig.mdx | 13 - .../references/system/DatabaseAdapter.mdx | 13 - .../references/system/EmailPasswordConfig.mdx | 15 - .../references/system/MagicLinkConfig.mdx | 11 - .../docs/references/system/OAuthProvider.mdx | 17 - .../docs/references/system/PasskeyConfig.mdx | 15 - .../references/system/RateLimitConfig.mdx | 14 - .../docs/references/system/SessionConfig.mdx | 17 - .../references/system/TwoFactorConfig.mdx | 13 - .../references/system/UserFieldMapping.mdx | 16 - docs/BETTER_AUTH_PLUGIN.md | 21 +- examples/auth-better-examples.ts | 377 --------- packages/plugin-better-auth/CHANGELOG.md | 13 + packages/plugin-better-auth/README.md | 55 ++ .../examples/plugin-examples.ts | 127 +++ .../plugin-better-auth/objectstack.config.ts | 127 +++ packages/plugin-better-auth/package.json | 33 + packages/plugin-better-auth/src/index.ts | 365 +++++++++ packages/plugin-better-auth/tsconfig.json | 18 + .../json-schema/AccountLinkingConfig.json | 27 - .../spec/json-schema/BetterAuthConfig.json | 601 -------------- .../json-schema/BetterAuthPluginConfig.json | 28 - .../spec/json-schema/BetterAuthProvider.json | 617 -------------- .../spec/json-schema/BetterAuthStrategy.json | 17 - packages/spec/json-schema/CSRFConfig.json | 31 - .../spec/json-schema/DatabaseAdapter.json | 38 - .../spec/json-schema/EmailAlertAction.json | 37 - .../spec/json-schema/EmailPasswordConfig.json | 43 - .../spec/json-schema/MagicLinkConfig.json | 21 - packages/spec/json-schema/OAuthProvider.json | 66 -- packages/spec/json-schema/PasskeyConfig.json | 54 -- packages/spec/json-schema/RateLimit.json | 26 - .../spec/json-schema/RateLimitConfig.json | 36 - packages/spec/json-schema/SessionConfig.json | 56 -- packages/spec/json-schema/SessionPolicy.json | 27 - .../spec/json-schema/TwoFactorConfig.json | 40 - .../spec/json-schema/UserFieldMapping.json | 47 -- packages/spec/src/index.ts | 1 - packages/spec/src/system/auth-better.test.ts | 752 ------------------ packages/spec/src/system/auth-better.zod.ts | 484 ----------- 45 files changed, 746 insertions(+), 3663 deletions(-) delete mode 100644 content/docs/references/system/AccountLinkingConfig.mdx delete mode 100644 content/docs/references/system/BetterAuthConfig.mdx delete mode 100644 content/docs/references/system/BetterAuthPluginConfig.mdx delete mode 100644 content/docs/references/system/BetterAuthProvider.mdx delete mode 100644 content/docs/references/system/BetterAuthStrategy.mdx delete mode 100644 content/docs/references/system/CSRFConfig.mdx delete mode 100644 content/docs/references/system/DatabaseAdapter.mdx delete mode 100644 content/docs/references/system/EmailPasswordConfig.mdx delete mode 100644 content/docs/references/system/MagicLinkConfig.mdx delete mode 100644 content/docs/references/system/OAuthProvider.mdx delete mode 100644 content/docs/references/system/PasskeyConfig.mdx delete mode 100644 content/docs/references/system/RateLimitConfig.mdx delete mode 100644 content/docs/references/system/SessionConfig.mdx delete mode 100644 content/docs/references/system/TwoFactorConfig.mdx delete mode 100644 content/docs/references/system/UserFieldMapping.mdx delete mode 100644 examples/auth-better-examples.ts create mode 100644 packages/plugin-better-auth/CHANGELOG.md create mode 100644 packages/plugin-better-auth/README.md create mode 100644 packages/plugin-better-auth/examples/plugin-examples.ts create mode 100644 packages/plugin-better-auth/objectstack.config.ts create mode 100644 packages/plugin-better-auth/package.json create mode 100644 packages/plugin-better-auth/src/index.ts create mode 100644 packages/plugin-better-auth/tsconfig.json delete mode 100644 packages/spec/json-schema/AccountLinkingConfig.json delete mode 100644 packages/spec/json-schema/BetterAuthConfig.json delete mode 100644 packages/spec/json-schema/BetterAuthPluginConfig.json delete mode 100644 packages/spec/json-schema/BetterAuthProvider.json delete mode 100644 packages/spec/json-schema/BetterAuthStrategy.json delete mode 100644 packages/spec/json-schema/CSRFConfig.json delete mode 100644 packages/spec/json-schema/DatabaseAdapter.json delete mode 100644 packages/spec/json-schema/EmailAlertAction.json delete mode 100644 packages/spec/json-schema/EmailPasswordConfig.json delete mode 100644 packages/spec/json-schema/MagicLinkConfig.json delete mode 100644 packages/spec/json-schema/OAuthProvider.json delete mode 100644 packages/spec/json-schema/PasskeyConfig.json delete mode 100644 packages/spec/json-schema/RateLimit.json delete mode 100644 packages/spec/json-schema/RateLimitConfig.json delete mode 100644 packages/spec/json-schema/SessionConfig.json delete mode 100644 packages/spec/json-schema/SessionPolicy.json delete mode 100644 packages/spec/json-schema/TwoFactorConfig.json delete mode 100644 packages/spec/json-schema/UserFieldMapping.json delete mode 100644 packages/spec/src/system/auth-better.test.ts delete mode 100644 packages/spec/src/system/auth-better.zod.ts diff --git a/content/docs/references/system/AccountLinkingConfig.mdx b/content/docs/references/system/AccountLinkingConfig.mdx deleted file mode 100644 index df6148efd..000000000 --- a/content/docs/references/system/AccountLinkingConfig.mdx +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: AccountLinkingConfig -description: AccountLinkingConfig Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | optional | Allow account linking | -| **autoLink** | `boolean` | optional | Automatically link accounts with same email | -| **requireVerification** | `boolean` | optional | Require email verification before linking | diff --git a/content/docs/references/system/BetterAuthConfig.mdx b/content/docs/references/system/BetterAuthConfig.mdx deleted file mode 100644 index 4757d17d9..000000000 --- a/content/docs/references/system/BetterAuthConfig.mdx +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: BetterAuthConfig -description: BetterAuthConfig Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Configuration name (snake_case) | -| **label** | `string` | ✅ | Display label | -| **strategies** | `Enum<'email_password' \| 'magic_link' \| 'oauth' \| 'passkey' \| 'otp' \| 'anonymous'>[]` | ✅ | Enabled authentication strategies | -| **baseUrl** | `string` | ✅ | Application base URL | -| **secret** | `string` | ✅ | Secret key for signing (min 32 chars) | -| **emailPassword** | `object` | optional | | -| **magicLink** | `object` | optional | | -| **passkey** | `object` | optional | | -| **oauth** | `object` | optional | | -| **session** | `object` | optional | | -| **rateLimit** | `object` | optional | | -| **csrf** | `object` | optional | | -| **accountLinking** | `object` | optional | | -| **twoFactor** | `object` | optional | | -| **userFieldMapping** | `object` | optional | | -| **database** | `object` | optional | | -| **plugins** | `object[]` | optional | | -| **hooks** | `object` | optional | Authentication lifecycle hooks | -| **security** | `object` | optional | Advanced security settings | -| **email** | `object` | optional | Email configuration | -| **ui** | `object` | optional | UI customization | -| **active** | `boolean` | optional | Whether this provider is active | -| **allowRegistration** | `boolean` | optional | Allow new user registration | diff --git a/content/docs/references/system/BetterAuthPluginConfig.mdx b/content/docs/references/system/BetterAuthPluginConfig.mdx deleted file mode 100644 index 976dda763..000000000 --- a/content/docs/references/system/BetterAuthPluginConfig.mdx +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: BetterAuthPluginConfig -description: BetterAuthPluginConfig Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Plugin name | -| **enabled** | `boolean` | optional | | -| **options** | `Record` | optional | Plugin-specific options | diff --git a/content/docs/references/system/BetterAuthProvider.mdx b/content/docs/references/system/BetterAuthProvider.mdx deleted file mode 100644 index 2673e536d..000000000 --- a/content/docs/references/system/BetterAuthProvider.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: BetterAuthProvider -description: BetterAuthProvider Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `string` | ✅ | Provider type identifier | -| **config** | `object` | ✅ | Better-auth configuration | diff --git a/content/docs/references/system/BetterAuthStrategy.mdx b/content/docs/references/system/BetterAuthStrategy.mdx deleted file mode 100644 index 834e2d0af..000000000 --- a/content/docs/references/system/BetterAuthStrategy.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: BetterAuthStrategy -description: BetterAuthStrategy Schema Reference ---- - -## Allowed Values - -* `email_password` -* `magic_link` -* `oauth` -* `passkey` -* `otp` -* `anonymous` \ No newline at end of file diff --git a/content/docs/references/system/CSRFConfig.mdx b/content/docs/references/system/CSRFConfig.mdx deleted file mode 100644 index eba2dddf6..000000000 --- a/content/docs/references/system/CSRFConfig.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: CSRFConfig -description: CSRFConfig Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | optional | | -| **tokenLength** | `number` | optional | CSRF token length | -| **cookieName** | `string` | optional | CSRF cookie name | -| **headerName** | `string` | optional | CSRF header name | diff --git a/content/docs/references/system/DatabaseAdapter.mdx b/content/docs/references/system/DatabaseAdapter.mdx deleted file mode 100644 index 804f75b03..000000000 --- a/content/docs/references/system/DatabaseAdapter.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: DatabaseAdapter -description: DatabaseAdapter Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `Enum<'prisma' \| 'drizzle' \| 'kysely' \| 'custom'>` | ✅ | Database adapter type | -| **connectionString** | `string` | optional | Database connection string | -| **tablePrefix** | `string` | optional | Prefix for auth tables | -| **schema** | `string` | optional | Database schema name | diff --git a/content/docs/references/system/EmailPasswordConfig.mdx b/content/docs/references/system/EmailPasswordConfig.mdx deleted file mode 100644 index ed35be72f..000000000 --- a/content/docs/references/system/EmailPasswordConfig.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: EmailPasswordConfig -description: EmailPasswordConfig Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | optional | | -| **requireEmailVerification** | `boolean` | optional | Require email verification before login | -| **minPasswordLength** | `number` | optional | Minimum password length | -| **requirePasswordComplexity** | `boolean` | optional | Require uppercase, lowercase, numbers, symbols | -| **allowPasswordReset** | `boolean` | optional | Enable password reset functionality | -| **passwordResetExpiry** | `number` | optional | Password reset token expiry in seconds | diff --git a/content/docs/references/system/MagicLinkConfig.mdx b/content/docs/references/system/MagicLinkConfig.mdx deleted file mode 100644 index 2971b2862..000000000 --- a/content/docs/references/system/MagicLinkConfig.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: MagicLinkConfig -description: MagicLinkConfig Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | optional | | -| **expiryTime** | `number` | optional | Magic link expiry time in seconds (default 15 min) | diff --git a/content/docs/references/system/OAuthProvider.mdx b/content/docs/references/system/OAuthProvider.mdx deleted file mode 100644 index 32ff86e83..000000000 --- a/content/docs/references/system/OAuthProvider.mdx +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: OAuthProvider -description: OAuthProvider Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **provider** | `Enum<'google' \| 'github' \| 'facebook' \| 'twitter' \| 'linkedin' \| 'microsoft' \| 'apple' \| 'discord' \| 'gitlab' \| 'custom'>` | ✅ | OAuth provider type | -| **clientId** | `string` | ✅ | OAuth client ID | -| **clientSecret** | `string` | ✅ | OAuth client secret (typically from ENV) | -| **scopes** | `string[]` | optional | Requested OAuth scopes | -| **redirectUri** | `string` | optional | OAuth callback URL | -| **enabled** | `boolean` | optional | Whether this provider is enabled | -| **displayName** | `string` | optional | Display name for the provider button | -| **icon** | `string` | optional | Icon URL or identifier | diff --git a/content/docs/references/system/PasskeyConfig.mdx b/content/docs/references/system/PasskeyConfig.mdx deleted file mode 100644 index 9e305ed4c..000000000 --- a/content/docs/references/system/PasskeyConfig.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: PasskeyConfig -description: PasskeyConfig Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | optional | | -| **rpName** | `string` | ✅ | Relying Party name | -| **rpId** | `string` | optional | Relying Party ID (defaults to domain) | -| **allowedOrigins** | `string[]` | optional | Allowed origins for WebAuthn | -| **userVerification** | `Enum<'required' \| 'preferred' \| 'discouraged'>` | optional | | -| **attestation** | `Enum<'none' \| 'indirect' \| 'direct' \| 'enterprise'>` | optional | | diff --git a/content/docs/references/system/RateLimitConfig.mdx b/content/docs/references/system/RateLimitConfig.mdx deleted file mode 100644 index 6682505c5..000000000 --- a/content/docs/references/system/RateLimitConfig.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: RateLimitConfig -description: RateLimitConfig Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | optional | | -| **maxAttempts** | `number` | optional | Maximum login attempts | -| **windowMs** | `number` | optional | Time window in milliseconds (default 15 min) | -| **blockDuration** | `number` | optional | Block duration after max attempts in ms | -| **skipSuccessfulRequests** | `boolean` | optional | Only count failed requests | diff --git a/content/docs/references/system/SessionConfig.mdx b/content/docs/references/system/SessionConfig.mdx deleted file mode 100644 index 5fb4b1c25..000000000 --- a/content/docs/references/system/SessionConfig.mdx +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: SessionConfig -description: SessionConfig Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **expiresIn** | `number` | optional | Session expiry in seconds (default 7 days) | -| **updateAge** | `number` | optional | Session update interval in seconds (default 1 day) | -| **cookieName** | `string` | optional | Session cookie name | -| **cookieSecure** | `boolean` | optional | Use secure cookies (HTTPS only) | -| **cookieSameSite** | `Enum<'strict' \| 'lax' \| 'none'>` | optional | SameSite cookie attribute | -| **cookieDomain** | `string` | optional | Cookie domain | -| **cookiePath** | `string` | optional | Cookie path | -| **cookieHttpOnly** | `boolean` | optional | HttpOnly cookie attribute | diff --git a/content/docs/references/system/TwoFactorConfig.mdx b/content/docs/references/system/TwoFactorConfig.mdx deleted file mode 100644 index 70add73db..000000000 --- a/content/docs/references/system/TwoFactorConfig.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: TwoFactorConfig -description: TwoFactorConfig Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | optional | | -| **issuer** | `string` | optional | TOTP issuer name | -| **qrCodeSize** | `number` | optional | QR code size in pixels | -| **backupCodes** | `object` | optional | | diff --git a/content/docs/references/system/UserFieldMapping.mdx b/content/docs/references/system/UserFieldMapping.mdx deleted file mode 100644 index bfbf94d14..000000000 --- a/content/docs/references/system/UserFieldMapping.mdx +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: UserFieldMapping -description: UserFieldMapping Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **id** | `string` | optional | User ID field | -| **email** | `string` | optional | Email field | -| **name** | `string` | optional | Name field | -| **image** | `string` | optional | Profile image field | -| **emailVerified** | `string` | optional | Email verification status field | -| **createdAt** | `string` | optional | Created timestamp field | -| **updatedAt** | `string` | optional | Updated timestamp field | diff --git a/docs/BETTER_AUTH_PLUGIN.md b/docs/BETTER_AUTH_PLUGIN.md index 4299ccf9d..1c8290921 100644 --- a/docs/BETTER_AUTH_PLUGIN.md +++ b/docs/BETTER_AUTH_PLUGIN.md @@ -1,10 +1,10 @@ # Better-Auth Authentication Plugin -A comprehensive authentication specification for integrating [better-auth](https://better-auth.com) into the ObjectStack ecosystem. +A comprehensive authentication plugin for integrating [better-auth](https://better-auth.com) into the ObjectStack ecosystem. ## Overview -This specification defines the schema and types for configuring better-auth as an authentication provider in ObjectStack applications. Better-auth is a modern, framework-agnostic authentication library that provides multiple authentication strategies, session management, and comprehensive security features. +This plugin provides a complete authentication solution using better-auth as the underlying authentication library. Better-auth is a modern, framework-agnostic authentication library that supports multiple authentication strategies, session management, and comprehensive security features. ## Features @@ -44,7 +44,7 @@ This specification defines the schema and types for configuring better-auth as a ## Installation ```bash -pnpm add @objectstack/spec +pnpm add @objectstack/plugin-better-auth ``` ## Usage @@ -52,11 +52,9 @@ pnpm add @objectstack/spec ### Basic Example ```typescript -import type { BetterAuthConfig } from '@objectstack/spec'; +import { createBetterAuthPlugin } from '@objectstack/plugin-better-auth'; -const authConfig: BetterAuthConfig = { - name: 'main_auth', - label: 'Main Authentication', +const authPlugin = createBetterAuthPlugin({ strategies: ['email_password'], baseUrl: 'https://app.example.com', secret: process.env.AUTH_SECRET!, @@ -66,12 +64,9 @@ const authConfig: BetterAuthConfig = { requireEmailVerification: true, minPasswordLength: 8, }, - - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, -}; +}); + +export default authPlugin; ``` ### OAuth Example diff --git a/examples/auth-better-examples.ts b/examples/auth-better-examples.ts deleted file mode 100644 index 81fd184ba..000000000 --- a/examples/auth-better-examples.ts +++ /dev/null @@ -1,377 +0,0 @@ -/** - * Better-Auth Plugin Example - * - * This example demonstrates how to configure a better-auth authentication provider - * for an ObjectStack application. - * - * @see https://better-auth.com for more information about better-auth - */ - -import type { BetterAuthConfig, BetterAuthProvider } from '@objectstack/spec'; - -/** - * Example 1: Basic Email/Password Authentication - * - * Simplest configuration with just email and password login. - */ -export const basicEmailAuth: BetterAuthConfig = { - name: 'basic_auth', - label: 'Email Login', - strategies: ['email_password'], - baseUrl: 'https://app.example.com', - secret: process.env.AUTH_SECRET || 'your-secret-key-min-32-characters-long', - - emailPassword: { - enabled: true, - requireEmailVerification: true, - minPasswordLength: 8, - requirePasswordComplexity: true, - }, - - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, -}; - -/** - * Example 2: OAuth with Google and GitHub - * - * Allows users to sign in with Google or GitHub accounts. - */ -export const oauthAuth: BetterAuthConfig = { - name: 'social_auth', - label: 'Social Login', - strategies: ['oauth'], - baseUrl: 'https://app.example.com', - secret: process.env.AUTH_SECRET!, - - oauth: { - providers: [ - { - provider: 'google', - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - scopes: ['openid', 'profile', 'email'], - enabled: true, - displayName: 'Sign in with Google', - }, - { - provider: 'github', - clientId: process.env.GITHUB_CLIENT_ID!, - clientSecret: process.env.GITHUB_CLIENT_SECRET!, - scopes: ['user:email'], - enabled: true, - displayName: 'Sign in with GitHub', - }, - ], - }, - - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: { - enabled: true, - autoLink: true, - requireVerification: false, - }, -}; - -/** - * Example 3: Multi-Strategy Authentication - * - * Comprehensive configuration supporting multiple authentication methods. - */ -export const comprehensiveAuth: BetterAuthConfig = { - name: 'main_auth', - label: 'Main Authentication', - strategies: ['email_password', 'oauth', 'magic_link', 'passkey'], - baseUrl: 'https://app.example.com', - secret: process.env.AUTH_SECRET!, - - // Email & Password Configuration - emailPassword: { - enabled: true, - requireEmailVerification: true, - minPasswordLength: 10, - requirePasswordComplexity: true, - allowPasswordReset: true, - passwordResetExpiry: 3600, // 1 hour - }, - - // Magic Link Configuration - magicLink: { - enabled: true, - expiryTime: 900, // 15 minutes - }, - - // Passkey (WebAuthn) Configuration - passkey: { - enabled: true, - rpName: 'My App', - rpId: 'app.example.com', - userVerification: 'preferred', - attestation: 'none', - }, - - // OAuth Providers - oauth: { - providers: [ - { - provider: 'google', - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - enabled: true, - }, - { - provider: 'github', - clientId: process.env.GITHUB_CLIENT_ID!, - clientSecret: process.env.GITHUB_CLIENT_SECRET!, - enabled: true, - }, - ], - }, - - // Session Configuration - session: { - expiresIn: 604800, // 7 days - updateAge: 86400, // 1 day - cookieName: 'app_session', - cookieSecure: true, - cookieSameSite: 'lax', - }, - - // Rate Limiting - rateLimit: { - enabled: true, - maxAttempts: 5, - windowMs: 900000, // 15 minutes - blockDuration: 900000, // 15 minutes - }, - - // CSRF Protection - csrf: { - enabled: true, - tokenLength: 32, - }, - - // Account Linking - accountLinking: { - enabled: true, - autoLink: false, - requireVerification: true, - }, - - // Two-Factor Authentication - twoFactor: { - enabled: true, - issuer: 'My App', - qrCodeSize: 256, - backupCodes: { - enabled: true, - count: 10, - }, - }, - - // Database Configuration - database: { - type: 'prisma', - tablePrefix: 'auth_', - }, - - // Lifecycle Hooks - hooks: { - beforeSignIn: async ({ email }) => { - console.log(`User ${email} attempting to sign in`); - }, - - afterSignIn: async ({ user, session }) => { - console.log(`User ${user.id} signed in successfully`); - // You could log this event, update last login time, etc. - }, - - afterSignUp: async ({ user }) => { - console.log(`New user registered: ${user.email}`); - // You could send a welcome email, create default data, etc. - }, - }, - - // Security Settings - security: { - allowedOrigins: ['https://app.example.com', 'https://admin.example.com'], - trustProxy: true, - ipRateLimiting: true, - sessionFingerprinting: true, - maxSessions: 5, - }, - - // Email Configuration - email: { - from: 'noreply@example.com', - fromName: 'My App', - provider: 'sendgrid', - config: { - apiKey: process.env.SENDGRID_API_KEY, - }, - }, - - // UI Customization - ui: { - brandName: 'My App', - logo: 'https://app.example.com/logo.png', - primaryColor: '#007bff', - }, - - active: true, - allowRegistration: true, -}; - -/** - * Example 4: Production-Ready Configuration - * - * Enterprise-grade authentication with all security features enabled. - */ -export const productionAuth: BetterAuthConfig = { - name: 'production_auth', - label: 'Production Authentication', - strategies: ['email_password', 'oauth'], - baseUrl: process.env.APP_URL!, - secret: process.env.AUTH_SECRET!, - - emailPassword: { - enabled: true, - requireEmailVerification: true, - minPasswordLength: 12, - requirePasswordComplexity: true, - allowPasswordReset: true, - passwordResetExpiry: 3600, - }, - - oauth: { - providers: [ - { - provider: 'google', - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - scopes: ['openid', 'profile', 'email'], - enabled: true, - }, - ], - }, - - session: { - expiresIn: 86400 * 30, // 30 days - updateAge: 86400, // 1 day - cookieSecure: true, - cookieSameSite: 'strict', - cookieHttpOnly: true, - }, - - rateLimit: { - enabled: true, - maxAttempts: 3, - windowMs: 600000, // 10 minutes - blockDuration: 1800000, // 30 minutes - }, - - csrf: { - enabled: true, - tokenLength: 64, - }, - - accountLinking: { - enabled: true, - autoLink: false, - requireVerification: true, - }, - - twoFactor: { - enabled: true, - issuer: process.env.APP_NAME!, - backupCodes: { - enabled: true, - count: 10, - }, - }, - - security: { - allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') || [], - trustProxy: true, - ipRateLimiting: true, - sessionFingerprinting: true, - maxSessions: 3, - }, - - database: { - type: 'prisma', - connectionString: process.env.DATABASE_URL, - tablePrefix: 'auth_', - }, - - email: { - from: process.env.EMAIL_FROM!, - fromName: process.env.APP_NAME!, - provider: 'sendgrid', - config: { - apiKey: process.env.SENDGRID_API_KEY, - }, - }, - - hooks: { - beforeSignIn: async ({ email }) => { - // Log authentication attempts - console.log(`[AUTH] Sign in attempt: ${email}`); - }, - - afterSignIn: async ({ user, session }) => { - // Audit logging - console.log(`[AUTH] User ${user.id} authenticated`); - }, - - afterSignUp: async ({ user }) => { - // Send welcome email, create default records, etc. - console.log(`[AUTH] New user registered: ${user.id}`); - }, - }, - - active: true, - allowRegistration: false, // Registration disabled for security -}; - -/** - * Example 5: Better-Auth Provider Wrapper - * - * Wrapping the configuration in the provider schema for use in identity system. - */ -export const betterAuthProvider: BetterAuthProvider = { - type: 'better_auth', - config: comprehensiveAuth, -}; - -/** - * Example 6: Plugin Configuration for ObjectStack Manifest - * - * How to include better-auth in an ObjectStack plugin manifest. - */ -export const authPluginManifest = { - id: 'com.example.auth', - name: 'Authentication Plugin', - version: '1.0.0', - type: 'plugin' as const, - description: 'Better-auth based authentication for ObjectStack', - - configuration: { - title: 'Authentication Settings', - properties: { - authConfig: { - type: 'object' as const, - description: 'Better-auth configuration', - required: true, - }, - }, - }, - - contributes: { - events: ['auth.signin', 'auth.signout', 'auth.signup'], - }, -}; diff --git a/packages/plugin-better-auth/CHANGELOG.md b/packages/plugin-better-auth/CHANGELOG.md new file mode 100644 index 000000000..c1f89db31 --- /dev/null +++ b/packages/plugin-better-auth/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## [1.0.0] - 2026-01-21 + +### Added +- Initial release of Better-Auth authentication plugin +- Support for multiple authentication strategies (email/password, OAuth, magic link, passkey, OTP, anonymous) +- 10+ OAuth providers (Google, GitHub, Facebook, Twitter, LinkedIn, Microsoft, Apple, Discord, GitLab, custom) +- Advanced security features (rate limiting, CSRF protection, 2FA, session fingerprinting) +- Database adapter support (Prisma, Drizzle, Kysely, custom) +- Email provider integration (SMTP, SendGrid, Mailgun, AWS SES, Resend, custom) +- Lifecycle hooks for authentication events +- Comprehensive examples and documentation diff --git a/packages/plugin-better-auth/README.md b/packages/plugin-better-auth/README.md new file mode 100644 index 000000000..638b23e2a --- /dev/null +++ b/packages/plugin-better-auth/README.md @@ -0,0 +1,55 @@ +# @objectstack/plugin-better-auth + +Better-Auth authentication plugin for ObjectStack. + +## Installation + +```bash +pnpm add @objectstack/plugin-better-auth +``` + +## Usage + +```typescript +import { BetterAuthPlugin } from '@objectstack/plugin-better-auth'; + +const authPlugin = new BetterAuthPlugin({ + strategies: ['email_password', 'oauth'], + baseUrl: 'https://app.example.com', + secret: process.env.AUTH_SECRET!, + + emailPassword: { + enabled: true, + requireEmailVerification: true, + }, + + oauth: { + providers: [ + { + provider: 'google', + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }, + ], + }, +}); + +export default authPlugin; +``` + +## Features + +- Multiple authentication strategies (email/password, OAuth, magic links, passkeys, OTP, anonymous) +- 10+ OAuth providers (Google, GitHub, Facebook, Twitter, LinkedIn, Microsoft, Apple, Discord, GitLab) +- Advanced security features (rate limiting, CSRF protection, 2FA, session fingerprinting) +- Database adapter support (Prisma, Drizzle, Kysely) +- Email provider integration (SMTP, SendGrid, Mailgun, AWS SES, Resend) +- Lifecycle hooks for authentication events + +## Documentation + +See [docs/BETTER_AUTH_PLUGIN.md](../../docs/BETTER_AUTH_PLUGIN.md) for comprehensive documentation. + +## License + +Apache-2.0 diff --git a/packages/plugin-better-auth/examples/plugin-examples.ts b/packages/plugin-better-auth/examples/plugin-examples.ts new file mode 100644 index 000000000..c0572278e --- /dev/null +++ b/packages/plugin-better-auth/examples/plugin-examples.ts @@ -0,0 +1,127 @@ +/** + * Better-Auth Plugin Examples + * + * This file demonstrates various configurations for the Better-Auth plugin. + */ + +import { createBetterAuthPlugin } from '../src/index'; + +/** + * Example 1: Basic Email/Password Authentication + */ +export const basicEmailAuthPlugin = createBetterAuthPlugin({ + strategies: ['email_password'], + baseUrl: 'https://app.example.com', + secret: process.env.AUTH_SECRET || 'your-secret-key-min-32-characters-long', + + emailPassword: { + enabled: true, + requireEmailVerification: true, + minPasswordLength: 8, + requirePasswordComplexity: true, + allowPasswordReset: true, + }, + + session: { + expiresIn: 604800, // 7 days + cookieSecure: true, + cookieSameSite: 'lax', + }, + + rateLimit: { + enabled: true, + maxAttempts: 5, + windowMs: 900000, // 15 minutes + }, + + csrf: { + enabled: true, + }, +}); + +/** + * Example 2: OAuth with Social Providers + */ +export const socialAuthPlugin = createBetterAuthPlugin({ + strategies: ['oauth'], + baseUrl: 'https://app.example.com', + secret: process.env.AUTH_SECRET!, + + oauth: { + providers: [ + { + provider: 'google', + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + scopes: ['openid', 'profile', 'email'], + enabled: true, + displayName: 'Sign in with Google', + }, + { + provider: 'github', + clientId: process.env.GITHUB_CLIENT_ID!, + clientSecret: process.env.GITHUB_CLIENT_SECRET!, + scopes: ['user:email'], + enabled: true, + displayName: 'Sign in with GitHub', + }, + ], + }, + + accountLinking: { + enabled: true, + autoLink: true, + }, + + session: { + expiresIn: 2592000, // 30 days + }, +}); + +/** + * Example 3: Multi-Strategy Authentication + */ +export const multiStrategyPlugin = createBetterAuthPlugin({ + strategies: ['email_password', 'oauth', 'magic_link'], + baseUrl: 'https://app.example.com', + secret: process.env.AUTH_SECRET!, + + emailPassword: { + enabled: true, + requireEmailVerification: true, + minPasswordLength: 10, + }, + + oauth: { + providers: [ + { + provider: 'google', + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }, + ], + }, + + magicLink: { + enabled: true, + expiryTime: 900, // 15 minutes + }, + + hooks: { + beforeSignIn: async ({ email }) => { + console.log(`User ${email} attempting to sign in`); + }, + + afterSignIn: async ({ user, session }) => { + console.log(`User ${user.id} signed in successfully`); + }, + + afterSignUp: async ({ user }) => { + console.log(`New user registered: ${user.email}`); + // Send welcome email, create default data, etc. + }, + }, +}); + +// Export default for easy importing +export default multiStrategyPlugin; diff --git a/packages/plugin-better-auth/objectstack.config.ts b/packages/plugin-better-auth/objectstack.config.ts new file mode 100644 index 000000000..29a5bd402 --- /dev/null +++ b/packages/plugin-better-auth/objectstack.config.ts @@ -0,0 +1,127 @@ +import { ObjectStackManifest } from '@objectstack/spec'; + +const manifest: ObjectStackManifest = { + id: 'com.objectstack.plugin.better-auth', + name: 'Better-Auth Authentication Plugin', + version: '1.0.0', + type: 'plugin', + description: 'Modern authentication plugin powered by better-auth, supporting multiple strategies including email/password, OAuth, magic links, passkeys, and more.', + + // Required permissions + permissions: [ + 'system.user.read', + 'system.user.write', + 'system.session.manage', + 'system.routes.register', + ], + + // Plugin configuration schema + configuration: { + title: 'Better-Auth Configuration', + properties: { + strategies: { + type: 'array', + description: 'Enabled authentication strategies', + required: true, + }, + baseUrl: { + type: 'string', + description: 'Application base URL', + required: true, + }, + secret: { + type: 'string', + description: 'Secret key for signing tokens (min 32 characters)', + required: true, + secret: true, + }, + emailPassword: { + type: 'object', + description: 'Email/Password authentication configuration', + }, + oauth: { + type: 'object', + description: 'OAuth providers configuration', + }, + session: { + type: 'object', + description: 'Session management configuration', + }, + rateLimit: { + type: 'object', + description: 'Rate limiting configuration', + }, + csrf: { + type: 'object', + description: 'CSRF protection configuration', + }, + twoFactor: { + type: 'object', + description: 'Two-factor authentication configuration', + }, + database: { + type: 'object', + description: 'Database adapter configuration', + }, + }, + }, + + // Platform contributions + contributes: { + events: [ + 'auth.before_signin', + 'auth.after_signin', + 'auth.before_signup', + 'auth.after_signup', + 'auth.before_signout', + 'auth.after_signout', + 'auth.session_created', + 'auth.session_expired', + ], + + actions: [ + { + name: 'authenticate_user', + label: 'Authenticate User', + description: 'Authenticate a user with the provided credentials', + input: { + email: 'string', + password: 'string', + }, + output: { + user: 'object', + session: 'object', + }, + }, + { + name: 'send_magic_link', + label: 'Send Magic Link', + description: 'Send a magic link to the user\'s email', + input: { + email: 'string', + }, + }, + { + name: 'verify_session', + label: 'Verify Session', + description: 'Verify if a session is valid', + input: { + sessionId: 'string', + }, + output: { + valid: 'boolean', + user: 'object', + }, + }, + ], + }, + + // Runtime entry point + extensions: { + runtime: { + entry: './dist/index.js', + }, + }, +}; + +export default manifest; diff --git a/packages/plugin-better-auth/package.json b/packages/plugin-better-auth/package.json new file mode 100644 index 000000000..d4dcc7a53 --- /dev/null +++ b/packages/plugin-better-auth/package.json @@ -0,0 +1,33 @@ +{ + "name": "@objectstack/plugin-better-auth", + "version": "1.0.0", + "description": "Better-Auth authentication plugin for ObjectStack", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test": "vitest run", + "test:watch": "vitest" + }, + "keywords": [ + "objectstack", + "plugin", + "authentication", + "better-auth", + "auth" + ], + "author": "ObjectStack", + "license": "Apache-2.0", + "dependencies": { + "@objectstack/spec": "workspace:*", + "better-auth": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0", + "vitest": "^2.0.0" + }, + "peerDependencies": { + "@objectstack/runtime": "^0.1.0" + } +} diff --git a/packages/plugin-better-auth/src/index.ts b/packages/plugin-better-auth/src/index.ts new file mode 100644 index 000000000..428afa3b7 --- /dev/null +++ b/packages/plugin-better-auth/src/index.ts @@ -0,0 +1,365 @@ +import { PluginDefinition, PluginContextData } from '@objectstack/spec'; + +/** + * Better-Auth Configuration Interface + * + * Configuration options for the better-auth authentication plugin. + */ +export interface BetterAuthConfig { + /** Enabled authentication strategies */ + strategies: Array<'email_password' | 'magic_link' | 'oauth' | 'passkey' | 'otp' | 'anonymous'>; + + /** Application base URL */ + baseUrl: string; + + /** Secret key for signing tokens (min 32 characters) */ + secret: string; + + /** Email/Password configuration */ + emailPassword?: { + enabled?: boolean; + requireEmailVerification?: boolean; + minPasswordLength?: number; + requirePasswordComplexity?: boolean; + allowPasswordReset?: boolean; + passwordResetExpiry?: number; + }; + + /** Magic Link configuration */ + magicLink?: { + enabled?: boolean; + expiryTime?: number; + }; + + /** Passkey (WebAuthn) configuration */ + passkey?: { + enabled?: boolean; + rpName: string; + rpId?: string; + userVerification?: 'required' | 'preferred' | 'discouraged'; + attestation?: 'none' | 'indirect' | 'direct' | 'enterprise'; + }; + + /** OAuth configuration */ + oauth?: { + providers: Array<{ + provider: 'google' | 'github' | 'facebook' | 'twitter' | 'linkedin' | 'microsoft' | 'apple' | 'discord' | 'gitlab' | 'custom'; + clientId: string; + clientSecret: string; + scopes?: string[]; + redirectUri?: string; + enabled?: boolean; + displayName?: string; + icon?: string; + }>; + }; + + /** Session configuration */ + session?: { + expiresIn?: number; + updateAge?: number; + cookieName?: string; + cookieSecure?: boolean; + cookieSameSite?: 'strict' | 'lax' | 'none'; + cookieDomain?: string; + cookiePath?: string; + cookieHttpOnly?: boolean; + }; + + /** Rate limiting configuration */ + rateLimit?: { + enabled?: boolean; + maxAttempts?: number; + windowMs?: number; + blockDuration?: number; + skipSuccessfulRequests?: boolean; + }; + + /** CSRF protection configuration */ + csrf?: { + enabled?: boolean; + tokenLength?: number; + cookieName?: string; + headerName?: string; + }; + + /** Account linking configuration */ + accountLinking?: { + enabled?: boolean; + autoLink?: boolean; + requireVerification?: boolean; + }; + + /** Two-factor authentication configuration */ + twoFactor?: { + enabled?: boolean; + issuer?: string; + qrCodeSize?: number; + backupCodes?: { + enabled?: boolean; + count?: number; + }; + }; + + /** Database adapter configuration */ + database?: { + type: 'prisma' | 'drizzle' | 'kysely' | 'custom'; + connectionString?: string; + tablePrefix?: string; + schema?: string; + }; + + /** Lifecycle hooks */ + hooks?: { + beforeSignIn?: (data: { email: string }) => Promise; + afterSignIn?: (data: { user: any; session: any }) => Promise; + beforeSignUp?: (data: { email: string; name?: string }) => Promise; + afterSignUp?: (data: { user: any }) => Promise; + beforeSignOut?: (data: { sessionId: string }) => Promise; + afterSignOut?: (data: { sessionId: string }) => Promise; + }; + + /** Security settings */ + security?: { + allowedOrigins?: string[]; + trustProxy?: boolean; + ipRateLimiting?: boolean; + sessionFingerprinting?: boolean; + maxSessions?: number; + }; + + /** Email configuration */ + email?: { + from: string; + fromName?: string; + provider: 'smtp' | 'sendgrid' | 'mailgun' | 'ses' | 'resend' | 'custom'; + config?: Record; + }; +} + +/** + * Better-Auth Plugin Class + * + * Integrates better-auth authentication library into ObjectStack applications. + */ +export class BetterAuthPlugin implements PluginDefinition { + id = 'com.objectstack.plugin.better-auth'; + version = '1.0.0'; + + private config: BetterAuthConfig; + private authInstance: any; // better-auth instance + + constructor(config: BetterAuthConfig) { + this.config = config; + } + + /** + * Called when the plugin is installed + */ + async onInstall(context: PluginContextData): Promise { + const { logger, ql } = context; + + logger.info('[Better-Auth] Installing plugin...'); + + // Create auth tables in database if needed + if (this.config.database) { + logger.info('[Better-Auth] Setting up database tables...'); + // In a real implementation, this would create the necessary tables + } + + logger.info('[Better-Auth] Plugin installed successfully'); + } + + /** + * Called when the plugin is enabled + */ + async onEnable(context: PluginContextData): Promise { + const { logger, app, storage, os } = context; + + logger.info('[Better-Auth] Enabling plugin...'); + + // Initialize better-auth (in real implementation, would use actual better-auth library) + this.authInstance = this.initializeAuth(); + + // Register authentication routes + this.registerRoutes(app.router, logger); + + // Store configuration + await storage.set('better-auth:config', this.config); + + logger.info('[Better-Auth] Plugin enabled successfully'); + logger.info(`[Better-Auth] Strategies: ${this.config.strategies.join(', ')}`); + } + + /** + * Called when the plugin is disabled + */ + async onDisable(context: PluginContextData): Promise { + const { logger } = context; + + logger.info('[Better-Auth] Disabling plugin...'); + + // Cleanup resources + this.authInstance = null; + + logger.info('[Better-Auth] Plugin disabled'); + } + + /** + * Called when the plugin is uninstalled + */ + async onUninstall(context: PluginContextData): Promise { + const { logger, storage } = context; + + logger.info('[Better-Auth] Uninstalling plugin...'); + + // Remove stored configuration + await storage.delete('better-auth:config'); + + // In a real implementation, might ask user if they want to keep database tables + + logger.info('[Better-Auth] Plugin uninstalled'); + } + + /** + * Initialize the better-auth instance + */ + private initializeAuth(): any { + // In a real implementation, this would create a better-auth instance + // with the provided configuration + + const auth = { + strategies: this.config.strategies, + config: this.config, + }; + + return auth; + } + + /** + * Register authentication routes + */ + private registerRoutes(router: any, logger: any): void { + // Sign In route + router.post('/auth/signin', async (req: any, res: any) => { + logger.info('[Better-Auth] Sign in request'); + + // Execute beforeSignIn hook + if (this.config.hooks?.beforeSignIn) { + await this.config.hooks.beforeSignIn({ email: req.body.email }); + } + + // In real implementation, would call better-auth sign in + const user = { id: '123', email: req.body.email }; + const session = { id: 'session-123', userId: '123' }; + + // Execute afterSignIn hook + if (this.config.hooks?.afterSignIn) { + await this.config.hooks.afterSignIn({ user, session }); + } + + return { success: true, user, session }; + }); + + // Sign Up route + router.post('/auth/signup', async (req: any, res: any) => { + logger.info('[Better-Auth] Sign up request'); + + // Execute beforeSignUp hook + if (this.config.hooks?.beforeSignUp) { + await this.config.hooks.beforeSignUp({ + email: req.body.email, + name: req.body.name + }); + } + + // In real implementation, would call better-auth sign up + const user = { id: '123', email: req.body.email, name: req.body.name }; + + // Execute afterSignUp hook + if (this.config.hooks?.afterSignUp) { + await this.config.hooks.afterSignUp({ user }); + } + + return { success: true, user }; + }); + + // Sign Out route + router.post('/auth/signout', async (req: any, res: any) => { + logger.info('[Better-Auth] Sign out request'); + + const sessionId = req.body.sessionId || 'session-123'; + + // Execute beforeSignOut hook + if (this.config.hooks?.beforeSignOut) { + await this.config.hooks.beforeSignOut({ sessionId }); + } + + // In real implementation, would call better-auth sign out + + // Execute afterSignOut hook + if (this.config.hooks?.afterSignOut) { + await this.config.hooks.afterSignOut({ sessionId }); + } + + return { success: true }; + }); + + // OAuth routes (if OAuth is enabled) + if (this.config.oauth) { + this.config.oauth.providers.forEach((provider) => { + if (provider.enabled !== false) { + router.get(`/auth/oauth/${provider.provider}`, async (req: any, res: any) => { + logger.info(`[Better-Auth] OAuth redirect for ${provider.provider}`); + // In real implementation, would redirect to OAuth provider + return { redirect: `https://${provider.provider}.com/oauth/authorize` }; + }); + + router.get(`/auth/oauth/${provider.provider}/callback`, async (req: any, res: any) => { + logger.info(`[Better-Auth] OAuth callback for ${provider.provider}`); + // In real implementation, would handle OAuth callback + return { success: true }; + }); + } + }); + } + + // Magic Link routes (if magic link is enabled) + if (this.config.strategies.includes('magic_link')) { + router.post('/auth/magic-link/send', async (req: any, res: any) => { + logger.info('[Better-Auth] Sending magic link'); + // In real implementation, would send magic link email + return { success: true, message: 'Magic link sent' }; + }); + + router.get('/auth/magic-link/verify', async (req: any, res: any) => { + logger.info('[Better-Auth] Verifying magic link'); + // In real implementation, would verify magic link token + return { success: true }; + }); + } + + // Status route + router.get('/auth/status', async () => { + return { + status: 'active', + strategies: this.config.strategies, + version: this.version, + }; + }); + + logger.info('[Better-Auth] Routes registered successfully'); + } +} + +/** + * Create a Better-Auth plugin instance + */ +export function createBetterAuthPlugin(config: BetterAuthConfig): PluginDefinition { + return new BetterAuthPlugin(config); +} + +/** + * Default export following ObjectStack plugin conventions + */ +export default createBetterAuthPlugin; diff --git a/packages/plugin-better-auth/tsconfig.json b/packages/plugin-better-auth/tsconfig.json new file mode 100644 index 000000000..1f147cc4d --- /dev/null +++ b/packages/plugin-better-auth/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/spec/json-schema/AccountLinkingConfig.json b/packages/spec/json-schema/AccountLinkingConfig.json deleted file mode 100644 index f635ac7cc..000000000 --- a/packages/spec/json-schema/AccountLinkingConfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$ref": "#/definitions/AccountLinkingConfig", - "definitions": { - "AccountLinkingConfig": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Allow account linking" - }, - "autoLink": { - "type": "boolean", - "default": false, - "description": "Automatically link accounts with same email" - }, - "requireVerification": { - "type": "boolean", - "default": true, - "description": "Require email verification before linking" - } - }, - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/BetterAuthConfig.json b/packages/spec/json-schema/BetterAuthConfig.json deleted file mode 100644 index c486939eb..000000000 --- a/packages/spec/json-schema/BetterAuthConfig.json +++ /dev/null @@ -1,601 +0,0 @@ -{ - "$ref": "#/definitions/BetterAuthConfig", - "definitions": { - "BetterAuthConfig": { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "^[a-z_][a-z0-9_]*$", - "description": "Configuration name (snake_case)" - }, - "label": { - "type": "string", - "description": "Display label" - }, - "strategies": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "email_password", - "magic_link", - "oauth", - "passkey", - "otp", - "anonymous" - ] - }, - "minItems": 1, - "description": "Enabled authentication strategies" - }, - "baseUrl": { - "type": "string", - "format": "uri", - "description": "Application base URL" - }, - "secret": { - "type": "string", - "minLength": 32, - "description": "Secret key for signing (min 32 chars)" - }, - "emailPassword": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "requireEmailVerification": { - "type": "boolean", - "default": true, - "description": "Require email verification before login" - }, - "minPasswordLength": { - "type": "number", - "minimum": 6, - "maximum": 128, - "default": 8, - "description": "Minimum password length" - }, - "requirePasswordComplexity": { - "type": "boolean", - "default": true, - "description": "Require uppercase, lowercase, numbers, symbols" - }, - "allowPasswordReset": { - "type": "boolean", - "default": true, - "description": "Enable password reset functionality" - }, - "passwordResetExpiry": { - "type": "number", - "default": 3600, - "description": "Password reset token expiry in seconds" - } - }, - "additionalProperties": false - }, - "magicLink": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "expiryTime": { - "type": "number", - "default": 900, - "description": "Magic link expiry time in seconds (default 15 min)" - } - }, - "additionalProperties": false - }, - "passkey": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false - }, - "rpName": { - "type": "string", - "description": "Relying Party name" - }, - "rpId": { - "type": "string", - "description": "Relying Party ID (defaults to domain)" - }, - "allowedOrigins": { - "type": "array", - "items": { - "type": "string", - "format": "uri" - }, - "description": "Allowed origins for WebAuthn" - }, - "userVerification": { - "type": "string", - "enum": [ - "required", - "preferred", - "discouraged" - ], - "default": "preferred" - }, - "attestation": { - "type": "string", - "enum": [ - "none", - "indirect", - "direct", - "enterprise" - ], - "default": "none" - } - }, - "required": [ - "rpName" - ], - "additionalProperties": false - }, - "oauth": { - "type": "object", - "properties": { - "providers": { - "type": "array", - "items": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "enum": [ - "google", - "github", - "facebook", - "twitter", - "linkedin", - "microsoft", - "apple", - "discord", - "gitlab", - "custom" - ], - "description": "OAuth provider type" - }, - "clientId": { - "type": "string", - "description": "OAuth client ID" - }, - "clientSecret": { - "type": "string", - "description": "OAuth client secret (typically from ENV)" - }, - "scopes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Requested OAuth scopes" - }, - "redirectUri": { - "type": "string", - "format": "uri", - "description": "OAuth callback URL" - }, - "enabled": { - "type": "boolean", - "default": true, - "description": "Whether this provider is enabled" - }, - "displayName": { - "type": "string", - "description": "Display name for the provider button" - }, - "icon": { - "type": "string", - "description": "Icon URL or identifier" - } - }, - "required": [ - "provider", - "clientId", - "clientSecret" - ], - "additionalProperties": false - }, - "minItems": 1 - } - }, - "required": [ - "providers" - ], - "additionalProperties": false - }, - "session": { - "type": "object", - "properties": { - "expiresIn": { - "type": "number", - "default": 604800, - "description": "Session expiry in seconds (default 7 days)" - }, - "updateAge": { - "type": "number", - "default": 86400, - "description": "Session update interval in seconds (default 1 day)" - }, - "cookieName": { - "type": "string", - "default": "better-auth.session_token", - "description": "Session cookie name" - }, - "cookieSecure": { - "type": "boolean", - "default": true, - "description": "Use secure cookies (HTTPS only)" - }, - "cookieSameSite": { - "type": "string", - "enum": [ - "strict", - "lax", - "none" - ], - "default": "lax", - "description": "SameSite cookie attribute" - }, - "cookieDomain": { - "type": "string", - "description": "Cookie domain" - }, - "cookiePath": { - "type": "string", - "default": "/", - "description": "Cookie path" - }, - "cookieHttpOnly": { - "type": "boolean", - "default": true, - "description": "HttpOnly cookie attribute" - } - }, - "additionalProperties": false, - "default": {} - }, - "rateLimit": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "maxAttempts": { - "type": "number", - "default": 5, - "description": "Maximum login attempts" - }, - "windowMs": { - "type": "number", - "default": 900000, - "description": "Time window in milliseconds (default 15 min)" - }, - "blockDuration": { - "type": "number", - "default": 900000, - "description": "Block duration after max attempts in ms" - }, - "skipSuccessfulRequests": { - "type": "boolean", - "default": false, - "description": "Only count failed requests" - } - }, - "additionalProperties": false, - "default": {} - }, - "csrf": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "tokenLength": { - "type": "number", - "default": 32, - "description": "CSRF token length" - }, - "cookieName": { - "type": "string", - "default": "better-auth.csrf_token", - "description": "CSRF cookie name" - }, - "headerName": { - "type": "string", - "default": "X-CSRF-Token", - "description": "CSRF header name" - } - }, - "additionalProperties": false, - "default": {} - }, - "accountLinking": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Allow account linking" - }, - "autoLink": { - "type": "boolean", - "default": false, - "description": "Automatically link accounts with same email" - }, - "requireVerification": { - "type": "boolean", - "default": true, - "description": "Require email verification before linking" - } - }, - "additionalProperties": false, - "default": {} - }, - "twoFactor": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false - }, - "issuer": { - "type": "string", - "description": "TOTP issuer name" - }, - "qrCodeSize": { - "type": "number", - "default": 200, - "description": "QR code size in pixels" - }, - "backupCodes": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "count": { - "type": "number", - "default": 10, - "description": "Number of backup codes to generate" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "userFieldMapping": { - "type": "object", - "properties": { - "id": { - "type": "string", - "default": "id", - "description": "User ID field" - }, - "email": { - "type": "string", - "default": "email", - "description": "Email field" - }, - "name": { - "type": "string", - "default": "name", - "description": "Name field" - }, - "image": { - "type": "string", - "default": "image", - "description": "Profile image field" - }, - "emailVerified": { - "type": "string", - "default": "email_verified", - "description": "Email verification status field" - }, - "createdAt": { - "type": "string", - "default": "created_at", - "description": "Created timestamp field" - }, - "updatedAt": { - "type": "string", - "default": "updated_at", - "description": "Updated timestamp field" - } - }, - "additionalProperties": false, - "default": {} - }, - "database": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "prisma", - "drizzle", - "kysely", - "custom" - ], - "description": "Database adapter type" - }, - "connectionString": { - "type": "string", - "description": "Database connection string" - }, - "tablePrefix": { - "type": "string", - "default": "auth_", - "description": "Prefix for auth tables" - }, - "schema": { - "type": "string", - "description": "Database schema name" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "plugins": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Plugin name" - }, - "enabled": { - "type": "boolean", - "default": true - }, - "options": { - "type": "object", - "additionalProperties": {}, - "description": "Plugin-specific options" - } - }, - "required": [ - "name" - ], - "additionalProperties": false - }, - "default": [] - }, - "hooks": { - "type": "object", - "properties": {}, - "additionalProperties": false, - "description": "Authentication lifecycle hooks" - }, - "security": { - "type": "object", - "properties": { - "allowedOrigins": { - "type": "array", - "items": { - "type": "string" - }, - "description": "CORS allowed origins" - }, - "trustProxy": { - "type": "boolean", - "default": false, - "description": "Trust proxy headers" - }, - "ipRateLimiting": { - "type": "boolean", - "default": true, - "description": "Enable IP-based rate limiting" - }, - "sessionFingerprinting": { - "type": "boolean", - "default": true, - "description": "Enable session fingerprinting" - }, - "maxSessions": { - "type": "number", - "default": 5, - "description": "Maximum concurrent sessions per user" - } - }, - "additionalProperties": false, - "description": "Advanced security settings" - }, - "email": { - "type": "object", - "properties": { - "from": { - "type": "string", - "format": "email", - "description": "From email address" - }, - "fromName": { - "type": "string", - "description": "From name" - }, - "provider": { - "type": "string", - "enum": [ - "smtp", - "sendgrid", - "mailgun", - "ses", - "resend", - "custom" - ], - "description": "Email provider" - }, - "config": { - "type": "object", - "additionalProperties": {}, - "description": "Provider-specific configuration" - } - }, - "required": [ - "from", - "provider" - ], - "additionalProperties": false, - "description": "Email configuration" - }, - "ui": { - "type": "object", - "properties": { - "brandName": { - "type": "string", - "description": "Brand name displayed in auth UI" - }, - "logo": { - "type": "string", - "description": "Logo URL" - }, - "primaryColor": { - "type": "string", - "description": "Primary brand color (hex)" - }, - "customCss": { - "type": "string", - "description": "Custom CSS for auth pages" - } - }, - "additionalProperties": false, - "description": "UI customization" - }, - "active": { - "type": "boolean", - "default": true, - "description": "Whether this provider is active" - }, - "allowRegistration": { - "type": "boolean", - "default": true, - "description": "Allow new user registration" - } - }, - "required": [ - "name", - "label", - "strategies", - "baseUrl", - "secret" - ], - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/BetterAuthPluginConfig.json b/packages/spec/json-schema/BetterAuthPluginConfig.json deleted file mode 100644 index 20aed5a85..000000000 --- a/packages/spec/json-schema/BetterAuthPluginConfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$ref": "#/definitions/BetterAuthPluginConfig", - "definitions": { - "BetterAuthPluginConfig": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Plugin name" - }, - "enabled": { - "type": "boolean", - "default": true - }, - "options": { - "type": "object", - "additionalProperties": {}, - "description": "Plugin-specific options" - } - }, - "required": [ - "name" - ], - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/BetterAuthProvider.json b/packages/spec/json-schema/BetterAuthProvider.json deleted file mode 100644 index 3d32dfbd5..000000000 --- a/packages/spec/json-schema/BetterAuthProvider.json +++ /dev/null @@ -1,617 +0,0 @@ -{ - "$ref": "#/definitions/BetterAuthProvider", - "definitions": { - "BetterAuthProvider": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "better_auth", - "description": "Provider type identifier" - }, - "config": { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "^[a-z_][a-z0-9_]*$", - "description": "Configuration name (snake_case)" - }, - "label": { - "type": "string", - "description": "Display label" - }, - "strategies": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "email_password", - "magic_link", - "oauth", - "passkey", - "otp", - "anonymous" - ] - }, - "minItems": 1, - "description": "Enabled authentication strategies" - }, - "baseUrl": { - "type": "string", - "format": "uri", - "description": "Application base URL" - }, - "secret": { - "type": "string", - "minLength": 32, - "description": "Secret key for signing (min 32 chars)" - }, - "emailPassword": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "requireEmailVerification": { - "type": "boolean", - "default": true, - "description": "Require email verification before login" - }, - "minPasswordLength": { - "type": "number", - "minimum": 6, - "maximum": 128, - "default": 8, - "description": "Minimum password length" - }, - "requirePasswordComplexity": { - "type": "boolean", - "default": true, - "description": "Require uppercase, lowercase, numbers, symbols" - }, - "allowPasswordReset": { - "type": "boolean", - "default": true, - "description": "Enable password reset functionality" - }, - "passwordResetExpiry": { - "type": "number", - "default": 3600, - "description": "Password reset token expiry in seconds" - } - }, - "additionalProperties": false - }, - "magicLink": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "expiryTime": { - "type": "number", - "default": 900, - "description": "Magic link expiry time in seconds (default 15 min)" - } - }, - "additionalProperties": false - }, - "passkey": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false - }, - "rpName": { - "type": "string", - "description": "Relying Party name" - }, - "rpId": { - "type": "string", - "description": "Relying Party ID (defaults to domain)" - }, - "allowedOrigins": { - "type": "array", - "items": { - "type": "string", - "format": "uri" - }, - "description": "Allowed origins for WebAuthn" - }, - "userVerification": { - "type": "string", - "enum": [ - "required", - "preferred", - "discouraged" - ], - "default": "preferred" - }, - "attestation": { - "type": "string", - "enum": [ - "none", - "indirect", - "direct", - "enterprise" - ], - "default": "none" - } - }, - "required": [ - "rpName" - ], - "additionalProperties": false - }, - "oauth": { - "type": "object", - "properties": { - "providers": { - "type": "array", - "items": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "enum": [ - "google", - "github", - "facebook", - "twitter", - "linkedin", - "microsoft", - "apple", - "discord", - "gitlab", - "custom" - ], - "description": "OAuth provider type" - }, - "clientId": { - "type": "string", - "description": "OAuth client ID" - }, - "clientSecret": { - "type": "string", - "description": "OAuth client secret (typically from ENV)" - }, - "scopes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Requested OAuth scopes" - }, - "redirectUri": { - "type": "string", - "format": "uri", - "description": "OAuth callback URL" - }, - "enabled": { - "type": "boolean", - "default": true, - "description": "Whether this provider is enabled" - }, - "displayName": { - "type": "string", - "description": "Display name for the provider button" - }, - "icon": { - "type": "string", - "description": "Icon URL or identifier" - } - }, - "required": [ - "provider", - "clientId", - "clientSecret" - ], - "additionalProperties": false - }, - "minItems": 1 - } - }, - "required": [ - "providers" - ], - "additionalProperties": false - }, - "session": { - "type": "object", - "properties": { - "expiresIn": { - "type": "number", - "default": 604800, - "description": "Session expiry in seconds (default 7 days)" - }, - "updateAge": { - "type": "number", - "default": 86400, - "description": "Session update interval in seconds (default 1 day)" - }, - "cookieName": { - "type": "string", - "default": "better-auth.session_token", - "description": "Session cookie name" - }, - "cookieSecure": { - "type": "boolean", - "default": true, - "description": "Use secure cookies (HTTPS only)" - }, - "cookieSameSite": { - "type": "string", - "enum": [ - "strict", - "lax", - "none" - ], - "default": "lax", - "description": "SameSite cookie attribute" - }, - "cookieDomain": { - "type": "string", - "description": "Cookie domain" - }, - "cookiePath": { - "type": "string", - "default": "/", - "description": "Cookie path" - }, - "cookieHttpOnly": { - "type": "boolean", - "default": true, - "description": "HttpOnly cookie attribute" - } - }, - "additionalProperties": false, - "default": {} - }, - "rateLimit": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "maxAttempts": { - "type": "number", - "default": 5, - "description": "Maximum login attempts" - }, - "windowMs": { - "type": "number", - "default": 900000, - "description": "Time window in milliseconds (default 15 min)" - }, - "blockDuration": { - "type": "number", - "default": 900000, - "description": "Block duration after max attempts in ms" - }, - "skipSuccessfulRequests": { - "type": "boolean", - "default": false, - "description": "Only count failed requests" - } - }, - "additionalProperties": false, - "default": {} - }, - "csrf": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "tokenLength": { - "type": "number", - "default": 32, - "description": "CSRF token length" - }, - "cookieName": { - "type": "string", - "default": "better-auth.csrf_token", - "description": "CSRF cookie name" - }, - "headerName": { - "type": "string", - "default": "X-CSRF-Token", - "description": "CSRF header name" - } - }, - "additionalProperties": false, - "default": {} - }, - "accountLinking": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Allow account linking" - }, - "autoLink": { - "type": "boolean", - "default": false, - "description": "Automatically link accounts with same email" - }, - "requireVerification": { - "type": "boolean", - "default": true, - "description": "Require email verification before linking" - } - }, - "additionalProperties": false, - "default": {} - }, - "twoFactor": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false - }, - "issuer": { - "type": "string", - "description": "TOTP issuer name" - }, - "qrCodeSize": { - "type": "number", - "default": 200, - "description": "QR code size in pixels" - }, - "backupCodes": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "count": { - "type": "number", - "default": 10, - "description": "Number of backup codes to generate" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "userFieldMapping": { - "type": "object", - "properties": { - "id": { - "type": "string", - "default": "id", - "description": "User ID field" - }, - "email": { - "type": "string", - "default": "email", - "description": "Email field" - }, - "name": { - "type": "string", - "default": "name", - "description": "Name field" - }, - "image": { - "type": "string", - "default": "image", - "description": "Profile image field" - }, - "emailVerified": { - "type": "string", - "default": "email_verified", - "description": "Email verification status field" - }, - "createdAt": { - "type": "string", - "default": "created_at", - "description": "Created timestamp field" - }, - "updatedAt": { - "type": "string", - "default": "updated_at", - "description": "Updated timestamp field" - } - }, - "additionalProperties": false, - "default": {} - }, - "database": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "prisma", - "drizzle", - "kysely", - "custom" - ], - "description": "Database adapter type" - }, - "connectionString": { - "type": "string", - "description": "Database connection string" - }, - "tablePrefix": { - "type": "string", - "default": "auth_", - "description": "Prefix for auth tables" - }, - "schema": { - "type": "string", - "description": "Database schema name" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "plugins": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Plugin name" - }, - "enabled": { - "type": "boolean", - "default": true - }, - "options": { - "type": "object", - "additionalProperties": {}, - "description": "Plugin-specific options" - } - }, - "required": [ - "name" - ], - "additionalProperties": false - }, - "default": [] - }, - "hooks": { - "type": "object", - "properties": {}, - "additionalProperties": false, - "description": "Authentication lifecycle hooks" - }, - "security": { - "type": "object", - "properties": { - "allowedOrigins": { - "type": "array", - "items": { - "type": "string" - }, - "description": "CORS allowed origins" - }, - "trustProxy": { - "type": "boolean", - "default": false, - "description": "Trust proxy headers" - }, - "ipRateLimiting": { - "type": "boolean", - "default": true, - "description": "Enable IP-based rate limiting" - }, - "sessionFingerprinting": { - "type": "boolean", - "default": true, - "description": "Enable session fingerprinting" - }, - "maxSessions": { - "type": "number", - "default": 5, - "description": "Maximum concurrent sessions per user" - } - }, - "additionalProperties": false, - "description": "Advanced security settings" - }, - "email": { - "type": "object", - "properties": { - "from": { - "type": "string", - "format": "email", - "description": "From email address" - }, - "fromName": { - "type": "string", - "description": "From name" - }, - "provider": { - "type": "string", - "enum": [ - "smtp", - "sendgrid", - "mailgun", - "ses", - "resend", - "custom" - ], - "description": "Email provider" - }, - "config": { - "type": "object", - "additionalProperties": {}, - "description": "Provider-specific configuration" - } - }, - "required": [ - "from", - "provider" - ], - "additionalProperties": false, - "description": "Email configuration" - }, - "ui": { - "type": "object", - "properties": { - "brandName": { - "type": "string", - "description": "Brand name displayed in auth UI" - }, - "logo": { - "type": "string", - "description": "Logo URL" - }, - "primaryColor": { - "type": "string", - "description": "Primary brand color (hex)" - }, - "customCss": { - "type": "string", - "description": "Custom CSS for auth pages" - } - }, - "additionalProperties": false, - "description": "UI customization" - }, - "active": { - "type": "boolean", - "default": true, - "description": "Whether this provider is active" - }, - "allowRegistration": { - "type": "boolean", - "default": true, - "description": "Allow new user registration" - } - }, - "required": [ - "name", - "label", - "strategies", - "baseUrl", - "secret" - ], - "additionalProperties": false, - "description": "Better-auth configuration" - } - }, - "required": [ - "type", - "config" - ], - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/BetterAuthStrategy.json b/packages/spec/json-schema/BetterAuthStrategy.json deleted file mode 100644 index 593ac2235..000000000 --- a/packages/spec/json-schema/BetterAuthStrategy.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$ref": "#/definitions/BetterAuthStrategy", - "definitions": { - "BetterAuthStrategy": { - "type": "string", - "enum": [ - "email_password", - "magic_link", - "oauth", - "passkey", - "otp", - "anonymous" - ] - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/CSRFConfig.json b/packages/spec/json-schema/CSRFConfig.json deleted file mode 100644 index eaed24aff..000000000 --- a/packages/spec/json-schema/CSRFConfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$ref": "#/definitions/CSRFConfig", - "definitions": { - "CSRFConfig": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "tokenLength": { - "type": "number", - "default": 32, - "description": "CSRF token length" - }, - "cookieName": { - "type": "string", - "default": "better-auth.csrf_token", - "description": "CSRF cookie name" - }, - "headerName": { - "type": "string", - "default": "X-CSRF-Token", - "description": "CSRF header name" - } - }, - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/DatabaseAdapter.json b/packages/spec/json-schema/DatabaseAdapter.json deleted file mode 100644 index 4695c1a23..000000000 --- a/packages/spec/json-schema/DatabaseAdapter.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "$ref": "#/definitions/DatabaseAdapter", - "definitions": { - "DatabaseAdapter": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "prisma", - "drizzle", - "kysely", - "custom" - ], - "description": "Database adapter type" - }, - "connectionString": { - "type": "string", - "description": "Database connection string" - }, - "tablePrefix": { - "type": "string", - "default": "auth_", - "description": "Prefix for auth tables" - }, - "schema": { - "type": "string", - "description": "Database schema name" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/EmailAlertAction.json b/packages/spec/json-schema/EmailAlertAction.json deleted file mode 100644 index 3462a3b7b..000000000 --- a/packages/spec/json-schema/EmailAlertAction.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "$ref": "#/definitions/EmailAlertAction", - "definitions": { - "EmailAlertAction": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Action name" - }, - "type": { - "type": "string", - "const": "email_alert" - }, - "template": { - "type": "string", - "description": "Email template ID/DevName" - }, - "recipients": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of recipient emails or user IDs" - } - }, - "required": [ - "name", - "type", - "template", - "recipients" - ], - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/EmailPasswordConfig.json b/packages/spec/json-schema/EmailPasswordConfig.json deleted file mode 100644 index 9bb0c155b..000000000 --- a/packages/spec/json-schema/EmailPasswordConfig.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "$ref": "#/definitions/EmailPasswordConfig", - "definitions": { - "EmailPasswordConfig": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "requireEmailVerification": { - "type": "boolean", - "default": true, - "description": "Require email verification before login" - }, - "minPasswordLength": { - "type": "number", - "minimum": 6, - "maximum": 128, - "default": 8, - "description": "Minimum password length" - }, - "requirePasswordComplexity": { - "type": "boolean", - "default": true, - "description": "Require uppercase, lowercase, numbers, symbols" - }, - "allowPasswordReset": { - "type": "boolean", - "default": true, - "description": "Enable password reset functionality" - }, - "passwordResetExpiry": { - "type": "number", - "default": 3600, - "description": "Password reset token expiry in seconds" - } - }, - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/MagicLinkConfig.json b/packages/spec/json-schema/MagicLinkConfig.json deleted file mode 100644 index 937680a51..000000000 --- a/packages/spec/json-schema/MagicLinkConfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$ref": "#/definitions/MagicLinkConfig", - "definitions": { - "MagicLinkConfig": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "expiryTime": { - "type": "number", - "default": 900, - "description": "Magic link expiry time in seconds (default 15 min)" - } - }, - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/OAuthProvider.json b/packages/spec/json-schema/OAuthProvider.json deleted file mode 100644 index 738d846ad..000000000 --- a/packages/spec/json-schema/OAuthProvider.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "$ref": "#/definitions/OAuthProvider", - "definitions": { - "OAuthProvider": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "enum": [ - "google", - "github", - "facebook", - "twitter", - "linkedin", - "microsoft", - "apple", - "discord", - "gitlab", - "custom" - ], - "description": "OAuth provider type" - }, - "clientId": { - "type": "string", - "description": "OAuth client ID" - }, - "clientSecret": { - "type": "string", - "description": "OAuth client secret (typically from ENV)" - }, - "scopes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Requested OAuth scopes" - }, - "redirectUri": { - "type": "string", - "format": "uri", - "description": "OAuth callback URL" - }, - "enabled": { - "type": "boolean", - "default": true, - "description": "Whether this provider is enabled" - }, - "displayName": { - "type": "string", - "description": "Display name for the provider button" - }, - "icon": { - "type": "string", - "description": "Icon URL or identifier" - } - }, - "required": [ - "provider", - "clientId", - "clientSecret" - ], - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/PasskeyConfig.json b/packages/spec/json-schema/PasskeyConfig.json deleted file mode 100644 index 335a54344..000000000 --- a/packages/spec/json-schema/PasskeyConfig.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "$ref": "#/definitions/PasskeyConfig", - "definitions": { - "PasskeyConfig": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false - }, - "rpName": { - "type": "string", - "description": "Relying Party name" - }, - "rpId": { - "type": "string", - "description": "Relying Party ID (defaults to domain)" - }, - "allowedOrigins": { - "type": "array", - "items": { - "type": "string", - "format": "uri" - }, - "description": "Allowed origins for WebAuthn" - }, - "userVerification": { - "type": "string", - "enum": [ - "required", - "preferred", - "discouraged" - ], - "default": "preferred" - }, - "attestation": { - "type": "string", - "enum": [ - "none", - "indirect", - "direct", - "enterprise" - ], - "default": "none" - } - }, - "required": [ - "rpName" - ], - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/RateLimit.json b/packages/spec/json-schema/RateLimit.json deleted file mode 100644 index 8abd9506b..000000000 --- a/packages/spec/json-schema/RateLimit.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$ref": "#/definitions/RateLimit", - "definitions": { - "RateLimit": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false - }, - "windowMs": { - "type": "number", - "default": 60000, - "description": "Time window in milliseconds" - }, - "maxRequests": { - "type": "number", - "default": 100, - "description": "Max requests per window" - } - }, - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/RateLimitConfig.json b/packages/spec/json-schema/RateLimitConfig.json deleted file mode 100644 index d8620aadb..000000000 --- a/packages/spec/json-schema/RateLimitConfig.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$ref": "#/definitions/RateLimitConfig", - "definitions": { - "RateLimitConfig": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "maxAttempts": { - "type": "number", - "default": 5, - "description": "Maximum login attempts" - }, - "windowMs": { - "type": "number", - "default": 900000, - "description": "Time window in milliseconds (default 15 min)" - }, - "blockDuration": { - "type": "number", - "default": 900000, - "description": "Block duration after max attempts in ms" - }, - "skipSuccessfulRequests": { - "type": "boolean", - "default": false, - "description": "Only count failed requests" - } - }, - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/SessionConfig.json b/packages/spec/json-schema/SessionConfig.json deleted file mode 100644 index 8aea856bf..000000000 --- a/packages/spec/json-schema/SessionConfig.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "$ref": "#/definitions/SessionConfig", - "definitions": { - "SessionConfig": { - "type": "object", - "properties": { - "expiresIn": { - "type": "number", - "default": 604800, - "description": "Session expiry in seconds (default 7 days)" - }, - "updateAge": { - "type": "number", - "default": 86400, - "description": "Session update interval in seconds (default 1 day)" - }, - "cookieName": { - "type": "string", - "default": "better-auth.session_token", - "description": "Session cookie name" - }, - "cookieSecure": { - "type": "boolean", - "default": true, - "description": "Use secure cookies (HTTPS only)" - }, - "cookieSameSite": { - "type": "string", - "enum": [ - "strict", - "lax", - "none" - ], - "default": "lax", - "description": "SameSite cookie attribute" - }, - "cookieDomain": { - "type": "string", - "description": "Cookie domain" - }, - "cookiePath": { - "type": "string", - "default": "/", - "description": "Cookie path" - }, - "cookieHttpOnly": { - "type": "boolean", - "default": true, - "description": "HttpOnly cookie attribute" - } - }, - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/SessionPolicy.json b/packages/spec/json-schema/SessionPolicy.json deleted file mode 100644 index 80ddd1017..000000000 --- a/packages/spec/json-schema/SessionPolicy.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$ref": "#/definitions/SessionPolicy", - "definitions": { - "SessionPolicy": { - "type": "object", - "properties": { - "idleTimeout": { - "type": "number", - "default": 30, - "description": "Minutes before idle session logout" - }, - "absoluteTimeout": { - "type": "number", - "default": 480, - "description": "Max session duration (minutes)" - }, - "forceMfa": { - "type": "boolean", - "default": false, - "description": "Require 2FA for all users" - } - }, - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/TwoFactorConfig.json b/packages/spec/json-schema/TwoFactorConfig.json deleted file mode 100644 index 488c80a3b..000000000 --- a/packages/spec/json-schema/TwoFactorConfig.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "$ref": "#/definitions/TwoFactorConfig", - "definitions": { - "TwoFactorConfig": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false - }, - "issuer": { - "type": "string", - "description": "TOTP issuer name" - }, - "qrCodeSize": { - "type": "number", - "default": 200, - "description": "QR code size in pixels" - }, - "backupCodes": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "count": { - "type": "number", - "default": 10, - "description": "Number of backup codes to generate" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/UserFieldMapping.json b/packages/spec/json-schema/UserFieldMapping.json deleted file mode 100644 index a07a8458c..000000000 --- a/packages/spec/json-schema/UserFieldMapping.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "$ref": "#/definitions/UserFieldMapping", - "definitions": { - "UserFieldMapping": { - "type": "object", - "properties": { - "id": { - "type": "string", - "default": "id", - "description": "User ID field" - }, - "email": { - "type": "string", - "default": "email", - "description": "Email field" - }, - "name": { - "type": "string", - "default": "name", - "description": "Name field" - }, - "image": { - "type": "string", - "default": "image", - "description": "Profile image field" - }, - "emailVerified": { - "type": "string", - "default": "email_verified", - "description": "Email verification status field" - }, - "createdAt": { - "type": "string", - "default": "created_at", - "description": "Created timestamp field" - }, - "updatedAt": { - "type": "string", - "default": "updated_at", - "description": "Updated timestamp field" - } - }, - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/src/index.ts b/packages/spec/src/index.ts index e5c29adba..80f8750d1 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -41,7 +41,6 @@ export * from './system/manifest.zod'; export * from './system/datasource.zod'; export * from './system/api.zod'; export * from './system/identity.zod'; -export * from './system/auth-better.zod'; export * from './system/policy.zod'; export * from './system/role.zod'; export * from './system/territory.zod'; diff --git a/packages/spec/src/system/auth-better.test.ts b/packages/spec/src/system/auth-better.test.ts deleted file mode 100644 index 52321a5b1..000000000 --- a/packages/spec/src/system/auth-better.test.ts +++ /dev/null @@ -1,752 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - BetterAuthStrategy, - OAuthProviderSchema, - EmailPasswordConfigSchema, - MagicLinkConfigSchema, - PasskeyConfigSchema, - SessionConfigSchema, - RateLimitConfigSchema, - CSRFConfigSchema, - AccountLinkingConfigSchema, - TwoFactorConfigSchema, - UserFieldMappingSchema, - DatabaseAdapterSchema, - BetterAuthPluginConfigSchema, - BetterAuthConfigSchema, - BetterAuthProviderSchema, - type BetterAuthConfig, - type BetterAuthProvider, - type OAuthProvider, -} from './auth-better.zod'; - -describe('BetterAuthStrategy', () => { - it('should accept valid authentication strategies', () => { - const strategies = [ - 'email_password', - 'magic_link', - 'oauth', - 'passkey', - 'otp', - 'anonymous', - ]; - - strategies.forEach((strategy) => { - expect(() => BetterAuthStrategy.parse(strategy)).not.toThrow(); - }); - }); - - it('should reject invalid strategies', () => { - expect(() => BetterAuthStrategy.parse('invalid')).toThrow(); - }); -}); - -describe('OAuthProviderSchema', () => { - it('should accept valid OAuth provider configuration', () => { - const provider: OAuthProvider = { - provider: 'google', - clientId: 'test-client-id', - clientSecret: 'test-client-secret', - scopes: ['openid', 'profile', 'email'], - enabled: true, - }; - - expect(() => OAuthProviderSchema.parse(provider)).not.toThrow(); - }); - - it('should accept minimal OAuth provider configuration', () => { - const provider = { - provider: 'github', - clientId: 'test-client-id', - clientSecret: 'test-client-secret', - }; - - const result = OAuthProviderSchema.parse(provider); - expect(result.enabled).toBe(true); // default value - }); - - it('should accept all supported OAuth providers', () => { - const providers = [ - 'google', - 'github', - 'facebook', - 'twitter', - 'linkedin', - 'microsoft', - 'apple', - 'discord', - 'gitlab', - 'custom', - ]; - - providers.forEach((provider) => { - const config = { - provider, - clientId: 'test-id', - clientSecret: 'test-secret', - }; - expect(() => OAuthProviderSchema.parse(config)).not.toThrow(); - }); - }); - - it('should validate redirect URI is a valid URL', () => { - const provider = { - provider: 'google', - clientId: 'test-id', - clientSecret: 'test-secret', - redirectUri: 'not-a-url', - }; - - expect(() => OAuthProviderSchema.parse(provider)).toThrow(); - }); -}); - -describe('EmailPasswordConfigSchema', () => { - it('should accept valid email password configuration', () => { - const config = { - enabled: true, - requireEmailVerification: true, - minPasswordLength: 12, - requirePasswordComplexity: true, - allowPasswordReset: true, - passwordResetExpiry: 3600, - }; - - expect(() => EmailPasswordConfigSchema.parse(config)).not.toThrow(); - }); - - it('should use default values', () => { - const config = {}; - const result = EmailPasswordConfigSchema.parse(config); - - expect(result.enabled).toBe(true); - expect(result.minPasswordLength).toBe(8); - expect(result.requireEmailVerification).toBe(true); - }); - - it('should enforce password length constraints', () => { - const tooShort = { minPasswordLength: 5 }; - expect(() => EmailPasswordConfigSchema.parse(tooShort)).toThrow(); - - const tooLong = { minPasswordLength: 200 }; - expect(() => EmailPasswordConfigSchema.parse(tooLong)).toThrow(); - - const justRight = { minPasswordLength: 10 }; - expect(() => EmailPasswordConfigSchema.parse(justRight)).not.toThrow(); - }); -}); - -describe('MagicLinkConfigSchema', () => { - it('should accept valid magic link configuration', () => { - const config = { - enabled: true, - expiryTime: 1800, // 30 minutes - }; - - expect(() => MagicLinkConfigSchema.parse(config)).not.toThrow(); - }); - - it('should use default expiry time', () => { - const config = { enabled: true }; - const result = MagicLinkConfigSchema.parse(config); - - expect(result.expiryTime).toBe(900); // 15 minutes default - }); -}); - -describe('PasskeyConfigSchema', () => { - it('should accept valid passkey configuration', () => { - const config = { - enabled: true, - rpName: 'ObjectStack', - rpId: 'objectstack.com', - userVerification: 'required' as const, - attestation: 'direct' as const, - }; - - expect(() => PasskeyConfigSchema.parse(config)).not.toThrow(); - }); - - it('should use default values', () => { - const config = { - rpName: 'Test App', - }; - const result = PasskeyConfigSchema.parse(config); - - expect(result.enabled).toBe(false); // disabled by default - expect(result.userVerification).toBe('preferred'); - expect(result.attestation).toBe('none'); - }); - - it('should validate allowed origins are URLs', () => { - const config = { - rpName: 'Test', - allowedOrigins: ['https://example.com', 'https://app.example.com'], - }; - - expect(() => PasskeyConfigSchema.parse(config)).not.toThrow(); - }); -}); - -describe('SessionConfigSchema', () => { - it('should accept valid session configuration', () => { - const config = { - expiresIn: 604800, // 7 days - updateAge: 86400, // 1 day - cookieName: 'my-session', - cookieSecure: true, - cookieSameSite: 'strict' as const, - cookieHttpOnly: true, - }; - - expect(() => SessionConfigSchema.parse(config)).not.toThrow(); - }); - - it('should use default values', () => { - const config = {}; - const result = SessionConfigSchema.parse(config); - - expect(result.expiresIn).toBe(86400 * 7); // 7 days - expect(result.cookieName).toBe('better-auth.session_token'); - expect(result.cookieSecure).toBe(true); - expect(result.cookieSameSite).toBe('lax'); - }); - - it('should accept all SameSite options', () => { - const options = ['strict', 'lax', 'none'] as const; - - options.forEach((sameSite) => { - const config = { cookieSameSite: sameSite }; - expect(() => SessionConfigSchema.parse(config)).not.toThrow(); - }); - }); -}); - -describe('RateLimitConfigSchema', () => { - it('should accept valid rate limit configuration', () => { - const config = { - enabled: true, - maxAttempts: 10, - windowMs: 1800000, // 30 minutes - blockDuration: 3600000, // 1 hour - skipSuccessfulRequests: true, - }; - - expect(() => RateLimitConfigSchema.parse(config)).not.toThrow(); - }); - - it('should use default values', () => { - const config = {}; - const result = RateLimitConfigSchema.parse(config); - - expect(result.enabled).toBe(true); - expect(result.maxAttempts).toBe(5); - expect(result.windowMs).toBe(900000); // 15 minutes - }); -}); - -describe('CSRFConfigSchema', () => { - it('should accept valid CSRF configuration', () => { - const config = { - enabled: true, - tokenLength: 64, - cookieName: 'csrf-token', - headerName: 'X-CSRF-Token', - }; - - expect(() => CSRFConfigSchema.parse(config)).not.toThrow(); - }); - - it('should use default values', () => { - const config = {}; - const result = CSRFConfigSchema.parse(config); - - expect(result.enabled).toBe(true); - expect(result.tokenLength).toBe(32); - expect(result.cookieName).toBe('better-auth.csrf_token'); - }); -}); - -describe('AccountLinkingConfigSchema', () => { - it('should accept valid account linking configuration', () => { - const config = { - enabled: true, - autoLink: true, - requireVerification: false, - }; - - expect(() => AccountLinkingConfigSchema.parse(config)).not.toThrow(); - }); - - it('should use default values', () => { - const config = {}; - const result = AccountLinkingConfigSchema.parse(config); - - expect(result.enabled).toBe(true); - expect(result.autoLink).toBe(false); - expect(result.requireVerification).toBe(true); - }); -}); - -describe('TwoFactorConfigSchema', () => { - it('should accept valid 2FA configuration', () => { - const config = { - enabled: true, - issuer: 'ObjectStack', - qrCodeSize: 256, - backupCodes: { - enabled: true, - count: 8, - }, - }; - - expect(() => TwoFactorConfigSchema.parse(config)).not.toThrow(); - }); - - it('should use default values', () => { - const config = { enabled: false }; - const result = TwoFactorConfigSchema.parse(config); - - expect(result.qrCodeSize).toBe(200); - }); -}); - -describe('UserFieldMappingSchema', () => { - it('should accept custom field mappings', () => { - const config = { - id: 'user_id', - email: 'user_email', - name: 'full_name', - emailVerified: 'is_email_verified', - }; - - expect(() => UserFieldMappingSchema.parse(config)).not.toThrow(); - }); - - it('should use default field names', () => { - const config = {}; - const result = UserFieldMappingSchema.parse(config); - - expect(result.id).toBe('id'); - expect(result.email).toBe('email'); - expect(result.name).toBe('name'); - expect(result.emailVerified).toBe('email_verified'); - }); -}); - -describe('DatabaseAdapterSchema', () => { - it('should accept valid database adapter configuration', () => { - const config = { - type: 'prisma' as const, - connectionString: 'postgresql://user:pass@localhost:5432/db', - tablePrefix: 'auth_', - schema: 'public', - }; - - expect(() => DatabaseAdapterSchema.parse(config)).not.toThrow(); - }); - - it('should accept all supported adapter types', () => { - const types = ['prisma', 'drizzle', 'kysely', 'custom'] as const; - - types.forEach((type) => { - const config = { type }; - expect(() => DatabaseAdapterSchema.parse(config)).not.toThrow(); - }); - }); - - it('should use default table prefix', () => { - const config = { type: 'drizzle' as const }; - const result = DatabaseAdapterSchema.parse(config); - - expect(result.tablePrefix).toBe('auth_'); - }); -}); - -describe('BetterAuthPluginConfigSchema', () => { - it('should accept valid plugin configuration', () => { - const config = { - name: 'organization', - enabled: true, - options: { - maxOrganizations: 5, - allowInvites: true, - }, - }; - - expect(() => BetterAuthPluginConfigSchema.parse(config)).not.toThrow(); - }); -}); - -describe('BetterAuthConfigSchema', () => { - it('should accept minimal valid configuration', () => { - const config: BetterAuthConfig = { - name: 'better_auth', - label: 'Better Auth', - strategies: ['email_password'], - baseUrl: 'https://example.com', - secret: 'a'.repeat(32), // 32 character secret - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, - }; - - expect(() => BetterAuthConfigSchema.parse(config)).not.toThrow(); - }); - - it('should accept comprehensive configuration', () => { - const config: BetterAuthConfig = { - name: 'main_auth', - label: 'Main Authentication', - strategies: ['email_password', 'oauth', 'magic_link'], - baseUrl: 'https://app.example.com', - secret: 'super-secret-key-with-at-least-32-characters', - - emailPassword: { - enabled: true, - requireEmailVerification: true, - minPasswordLength: 10, - requirePasswordComplexity: true, - allowPasswordReset: true, - passwordResetExpiry: 7200, - }, - - magicLink: { - enabled: true, - expiryTime: 900, - }, - - oauth: { - providers: [ - { - provider: 'google', - clientId: 'google-client-id', - clientSecret: 'google-client-secret', - scopes: ['openid', 'profile', 'email'], - enabled: true, - }, - { - provider: 'github', - clientId: 'github-client-id', - clientSecret: 'github-client-secret', - enabled: true, - }, - ], - }, - - session: { - expiresIn: 604800, - cookieName: 'app-session', - cookieSecure: true, - cookieSameSite: 'lax', - }, - - rateLimit: { - enabled: true, - maxAttempts: 5, - windowMs: 900000, - }, - - csrf: { - enabled: true, - tokenLength: 32, - }, - - accountLinking: { - enabled: true, - autoLink: false, - requireVerification: true, - }, - - twoFactor: { - enabled: true, - issuer: 'My App', - }, - - userFieldMapping: {}, - - database: { - type: 'prisma', - tablePrefix: 'auth_', - }, - - plugins: [], - - active: true, - allowRegistration: true, - }; - - expect(() => BetterAuthConfigSchema.parse(config)).not.toThrow(); - }); - - it('should enforce snake_case for name field', () => { - const invalidConfig = { - name: 'betterAuth', // camelCase - invalid - label: 'Better Auth', - strategies: ['email_password'], - baseUrl: 'https://example.com', - secret: 'a'.repeat(32), - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, - }; - - expect(() => BetterAuthConfigSchema.parse(invalidConfig)).toThrow(); - - const validConfig = { - ...invalidConfig, - name: 'better_auth', // snake_case - valid - }; - - expect(() => BetterAuthConfigSchema.parse(validConfig)).not.toThrow(); - }); - - it('should require at least one strategy', () => { - const config = { - name: 'test_auth', - label: 'Test', - strategies: [], // empty - invalid - baseUrl: 'https://example.com', - secret: 'a'.repeat(32), - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, - }; - - expect(() => BetterAuthConfigSchema.parse(config)).toThrow(); - }); - - it('should require secret to be at least 32 characters', () => { - const config = { - name: 'test_auth', - label: 'Test', - strategies: ['email_password'], - baseUrl: 'https://example.com', - secret: 'short', // too short - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, - }; - - expect(() => BetterAuthConfigSchema.parse(config)).toThrow(); - }); - - it('should validate baseUrl is a valid URL', () => { - const config = { - name: 'test_auth', - label: 'Test', - strategies: ['email_password'], - baseUrl: 'not-a-url', // invalid URL - secret: 'a'.repeat(32), - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, - }; - - expect(() => BetterAuthConfigSchema.parse(config)).toThrow(); - }); - - it('should accept configuration with hooks', () => { - const config = { - name: 'test_auth', - label: 'Test', - strategies: ['email_password'], - baseUrl: 'https://example.com', - secret: 'a'.repeat(32), - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, - hooks: { - beforeSignIn: async ({ email }: { email: string }) => { - console.log('Before sign in:', email); - }, - afterSignIn: async ({ user, session }: { user: any; session: any }) => { - console.log('After sign in:', user.id); - }, - }, - }; - - expect(() => BetterAuthConfigSchema.parse(config)).not.toThrow(); - }); - - it('should accept configuration with security settings', () => { - const config = { - name: 'test_auth', - label: 'Test', - strategies: ['email_password'], - baseUrl: 'https://example.com', - secret: 'a'.repeat(32), - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, - security: { - allowedOrigins: ['https://app.example.com'], - trustProxy: true, - ipRateLimiting: true, - sessionFingerprinting: true, - maxSessions: 3, - }, - }; - - expect(() => BetterAuthConfigSchema.parse(config)).not.toThrow(); - }); - - it('should accept configuration with email settings', () => { - const config = { - name: 'test_auth', - label: 'Test', - strategies: ['email_password'], - baseUrl: 'https://example.com', - secret: 'a'.repeat(32), - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, - email: { - from: 'noreply@example.com', - fromName: 'My App', - provider: 'sendgrid' as const, - config: { - apiKey: 'sendgrid-api-key', - }, - }, - }; - - expect(() => BetterAuthConfigSchema.parse(config)).not.toThrow(); - }); - - it('should accept configuration with UI customization', () => { - const config = { - name: 'test_auth', - label: 'Test', - strategies: ['email_password'], - baseUrl: 'https://example.com', - secret: 'a'.repeat(32), - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, - ui: { - brandName: 'My Brand', - logo: 'https://example.com/logo.png', - primaryColor: '#007bff', - customCss: '.button { color: blue; }', - }, - }; - - expect(() => BetterAuthConfigSchema.parse(config)).not.toThrow(); - }); - - it('should use default values for optional fields', () => { - const config = { - name: 'test_auth', - label: 'Test', - strategies: ['email_password'], - baseUrl: 'https://example.com', - secret: 'a'.repeat(32), - }; - - const result = BetterAuthConfigSchema.parse(config); - - expect(result.active).toBe(true); - expect(result.allowRegistration).toBe(true); - expect(result.plugins).toEqual([]); - }); -}); - -describe('BetterAuthProviderSchema', () => { - it('should accept valid better-auth provider', () => { - const provider: BetterAuthProvider = { - type: 'better_auth', - config: { - name: 'main_auth', - label: 'Main Auth', - strategies: ['email_password', 'oauth'], - baseUrl: 'https://example.com', - secret: 'a'.repeat(32), - oauth: { - providers: [ - { - provider: 'google', - clientId: 'test-id', - clientSecret: 'test-secret', - }, - ], - }, - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, - }, - }; - - expect(() => BetterAuthProviderSchema.parse(provider)).not.toThrow(); - }); - - it('should require type to be "better_auth"', () => { - const provider = { - type: 'other_auth', // invalid - config: { - name: 'test', - label: 'Test', - strategies: ['email_password'], - baseUrl: 'https://example.com', - secret: 'a'.repeat(32), - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, - }, - }; - - expect(() => BetterAuthProviderSchema.parse(provider)).toThrow(); - }); -}); - -describe('Type inference', () => { - it('should correctly infer BetterAuthConfig type', () => { - const config: BetterAuthConfig = { - name: 'test_auth', - label: 'Test Auth', - strategies: ['email_password'], - baseUrl: 'https://example.com', - secret: 'a'.repeat(32), - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, - }; - - // This test passes if TypeScript compiles without errors - expect(config.name).toBe('test_auth'); - expect(config.strategies).toContain('email_password'); - }); - - it('should correctly infer BetterAuthProvider type', () => { - const provider: BetterAuthProvider = { - type: 'better_auth', - config: { - name: 'test_auth', - label: 'Test', - strategies: ['email_password'], - baseUrl: 'https://example.com', - secret: 'a'.repeat(32), - session: {}, - rateLimit: {}, - csrf: {}, - accountLinking: {}, - }, - }; - - // This test passes if TypeScript compiles without errors - expect(provider.type).toBe('better_auth'); - expect(provider.config.name).toBe('test_auth'); - }); -}); diff --git a/packages/spec/src/system/auth-better.zod.ts b/packages/spec/src/system/auth-better.zod.ts deleted file mode 100644 index aa6d6c271..000000000 --- a/packages/spec/src/system/auth-better.zod.ts +++ /dev/null @@ -1,484 +0,0 @@ -import { z } from 'zod'; - -/** - * Better-Auth Authentication Protocol - * - * Defines the schema for integrating better-auth (https://better-auth.com) - * as an authentication provider in the ObjectStack ecosystem. - * - * Better-auth is a modern, framework-agnostic authentication library that provides: - * - Multiple authentication strategies (email/password, OAuth, magic links, passkeys) - * - Session management - * - Security features (rate limiting, CSRF protection, etc.) - * - Plugin architecture - */ - -/** - * Supported authentication strategies in better-auth - */ -export const BetterAuthStrategy = z.enum([ - 'email_password', // Traditional email & password authentication - 'magic_link', // Passwordless email magic link - 'oauth', // OAuth2 providers (Google, GitHub, etc.) - 'passkey', // WebAuthn / FIDO2 passkeys - 'otp', // One-time password (SMS, Email) - 'anonymous', // Anonymous/guest sessions -]); - -export type BetterAuthStrategy = z.infer; - -/** - * OAuth Provider Configuration - * Supports popular OAuth2 providers - */ -export const OAuthProviderSchema = z.object({ - provider: z.enum([ - 'google', - 'github', - 'facebook', - 'twitter', - 'linkedin', - 'microsoft', - 'apple', - 'discord', - 'gitlab', - 'custom', - ]).describe('OAuth provider type'), - - clientId: z.string().describe('OAuth client ID'), - clientSecret: z.string().describe('OAuth client secret (typically from ENV)'), - - scopes: z.array(z.string()).optional().describe('Requested OAuth scopes'), - - redirectUri: z.string().url().optional().describe('OAuth callback URL'), - - enabled: z.boolean().default(true).describe('Whether this provider is enabled'), - - displayName: z.string().optional().describe('Display name for the provider button'), - - icon: z.string().optional().describe('Icon URL or identifier'), -}); - -export type OAuthProvider = z.infer; - -/** - * Email & Password Strategy Configuration - */ -export const EmailPasswordConfigSchema = z.object({ - enabled: z.boolean().default(true), - - requireEmailVerification: z.boolean().default(true).describe('Require email verification before login'), - - minPasswordLength: z.number().min(6).max(128).default(8).describe('Minimum password length'), - - requirePasswordComplexity: z.boolean().default(true).describe('Require uppercase, lowercase, numbers, symbols'), - - allowPasswordReset: z.boolean().default(true).describe('Enable password reset functionality'), - - passwordResetExpiry: z.number().default(3600).describe('Password reset token expiry in seconds'), -}); - -export type EmailPasswordConfig = z.infer; - -/** - * Magic Link Strategy Configuration - */ -export const MagicLinkConfigSchema = z.object({ - enabled: z.boolean().default(true), - - expiryTime: z.number().default(900).describe('Magic link expiry time in seconds (default 15 min)'), - - sendEmail: z.function() - .args(z.object({ - to: z.string().email(), - link: z.string().url(), - token: z.string(), - })) - .returns(z.promise(z.void())) - .optional() - .describe('Custom email sending function'), -}); - -export type MagicLinkConfig = z.infer; - -/** - * Passkey (WebAuthn) Strategy Configuration - */ -export const PasskeyConfigSchema = z.object({ - enabled: z.boolean().default(false), - - rpName: z.string().describe('Relying Party name'), - - rpId: z.string().optional().describe('Relying Party ID (defaults to domain)'), - - allowedOrigins: z.array(z.string().url()).optional().describe('Allowed origins for WebAuthn'), - - userVerification: z.enum(['required', 'preferred', 'discouraged']).default('preferred'), - - attestation: z.enum(['none', 'indirect', 'direct', 'enterprise']).default('none'), -}); - -export type PasskeyConfig = z.infer; - -/** - * Session Configuration - */ -export const SessionConfigSchema = z.object({ - expiresIn: z.number().default(86400 * 7).describe('Session expiry in seconds (default 7 days)'), - - updateAge: z.number().default(86400).describe('Session update interval in seconds (default 1 day)'), - - cookieName: z.string().default('better-auth.session_token').describe('Session cookie name'), - - cookieSecure: z.boolean().default(true).describe('Use secure cookies (HTTPS only)'), - - cookieSameSite: z.enum(['strict', 'lax', 'none']).default('lax').describe('SameSite cookie attribute'), - - cookieDomain: z.string().optional().describe('Cookie domain'), - - cookiePath: z.string().default('/').describe('Cookie path'), - - cookieHttpOnly: z.boolean().default(true).describe('HttpOnly cookie attribute'), -}); - -export type SessionConfig = z.infer; - -/** - * Rate Limiting Configuration - */ -export const RateLimitConfigSchema = z.object({ - enabled: z.boolean().default(true), - - maxAttempts: z.number().default(5).describe('Maximum login attempts'), - - windowMs: z.number().default(900000).describe('Time window in milliseconds (default 15 min)'), - - blockDuration: z.number().default(900000).describe('Block duration after max attempts in ms'), - - skipSuccessfulRequests: z.boolean().default(false).describe('Only count failed requests'), -}); - -export type RateLimitConfig = z.infer; - -/** - * CSRF Protection Configuration - */ -export const CSRFConfigSchema = z.object({ - enabled: z.boolean().default(true), - - tokenLength: z.number().default(32).describe('CSRF token length'), - - cookieName: z.string().default('better-auth.csrf_token').describe('CSRF cookie name'), - - headerName: z.string().default('X-CSRF-Token').describe('CSRF header name'), -}); - -export type CSRFConfig = z.infer; - -/** - * Account Linking Configuration - * Allows linking multiple auth methods to a single user account - */ -export const AccountLinkingConfigSchema = z.object({ - enabled: z.boolean().default(true).describe('Allow account linking'), - - autoLink: z.boolean().default(false).describe('Automatically link accounts with same email'), - - requireVerification: z.boolean().default(true).describe('Require email verification before linking'), -}); - -export type AccountLinkingConfig = z.infer; - -/** - * Two-Factor Authentication (2FA) Configuration - */ -export const TwoFactorConfigSchema = z.object({ - enabled: z.boolean().default(false), - - issuer: z.string().optional().describe('TOTP issuer name'), - - qrCodeSize: z.number().default(200).describe('QR code size in pixels'), - - backupCodes: z.object({ - enabled: z.boolean().default(true), - count: z.number().default(10).describe('Number of backup codes to generate'), - }).optional(), -}); - -export type TwoFactorConfig = z.infer; - -/** - * User Field Mapping Configuration - * Maps better-auth user fields to ObjectStack user object fields - */ -export const UserFieldMappingSchema = z.object({ - id: z.string().default('id').describe('User ID field'), - email: z.string().default('email').describe('Email field'), - name: z.string().default('name').describe('Name field'), - image: z.string().default('image').optional().describe('Profile image field'), - emailVerified: z.string().default('email_verified').describe('Email verification status field'), - createdAt: z.string().default('created_at').describe('Created timestamp field'), - updatedAt: z.string().default('updated_at').describe('Updated timestamp field'), -}); - -export type UserFieldMapping = z.infer; - -/** - * Database Adapter Configuration - */ -export const DatabaseAdapterSchema = z.object({ - type: z.enum(['prisma', 'drizzle', 'kysely', 'custom']).describe('Database adapter type'), - - connectionString: z.string().optional().describe('Database connection string'), - - tablePrefix: z.string().default('auth_').describe('Prefix for auth tables'), - - schema: z.string().optional().describe('Database schema name'), -}); - -export type DatabaseAdapter = z.infer; - -/** - * Better-Auth Plugin Configuration - * Extends better-auth with additional features - */ -export const BetterAuthPluginConfigSchema = z.object({ - name: z.string().describe('Plugin name'), - - enabled: z.boolean().default(true), - - options: z.record(z.any()).optional().describe('Plugin-specific options'), -}); - -export type BetterAuthPluginConfig = z.infer; - -/** - * Complete Better-Auth Configuration Schema - * - * This is the main configuration object for integrating better-auth - * into an ObjectStack application. - * - * @example - * ```typescript - * const authConfig: BetterAuthConfig = { - * name: 'better_auth', - * label: 'Better Auth', - * strategies: ['email_password', 'oauth'], - * emailPassword: { - * enabled: true, - * minPasswordLength: 8, - * }, - * oauth: { - * providers: [{ - * provider: 'google', - * clientId: process.env.GOOGLE_CLIENT_ID, - * clientSecret: process.env.GOOGLE_CLIENT_SECRET, - * }], - * }, - * session: { - * expiresIn: 604800, // 7 days - * }, - * }; - * ``` - */ -export const BetterAuthConfigSchema = z.object({ - /** - * Unique identifier for this auth configuration - * Must be in snake_case following ObjectStack conventions - */ - name: z.string() - .regex(/^[a-z_][a-z0-9_]*$/) - .describe('Configuration name (snake_case)'), - - /** - * Human-readable label - */ - label: z.string().describe('Display label'), - - /** - * Enabled authentication strategies - */ - strategies: z.array(BetterAuthStrategy).min(1).describe('Enabled authentication strategies'), - - /** - * Base URL for the application - */ - baseUrl: z.string().url().describe('Application base URL'), - - /** - * Secret key for signing tokens and cookies - * Should be loaded from environment variables - */ - secret: z.string().min(32).describe('Secret key for signing (min 32 chars)'), - - /** - * Email & Password configuration - */ - emailPassword: EmailPasswordConfigSchema.optional(), - - /** - * Magic Link configuration - */ - magicLink: MagicLinkConfigSchema.optional(), - - /** - * Passkey (WebAuthn) configuration - */ - passkey: PasskeyConfigSchema.optional(), - - /** - * OAuth configuration - */ - oauth: z.object({ - providers: z.array(OAuthProviderSchema).min(1), - }).optional(), - - /** - * Session configuration - */ - session: SessionConfigSchema.default({}), - - /** - * Rate limiting configuration - */ - rateLimit: RateLimitConfigSchema.default({}), - - /** - * CSRF protection configuration - */ - csrf: CSRFConfigSchema.default({}), - - /** - * Account linking configuration - */ - accountLinking: AccountLinkingConfigSchema.default({}), - - /** - * Two-factor authentication configuration - */ - twoFactor: TwoFactorConfigSchema.optional(), - - /** - * User field mapping - */ - userFieldMapping: UserFieldMappingSchema.default({}), - - /** - * Database adapter configuration - */ - database: DatabaseAdapterSchema.optional(), - - /** - * Additional better-auth plugins - */ - plugins: z.array(BetterAuthPluginConfigSchema).default([]), - - /** - * Custom hooks for authentication events - */ - hooks: z.object({ - beforeSignIn: z.function() - .args(z.object({ email: z.string() })) - .returns(z.promise(z.void())) - .optional() - .describe('Called before user sign in'), - - afterSignIn: z.function() - .args(z.object({ user: z.any(), session: z.any() })) - .returns(z.promise(z.void())) - .optional() - .describe('Called after user sign in'), - - beforeSignUp: z.function() - .args(z.object({ email: z.string(), name: z.string().optional() })) - .returns(z.promise(z.void())) - .optional() - .describe('Called before user registration'), - - afterSignUp: z.function() - .args(z.object({ user: z.any() })) - .returns(z.promise(z.void())) - .optional() - .describe('Called after user registration'), - - beforeSignOut: z.function() - .args(z.object({ sessionId: z.string() })) - .returns(z.promise(z.void())) - .optional() - .describe('Called before user sign out'), - - afterSignOut: z.function() - .args(z.object({ sessionId: z.string() })) - .returns(z.promise(z.void())) - .optional() - .describe('Called after user sign out'), - }).optional().describe('Authentication lifecycle hooks'), - - /** - * Advanced security settings - */ - security: z.object({ - allowedOrigins: z.array(z.string()).optional().describe('CORS allowed origins'), - - trustProxy: z.boolean().default(false).describe('Trust proxy headers'), - - ipRateLimiting: z.boolean().default(true).describe('Enable IP-based rate limiting'), - - sessionFingerprinting: z.boolean().default(true).describe('Enable session fingerprinting'), - - maxSessions: z.number().default(5).describe('Maximum concurrent sessions per user'), - }).optional().describe('Advanced security settings'), - - /** - * Email configuration for transactional emails - */ - email: z.object({ - from: z.string().email().describe('From email address'), - - fromName: z.string().optional().describe('From name'), - - provider: z.enum(['smtp', 'sendgrid', 'mailgun', 'ses', 'resend', 'custom']).describe('Email provider'), - - config: z.record(z.any()).optional().describe('Provider-specific configuration'), - }).optional().describe('Email configuration'), - - /** - * UI customization options - */ - ui: z.object({ - brandName: z.string().optional().describe('Brand name displayed in auth UI'), - - logo: z.string().optional().describe('Logo URL'), - - primaryColor: z.string().optional().describe('Primary brand color (hex)'), - - customCss: z.string().optional().describe('Custom CSS for auth pages'), - }).optional().describe('UI customization'), - - /** - * Whether this auth provider is active - */ - active: z.boolean().default(true).describe('Whether this provider is active'), - - /** - * Whether to allow new user registration - */ - allowRegistration: z.boolean().default(true).describe('Allow new user registration'), -}); - -/** - * TypeScript type inferred from BetterAuthConfigSchema - */ -export type BetterAuthConfig = z.infer; - -/** - * Better-Auth Provider Schema - * Wraps the configuration for use in the identity system - */ -export const BetterAuthProviderSchema = z.object({ - type: z.literal('better_auth').describe('Provider type identifier'), - - config: BetterAuthConfigSchema.describe('Better-auth configuration'), -}); - -export type BetterAuthProvider = z.infer; From b3a909ef911b6424d4096d5ebb400771a3fc5987 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 00:50:33 +0000 Subject: [PATCH 5/7] Restructure as core spec: Rename to Authentication protocol (generic, not BetterAuth) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../system/AccountLinkingConfig.mdx | 12 + .../references/system/AuthPluginConfig.mdx | 12 + .../docs/references/system/AuthStrategy.mdx | 13 + .../system/AuthenticationConfig.mdx | 32 + .../system/AuthenticationProvider.mdx | 11 + content/docs/references/system/CSRFConfig.mdx | 13 + .../references/system/DatabaseAdapter.mdx | 13 + .../references/system/EmailPasswordConfig.mdx | 15 + .../references/system/MagicLinkConfig.mdx | 11 + .../docs/references/system/OAuthProvider.mdx | 17 + .../docs/references/system/PasskeyConfig.mdx | 15 + .../references/system/RateLimitConfig.mdx | 14 + .../docs/references/system/SessionConfig.mdx | 17 + .../references/system/TwoFactorConfig.mdx | 13 + .../references/system/UserFieldMapping.mdx | 16 + ...ETTER_AUTH_PLUGIN.md => AUTHENTICATION.md} | 23 +- packages/plugin-better-auth/CHANGELOG.md | 13 - packages/plugin-better-auth/README.md | 55 -- .../examples/plugin-examples.ts | 127 --- .../plugin-better-auth/objectstack.config.ts | 127 --- packages/plugin-better-auth/package.json | 33 - packages/plugin-better-auth/src/index.ts | 365 --------- packages/plugin-better-auth/tsconfig.json | 18 - .../json-schema/AccountLinkingConfig.json | 27 + .../spec/json-schema/AuthPluginConfig.json | 28 + packages/spec/json-schema/AuthStrategy.json | 17 + .../json-schema/AuthenticationConfig.json | 601 ++++++++++++++ .../json-schema/AuthenticationProvider.json | 617 ++++++++++++++ packages/spec/json-schema/CSRFConfig.json | 31 + .../spec/json-schema/DatabaseAdapter.json | 38 + .../spec/json-schema/EmailAlertAction.json | 37 + .../spec/json-schema/EmailPasswordConfig.json | 43 + .../spec/json-schema/MagicLinkConfig.json | 21 + packages/spec/json-schema/OAuthProvider.json | 66 ++ packages/spec/json-schema/PasskeyConfig.json | 54 ++ packages/spec/json-schema/RateLimit.json | 26 + .../spec/json-schema/RateLimitConfig.json | 36 + packages/spec/json-schema/SessionConfig.json | 56 ++ packages/spec/json-schema/SessionPolicy.json | 27 + .../spec/json-schema/TwoFactorConfig.json | 40 + .../spec/json-schema/UserFieldMapping.json | 47 ++ packages/spec/src/index.ts | 1 + .../spec/src/system/authentication.test.ts | 752 ++++++++++++++++++ .../spec/src/system/authentication.zod.ts | 484 +++++++++++ 44 files changed, 3288 insertions(+), 746 deletions(-) create mode 100644 content/docs/references/system/AccountLinkingConfig.mdx create mode 100644 content/docs/references/system/AuthPluginConfig.mdx create mode 100644 content/docs/references/system/AuthStrategy.mdx create mode 100644 content/docs/references/system/AuthenticationConfig.mdx create mode 100644 content/docs/references/system/AuthenticationProvider.mdx create mode 100644 content/docs/references/system/CSRFConfig.mdx create mode 100644 content/docs/references/system/DatabaseAdapter.mdx create mode 100644 content/docs/references/system/EmailPasswordConfig.mdx create mode 100644 content/docs/references/system/MagicLinkConfig.mdx create mode 100644 content/docs/references/system/OAuthProvider.mdx create mode 100644 content/docs/references/system/PasskeyConfig.mdx create mode 100644 content/docs/references/system/RateLimitConfig.mdx create mode 100644 content/docs/references/system/SessionConfig.mdx create mode 100644 content/docs/references/system/TwoFactorConfig.mdx create mode 100644 content/docs/references/system/UserFieldMapping.mdx rename docs/{BETTER_AUTH_PLUGIN.md => AUTHENTICATION.md} (93%) delete mode 100644 packages/plugin-better-auth/CHANGELOG.md delete mode 100644 packages/plugin-better-auth/README.md delete mode 100644 packages/plugin-better-auth/examples/plugin-examples.ts delete mode 100644 packages/plugin-better-auth/objectstack.config.ts delete mode 100644 packages/plugin-better-auth/package.json delete mode 100644 packages/plugin-better-auth/src/index.ts delete mode 100644 packages/plugin-better-auth/tsconfig.json create mode 100644 packages/spec/json-schema/AccountLinkingConfig.json create mode 100644 packages/spec/json-schema/AuthPluginConfig.json create mode 100644 packages/spec/json-schema/AuthStrategy.json create mode 100644 packages/spec/json-schema/AuthenticationConfig.json create mode 100644 packages/spec/json-schema/AuthenticationProvider.json create mode 100644 packages/spec/json-schema/CSRFConfig.json create mode 100644 packages/spec/json-schema/DatabaseAdapter.json create mode 100644 packages/spec/json-schema/EmailAlertAction.json create mode 100644 packages/spec/json-schema/EmailPasswordConfig.json create mode 100644 packages/spec/json-schema/MagicLinkConfig.json create mode 100644 packages/spec/json-schema/OAuthProvider.json create mode 100644 packages/spec/json-schema/PasskeyConfig.json create mode 100644 packages/spec/json-schema/RateLimit.json create mode 100644 packages/spec/json-schema/RateLimitConfig.json create mode 100644 packages/spec/json-schema/SessionConfig.json create mode 100644 packages/spec/json-schema/SessionPolicy.json create mode 100644 packages/spec/json-schema/TwoFactorConfig.json create mode 100644 packages/spec/json-schema/UserFieldMapping.json create mode 100644 packages/spec/src/system/authentication.test.ts create mode 100644 packages/spec/src/system/authentication.zod.ts diff --git a/content/docs/references/system/AccountLinkingConfig.mdx b/content/docs/references/system/AccountLinkingConfig.mdx new file mode 100644 index 000000000..df6148efd --- /dev/null +++ b/content/docs/references/system/AccountLinkingConfig.mdx @@ -0,0 +1,12 @@ +--- +title: AccountLinkingConfig +description: AccountLinkingConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | Allow account linking | +| **autoLink** | `boolean` | optional | Automatically link accounts with same email | +| **requireVerification** | `boolean` | optional | Require email verification before linking | diff --git a/content/docs/references/system/AuthPluginConfig.mdx b/content/docs/references/system/AuthPluginConfig.mdx new file mode 100644 index 000000000..701df2e7b --- /dev/null +++ b/content/docs/references/system/AuthPluginConfig.mdx @@ -0,0 +1,12 @@ +--- +title: AuthPluginConfig +description: AuthPluginConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Plugin name | +| **enabled** | `boolean` | optional | | +| **options** | `Record` | optional | Plugin-specific options | diff --git a/content/docs/references/system/AuthStrategy.mdx b/content/docs/references/system/AuthStrategy.mdx new file mode 100644 index 000000000..9baafb1ad --- /dev/null +++ b/content/docs/references/system/AuthStrategy.mdx @@ -0,0 +1,13 @@ +--- +title: AuthStrategy +description: AuthStrategy Schema Reference +--- + +## Allowed Values + +* `email_password` +* `magic_link` +* `oauth` +* `passkey` +* `otp` +* `anonymous` \ No newline at end of file diff --git a/content/docs/references/system/AuthenticationConfig.mdx b/content/docs/references/system/AuthenticationConfig.mdx new file mode 100644 index 000000000..eb7e2f280 --- /dev/null +++ b/content/docs/references/system/AuthenticationConfig.mdx @@ -0,0 +1,32 @@ +--- +title: AuthenticationConfig +description: AuthenticationConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Configuration name (snake_case) | +| **label** | `string` | ✅ | Display label | +| **strategies** | `Enum<'email_password' \| 'magic_link' \| 'oauth' \| 'passkey' \| 'otp' \| 'anonymous'>[]` | ✅ | Enabled authentication strategies | +| **baseUrl** | `string` | ✅ | Application base URL | +| **secret** | `string` | ✅ | Secret key for signing (min 32 chars) | +| **emailPassword** | `object` | optional | | +| **magicLink** | `object` | optional | | +| **passkey** | `object` | optional | | +| **oauth** | `object` | optional | | +| **session** | `object` | optional | | +| **rateLimit** | `object` | optional | | +| **csrf** | `object` | optional | | +| **accountLinking** | `object` | optional | | +| **twoFactor** | `object` | optional | | +| **userFieldMapping** | `object` | optional | | +| **database** | `object` | optional | | +| **plugins** | `object[]` | optional | | +| **hooks** | `object` | optional | Authentication lifecycle hooks | +| **security** | `object` | optional | Advanced security settings | +| **email** | `object` | optional | Email configuration | +| **ui** | `object` | optional | UI customization | +| **active** | `boolean` | optional | Whether this provider is active | +| **allowRegistration** | `boolean` | optional | Allow new user registration | diff --git a/content/docs/references/system/AuthenticationProvider.mdx b/content/docs/references/system/AuthenticationProvider.mdx new file mode 100644 index 000000000..e5cc9803b --- /dev/null +++ b/content/docs/references/system/AuthenticationProvider.mdx @@ -0,0 +1,11 @@ +--- +title: AuthenticationProvider +description: AuthenticationProvider Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Provider type identifier | +| **config** | `object` | ✅ | Authentication configuration | diff --git a/content/docs/references/system/CSRFConfig.mdx b/content/docs/references/system/CSRFConfig.mdx new file mode 100644 index 000000000..eba2dddf6 --- /dev/null +++ b/content/docs/references/system/CSRFConfig.mdx @@ -0,0 +1,13 @@ +--- +title: CSRFConfig +description: CSRFConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **tokenLength** | `number` | optional | CSRF token length | +| **cookieName** | `string` | optional | CSRF cookie name | +| **headerName** | `string` | optional | CSRF header name | diff --git a/content/docs/references/system/DatabaseAdapter.mdx b/content/docs/references/system/DatabaseAdapter.mdx new file mode 100644 index 000000000..804f75b03 --- /dev/null +++ b/content/docs/references/system/DatabaseAdapter.mdx @@ -0,0 +1,13 @@ +--- +title: DatabaseAdapter +description: DatabaseAdapter Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'prisma' \| 'drizzle' \| 'kysely' \| 'custom'>` | ✅ | Database adapter type | +| **connectionString** | `string` | optional | Database connection string | +| **tablePrefix** | `string` | optional | Prefix for auth tables | +| **schema** | `string` | optional | Database schema name | diff --git a/content/docs/references/system/EmailPasswordConfig.mdx b/content/docs/references/system/EmailPasswordConfig.mdx new file mode 100644 index 000000000..ed35be72f --- /dev/null +++ b/content/docs/references/system/EmailPasswordConfig.mdx @@ -0,0 +1,15 @@ +--- +title: EmailPasswordConfig +description: EmailPasswordConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **requireEmailVerification** | `boolean` | optional | Require email verification before login | +| **minPasswordLength** | `number` | optional | Minimum password length | +| **requirePasswordComplexity** | `boolean` | optional | Require uppercase, lowercase, numbers, symbols | +| **allowPasswordReset** | `boolean` | optional | Enable password reset functionality | +| **passwordResetExpiry** | `number` | optional | Password reset token expiry in seconds | diff --git a/content/docs/references/system/MagicLinkConfig.mdx b/content/docs/references/system/MagicLinkConfig.mdx new file mode 100644 index 000000000..2971b2862 --- /dev/null +++ b/content/docs/references/system/MagicLinkConfig.mdx @@ -0,0 +1,11 @@ +--- +title: MagicLinkConfig +description: MagicLinkConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **expiryTime** | `number` | optional | Magic link expiry time in seconds (default 15 min) | diff --git a/content/docs/references/system/OAuthProvider.mdx b/content/docs/references/system/OAuthProvider.mdx new file mode 100644 index 000000000..32ff86e83 --- /dev/null +++ b/content/docs/references/system/OAuthProvider.mdx @@ -0,0 +1,17 @@ +--- +title: OAuthProvider +description: OAuthProvider Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `Enum<'google' \| 'github' \| 'facebook' \| 'twitter' \| 'linkedin' \| 'microsoft' \| 'apple' \| 'discord' \| 'gitlab' \| 'custom'>` | ✅ | OAuth provider type | +| **clientId** | `string` | ✅ | OAuth client ID | +| **clientSecret** | `string` | ✅ | OAuth client secret (typically from ENV) | +| **scopes** | `string[]` | optional | Requested OAuth scopes | +| **redirectUri** | `string` | optional | OAuth callback URL | +| **enabled** | `boolean` | optional | Whether this provider is enabled | +| **displayName** | `string` | optional | Display name for the provider button | +| **icon** | `string` | optional | Icon URL or identifier | diff --git a/content/docs/references/system/PasskeyConfig.mdx b/content/docs/references/system/PasskeyConfig.mdx new file mode 100644 index 000000000..9e305ed4c --- /dev/null +++ b/content/docs/references/system/PasskeyConfig.mdx @@ -0,0 +1,15 @@ +--- +title: PasskeyConfig +description: PasskeyConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **rpName** | `string` | ✅ | Relying Party name | +| **rpId** | `string` | optional | Relying Party ID (defaults to domain) | +| **allowedOrigins** | `string[]` | optional | Allowed origins for WebAuthn | +| **userVerification** | `Enum<'required' \| 'preferred' \| 'discouraged'>` | optional | | +| **attestation** | `Enum<'none' \| 'indirect' \| 'direct' \| 'enterprise'>` | optional | | diff --git a/content/docs/references/system/RateLimitConfig.mdx b/content/docs/references/system/RateLimitConfig.mdx new file mode 100644 index 000000000..6682505c5 --- /dev/null +++ b/content/docs/references/system/RateLimitConfig.mdx @@ -0,0 +1,14 @@ +--- +title: RateLimitConfig +description: RateLimitConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **maxAttempts** | `number` | optional | Maximum login attempts | +| **windowMs** | `number` | optional | Time window in milliseconds (default 15 min) | +| **blockDuration** | `number` | optional | Block duration after max attempts in ms | +| **skipSuccessfulRequests** | `boolean` | optional | Only count failed requests | diff --git a/content/docs/references/system/SessionConfig.mdx b/content/docs/references/system/SessionConfig.mdx new file mode 100644 index 000000000..5fb4b1c25 --- /dev/null +++ b/content/docs/references/system/SessionConfig.mdx @@ -0,0 +1,17 @@ +--- +title: SessionConfig +description: SessionConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **expiresIn** | `number` | optional | Session expiry in seconds (default 7 days) | +| **updateAge** | `number` | optional | Session update interval in seconds (default 1 day) | +| **cookieName** | `string` | optional | Session cookie name | +| **cookieSecure** | `boolean` | optional | Use secure cookies (HTTPS only) | +| **cookieSameSite** | `Enum<'strict' \| 'lax' \| 'none'>` | optional | SameSite cookie attribute | +| **cookieDomain** | `string` | optional | Cookie domain | +| **cookiePath** | `string` | optional | Cookie path | +| **cookieHttpOnly** | `boolean` | optional | HttpOnly cookie attribute | diff --git a/content/docs/references/system/TwoFactorConfig.mdx b/content/docs/references/system/TwoFactorConfig.mdx new file mode 100644 index 000000000..70add73db --- /dev/null +++ b/content/docs/references/system/TwoFactorConfig.mdx @@ -0,0 +1,13 @@ +--- +title: TwoFactorConfig +description: TwoFactorConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **issuer** | `string` | optional | TOTP issuer name | +| **qrCodeSize** | `number` | optional | QR code size in pixels | +| **backupCodes** | `object` | optional | | diff --git a/content/docs/references/system/UserFieldMapping.mdx b/content/docs/references/system/UserFieldMapping.mdx new file mode 100644 index 000000000..bfbf94d14 --- /dev/null +++ b/content/docs/references/system/UserFieldMapping.mdx @@ -0,0 +1,16 @@ +--- +title: UserFieldMapping +description: UserFieldMapping Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | optional | User ID field | +| **email** | `string` | optional | Email field | +| **name** | `string` | optional | Name field | +| **image** | `string` | optional | Profile image field | +| **emailVerified** | `string` | optional | Email verification status field | +| **createdAt** | `string` | optional | Created timestamp field | +| **updatedAt** | `string` | optional | Updated timestamp field | diff --git a/docs/BETTER_AUTH_PLUGIN.md b/docs/AUTHENTICATION.md similarity index 93% rename from docs/BETTER_AUTH_PLUGIN.md rename to docs/AUTHENTICATION.md index 1c8290921..756f900e5 100644 --- a/docs/BETTER_AUTH_PLUGIN.md +++ b/docs/AUTHENTICATION.md @@ -1,10 +1,12 @@ -# Better-Auth Authentication Plugin +# Authentication Protocol -A comprehensive authentication plugin for integrating [better-auth](https://better-auth.com) into the ObjectStack ecosystem. +A comprehensive authentication specification for the ObjectStack ecosystem. ## Overview -This plugin provides a complete authentication solution using better-auth as the underlying authentication library. Better-auth is a modern, framework-agnostic authentication library that supports multiple authentication strategies, session management, and comprehensive security features. +This specification defines the standard authentication protocol for ObjectStack applications. It supports multiple authentication strategies, session management, and comprehensive security features. + +The specification is framework-agnostic and can be implemented with any authentication library (better-auth, Auth.js, Passport, etc.) ## Features @@ -52,9 +54,11 @@ pnpm add @objectstack/plugin-better-auth ### Basic Example ```typescript -import { createBetterAuthPlugin } from '@objectstack/plugin-better-auth'; +import type { AuthenticationConfig } from '@objectstack/spec'; -const authPlugin = createBetterAuthPlugin({ +const authConfig: AuthenticationConfig = { + name: 'main_auth', + label: 'Main Authentication', strategies: ['email_password'], baseUrl: 'https://app.example.com', secret: process.env.AUTH_SECRET!, @@ -64,9 +68,12 @@ const authPlugin = createBetterAuthPlugin({ requireEmailVerification: true, minPasswordLength: 8, }, -}); - -export default authPlugin; + + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, +}; ``` ### OAuth Example diff --git a/packages/plugin-better-auth/CHANGELOG.md b/packages/plugin-better-auth/CHANGELOG.md deleted file mode 100644 index c1f89db31..000000000 --- a/packages/plugin-better-auth/CHANGELOG.md +++ /dev/null @@ -1,13 +0,0 @@ -# Changelog - -## [1.0.0] - 2026-01-21 - -### Added -- Initial release of Better-Auth authentication plugin -- Support for multiple authentication strategies (email/password, OAuth, magic link, passkey, OTP, anonymous) -- 10+ OAuth providers (Google, GitHub, Facebook, Twitter, LinkedIn, Microsoft, Apple, Discord, GitLab, custom) -- Advanced security features (rate limiting, CSRF protection, 2FA, session fingerprinting) -- Database adapter support (Prisma, Drizzle, Kysely, custom) -- Email provider integration (SMTP, SendGrid, Mailgun, AWS SES, Resend, custom) -- Lifecycle hooks for authentication events -- Comprehensive examples and documentation diff --git a/packages/plugin-better-auth/README.md b/packages/plugin-better-auth/README.md deleted file mode 100644 index 638b23e2a..000000000 --- a/packages/plugin-better-auth/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# @objectstack/plugin-better-auth - -Better-Auth authentication plugin for ObjectStack. - -## Installation - -```bash -pnpm add @objectstack/plugin-better-auth -``` - -## Usage - -```typescript -import { BetterAuthPlugin } from '@objectstack/plugin-better-auth'; - -const authPlugin = new BetterAuthPlugin({ - strategies: ['email_password', 'oauth'], - baseUrl: 'https://app.example.com', - secret: process.env.AUTH_SECRET!, - - emailPassword: { - enabled: true, - requireEmailVerification: true, - }, - - oauth: { - providers: [ - { - provider: 'google', - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - }, - ], - }, -}); - -export default authPlugin; -``` - -## Features - -- Multiple authentication strategies (email/password, OAuth, magic links, passkeys, OTP, anonymous) -- 10+ OAuth providers (Google, GitHub, Facebook, Twitter, LinkedIn, Microsoft, Apple, Discord, GitLab) -- Advanced security features (rate limiting, CSRF protection, 2FA, session fingerprinting) -- Database adapter support (Prisma, Drizzle, Kysely) -- Email provider integration (SMTP, SendGrid, Mailgun, AWS SES, Resend) -- Lifecycle hooks for authentication events - -## Documentation - -See [docs/BETTER_AUTH_PLUGIN.md](../../docs/BETTER_AUTH_PLUGIN.md) for comprehensive documentation. - -## License - -Apache-2.0 diff --git a/packages/plugin-better-auth/examples/plugin-examples.ts b/packages/plugin-better-auth/examples/plugin-examples.ts deleted file mode 100644 index c0572278e..000000000 --- a/packages/plugin-better-auth/examples/plugin-examples.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Better-Auth Plugin Examples - * - * This file demonstrates various configurations for the Better-Auth plugin. - */ - -import { createBetterAuthPlugin } from '../src/index'; - -/** - * Example 1: Basic Email/Password Authentication - */ -export const basicEmailAuthPlugin = createBetterAuthPlugin({ - strategies: ['email_password'], - baseUrl: 'https://app.example.com', - secret: process.env.AUTH_SECRET || 'your-secret-key-min-32-characters-long', - - emailPassword: { - enabled: true, - requireEmailVerification: true, - minPasswordLength: 8, - requirePasswordComplexity: true, - allowPasswordReset: true, - }, - - session: { - expiresIn: 604800, // 7 days - cookieSecure: true, - cookieSameSite: 'lax', - }, - - rateLimit: { - enabled: true, - maxAttempts: 5, - windowMs: 900000, // 15 minutes - }, - - csrf: { - enabled: true, - }, -}); - -/** - * Example 2: OAuth with Social Providers - */ -export const socialAuthPlugin = createBetterAuthPlugin({ - strategies: ['oauth'], - baseUrl: 'https://app.example.com', - secret: process.env.AUTH_SECRET!, - - oauth: { - providers: [ - { - provider: 'google', - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - scopes: ['openid', 'profile', 'email'], - enabled: true, - displayName: 'Sign in with Google', - }, - { - provider: 'github', - clientId: process.env.GITHUB_CLIENT_ID!, - clientSecret: process.env.GITHUB_CLIENT_SECRET!, - scopes: ['user:email'], - enabled: true, - displayName: 'Sign in with GitHub', - }, - ], - }, - - accountLinking: { - enabled: true, - autoLink: true, - }, - - session: { - expiresIn: 2592000, // 30 days - }, -}); - -/** - * Example 3: Multi-Strategy Authentication - */ -export const multiStrategyPlugin = createBetterAuthPlugin({ - strategies: ['email_password', 'oauth', 'magic_link'], - baseUrl: 'https://app.example.com', - secret: process.env.AUTH_SECRET!, - - emailPassword: { - enabled: true, - requireEmailVerification: true, - minPasswordLength: 10, - }, - - oauth: { - providers: [ - { - provider: 'google', - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - }, - ], - }, - - magicLink: { - enabled: true, - expiryTime: 900, // 15 minutes - }, - - hooks: { - beforeSignIn: async ({ email }) => { - console.log(`User ${email} attempting to sign in`); - }, - - afterSignIn: async ({ user, session }) => { - console.log(`User ${user.id} signed in successfully`); - }, - - afterSignUp: async ({ user }) => { - console.log(`New user registered: ${user.email}`); - // Send welcome email, create default data, etc. - }, - }, -}); - -// Export default for easy importing -export default multiStrategyPlugin; diff --git a/packages/plugin-better-auth/objectstack.config.ts b/packages/plugin-better-auth/objectstack.config.ts deleted file mode 100644 index 29a5bd402..000000000 --- a/packages/plugin-better-auth/objectstack.config.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { ObjectStackManifest } from '@objectstack/spec'; - -const manifest: ObjectStackManifest = { - id: 'com.objectstack.plugin.better-auth', - name: 'Better-Auth Authentication Plugin', - version: '1.0.0', - type: 'plugin', - description: 'Modern authentication plugin powered by better-auth, supporting multiple strategies including email/password, OAuth, magic links, passkeys, and more.', - - // Required permissions - permissions: [ - 'system.user.read', - 'system.user.write', - 'system.session.manage', - 'system.routes.register', - ], - - // Plugin configuration schema - configuration: { - title: 'Better-Auth Configuration', - properties: { - strategies: { - type: 'array', - description: 'Enabled authentication strategies', - required: true, - }, - baseUrl: { - type: 'string', - description: 'Application base URL', - required: true, - }, - secret: { - type: 'string', - description: 'Secret key for signing tokens (min 32 characters)', - required: true, - secret: true, - }, - emailPassword: { - type: 'object', - description: 'Email/Password authentication configuration', - }, - oauth: { - type: 'object', - description: 'OAuth providers configuration', - }, - session: { - type: 'object', - description: 'Session management configuration', - }, - rateLimit: { - type: 'object', - description: 'Rate limiting configuration', - }, - csrf: { - type: 'object', - description: 'CSRF protection configuration', - }, - twoFactor: { - type: 'object', - description: 'Two-factor authentication configuration', - }, - database: { - type: 'object', - description: 'Database adapter configuration', - }, - }, - }, - - // Platform contributions - contributes: { - events: [ - 'auth.before_signin', - 'auth.after_signin', - 'auth.before_signup', - 'auth.after_signup', - 'auth.before_signout', - 'auth.after_signout', - 'auth.session_created', - 'auth.session_expired', - ], - - actions: [ - { - name: 'authenticate_user', - label: 'Authenticate User', - description: 'Authenticate a user with the provided credentials', - input: { - email: 'string', - password: 'string', - }, - output: { - user: 'object', - session: 'object', - }, - }, - { - name: 'send_magic_link', - label: 'Send Magic Link', - description: 'Send a magic link to the user\'s email', - input: { - email: 'string', - }, - }, - { - name: 'verify_session', - label: 'Verify Session', - description: 'Verify if a session is valid', - input: { - sessionId: 'string', - }, - output: { - valid: 'boolean', - user: 'object', - }, - }, - ], - }, - - // Runtime entry point - extensions: { - runtime: { - entry: './dist/index.js', - }, - }, -}; - -export default manifest; diff --git a/packages/plugin-better-auth/package.json b/packages/plugin-better-auth/package.json deleted file mode 100644 index d4dcc7a53..000000000 --- a/packages/plugin-better-auth/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@objectstack/plugin-better-auth", - "version": "1.0.0", - "description": "Better-Auth authentication plugin for ObjectStack", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "scripts": { - "build": "tsc", - "test": "vitest run", - "test:watch": "vitest" - }, - "keywords": [ - "objectstack", - "plugin", - "authentication", - "better-auth", - "auth" - ], - "author": "ObjectStack", - "license": "Apache-2.0", - "dependencies": { - "@objectstack/spec": "workspace:*", - "better-auth": "^1.0.0" - }, - "devDependencies": { - "@types/node": "^20.0.0", - "typescript": "^5.0.0", - "vitest": "^2.0.0" - }, - "peerDependencies": { - "@objectstack/runtime": "^0.1.0" - } -} diff --git a/packages/plugin-better-auth/src/index.ts b/packages/plugin-better-auth/src/index.ts deleted file mode 100644 index 428afa3b7..000000000 --- a/packages/plugin-better-auth/src/index.ts +++ /dev/null @@ -1,365 +0,0 @@ -import { PluginDefinition, PluginContextData } from '@objectstack/spec'; - -/** - * Better-Auth Configuration Interface - * - * Configuration options for the better-auth authentication plugin. - */ -export interface BetterAuthConfig { - /** Enabled authentication strategies */ - strategies: Array<'email_password' | 'magic_link' | 'oauth' | 'passkey' | 'otp' | 'anonymous'>; - - /** Application base URL */ - baseUrl: string; - - /** Secret key for signing tokens (min 32 characters) */ - secret: string; - - /** Email/Password configuration */ - emailPassword?: { - enabled?: boolean; - requireEmailVerification?: boolean; - minPasswordLength?: number; - requirePasswordComplexity?: boolean; - allowPasswordReset?: boolean; - passwordResetExpiry?: number; - }; - - /** Magic Link configuration */ - magicLink?: { - enabled?: boolean; - expiryTime?: number; - }; - - /** Passkey (WebAuthn) configuration */ - passkey?: { - enabled?: boolean; - rpName: string; - rpId?: string; - userVerification?: 'required' | 'preferred' | 'discouraged'; - attestation?: 'none' | 'indirect' | 'direct' | 'enterprise'; - }; - - /** OAuth configuration */ - oauth?: { - providers: Array<{ - provider: 'google' | 'github' | 'facebook' | 'twitter' | 'linkedin' | 'microsoft' | 'apple' | 'discord' | 'gitlab' | 'custom'; - clientId: string; - clientSecret: string; - scopes?: string[]; - redirectUri?: string; - enabled?: boolean; - displayName?: string; - icon?: string; - }>; - }; - - /** Session configuration */ - session?: { - expiresIn?: number; - updateAge?: number; - cookieName?: string; - cookieSecure?: boolean; - cookieSameSite?: 'strict' | 'lax' | 'none'; - cookieDomain?: string; - cookiePath?: string; - cookieHttpOnly?: boolean; - }; - - /** Rate limiting configuration */ - rateLimit?: { - enabled?: boolean; - maxAttempts?: number; - windowMs?: number; - blockDuration?: number; - skipSuccessfulRequests?: boolean; - }; - - /** CSRF protection configuration */ - csrf?: { - enabled?: boolean; - tokenLength?: number; - cookieName?: string; - headerName?: string; - }; - - /** Account linking configuration */ - accountLinking?: { - enabled?: boolean; - autoLink?: boolean; - requireVerification?: boolean; - }; - - /** Two-factor authentication configuration */ - twoFactor?: { - enabled?: boolean; - issuer?: string; - qrCodeSize?: number; - backupCodes?: { - enabled?: boolean; - count?: number; - }; - }; - - /** Database adapter configuration */ - database?: { - type: 'prisma' | 'drizzle' | 'kysely' | 'custom'; - connectionString?: string; - tablePrefix?: string; - schema?: string; - }; - - /** Lifecycle hooks */ - hooks?: { - beforeSignIn?: (data: { email: string }) => Promise; - afterSignIn?: (data: { user: any; session: any }) => Promise; - beforeSignUp?: (data: { email: string; name?: string }) => Promise; - afterSignUp?: (data: { user: any }) => Promise; - beforeSignOut?: (data: { sessionId: string }) => Promise; - afterSignOut?: (data: { sessionId: string }) => Promise; - }; - - /** Security settings */ - security?: { - allowedOrigins?: string[]; - trustProxy?: boolean; - ipRateLimiting?: boolean; - sessionFingerprinting?: boolean; - maxSessions?: number; - }; - - /** Email configuration */ - email?: { - from: string; - fromName?: string; - provider: 'smtp' | 'sendgrid' | 'mailgun' | 'ses' | 'resend' | 'custom'; - config?: Record; - }; -} - -/** - * Better-Auth Plugin Class - * - * Integrates better-auth authentication library into ObjectStack applications. - */ -export class BetterAuthPlugin implements PluginDefinition { - id = 'com.objectstack.plugin.better-auth'; - version = '1.0.0'; - - private config: BetterAuthConfig; - private authInstance: any; // better-auth instance - - constructor(config: BetterAuthConfig) { - this.config = config; - } - - /** - * Called when the plugin is installed - */ - async onInstall(context: PluginContextData): Promise { - const { logger, ql } = context; - - logger.info('[Better-Auth] Installing plugin...'); - - // Create auth tables in database if needed - if (this.config.database) { - logger.info('[Better-Auth] Setting up database tables...'); - // In a real implementation, this would create the necessary tables - } - - logger.info('[Better-Auth] Plugin installed successfully'); - } - - /** - * Called when the plugin is enabled - */ - async onEnable(context: PluginContextData): Promise { - const { logger, app, storage, os } = context; - - logger.info('[Better-Auth] Enabling plugin...'); - - // Initialize better-auth (in real implementation, would use actual better-auth library) - this.authInstance = this.initializeAuth(); - - // Register authentication routes - this.registerRoutes(app.router, logger); - - // Store configuration - await storage.set('better-auth:config', this.config); - - logger.info('[Better-Auth] Plugin enabled successfully'); - logger.info(`[Better-Auth] Strategies: ${this.config.strategies.join(', ')}`); - } - - /** - * Called when the plugin is disabled - */ - async onDisable(context: PluginContextData): Promise { - const { logger } = context; - - logger.info('[Better-Auth] Disabling plugin...'); - - // Cleanup resources - this.authInstance = null; - - logger.info('[Better-Auth] Plugin disabled'); - } - - /** - * Called when the plugin is uninstalled - */ - async onUninstall(context: PluginContextData): Promise { - const { logger, storage } = context; - - logger.info('[Better-Auth] Uninstalling plugin...'); - - // Remove stored configuration - await storage.delete('better-auth:config'); - - // In a real implementation, might ask user if they want to keep database tables - - logger.info('[Better-Auth] Plugin uninstalled'); - } - - /** - * Initialize the better-auth instance - */ - private initializeAuth(): any { - // In a real implementation, this would create a better-auth instance - // with the provided configuration - - const auth = { - strategies: this.config.strategies, - config: this.config, - }; - - return auth; - } - - /** - * Register authentication routes - */ - private registerRoutes(router: any, logger: any): void { - // Sign In route - router.post('/auth/signin', async (req: any, res: any) => { - logger.info('[Better-Auth] Sign in request'); - - // Execute beforeSignIn hook - if (this.config.hooks?.beforeSignIn) { - await this.config.hooks.beforeSignIn({ email: req.body.email }); - } - - // In real implementation, would call better-auth sign in - const user = { id: '123', email: req.body.email }; - const session = { id: 'session-123', userId: '123' }; - - // Execute afterSignIn hook - if (this.config.hooks?.afterSignIn) { - await this.config.hooks.afterSignIn({ user, session }); - } - - return { success: true, user, session }; - }); - - // Sign Up route - router.post('/auth/signup', async (req: any, res: any) => { - logger.info('[Better-Auth] Sign up request'); - - // Execute beforeSignUp hook - if (this.config.hooks?.beforeSignUp) { - await this.config.hooks.beforeSignUp({ - email: req.body.email, - name: req.body.name - }); - } - - // In real implementation, would call better-auth sign up - const user = { id: '123', email: req.body.email, name: req.body.name }; - - // Execute afterSignUp hook - if (this.config.hooks?.afterSignUp) { - await this.config.hooks.afterSignUp({ user }); - } - - return { success: true, user }; - }); - - // Sign Out route - router.post('/auth/signout', async (req: any, res: any) => { - logger.info('[Better-Auth] Sign out request'); - - const sessionId = req.body.sessionId || 'session-123'; - - // Execute beforeSignOut hook - if (this.config.hooks?.beforeSignOut) { - await this.config.hooks.beforeSignOut({ sessionId }); - } - - // In real implementation, would call better-auth sign out - - // Execute afterSignOut hook - if (this.config.hooks?.afterSignOut) { - await this.config.hooks.afterSignOut({ sessionId }); - } - - return { success: true }; - }); - - // OAuth routes (if OAuth is enabled) - if (this.config.oauth) { - this.config.oauth.providers.forEach((provider) => { - if (provider.enabled !== false) { - router.get(`/auth/oauth/${provider.provider}`, async (req: any, res: any) => { - logger.info(`[Better-Auth] OAuth redirect for ${provider.provider}`); - // In real implementation, would redirect to OAuth provider - return { redirect: `https://${provider.provider}.com/oauth/authorize` }; - }); - - router.get(`/auth/oauth/${provider.provider}/callback`, async (req: any, res: any) => { - logger.info(`[Better-Auth] OAuth callback for ${provider.provider}`); - // In real implementation, would handle OAuth callback - return { success: true }; - }); - } - }); - } - - // Magic Link routes (if magic link is enabled) - if (this.config.strategies.includes('magic_link')) { - router.post('/auth/magic-link/send', async (req: any, res: any) => { - logger.info('[Better-Auth] Sending magic link'); - // In real implementation, would send magic link email - return { success: true, message: 'Magic link sent' }; - }); - - router.get('/auth/magic-link/verify', async (req: any, res: any) => { - logger.info('[Better-Auth] Verifying magic link'); - // In real implementation, would verify magic link token - return { success: true }; - }); - } - - // Status route - router.get('/auth/status', async () => { - return { - status: 'active', - strategies: this.config.strategies, - version: this.version, - }; - }); - - logger.info('[Better-Auth] Routes registered successfully'); - } -} - -/** - * Create a Better-Auth plugin instance - */ -export function createBetterAuthPlugin(config: BetterAuthConfig): PluginDefinition { - return new BetterAuthPlugin(config); -} - -/** - * Default export following ObjectStack plugin conventions - */ -export default createBetterAuthPlugin; diff --git a/packages/plugin-better-auth/tsconfig.json b/packages/plugin-better-auth/tsconfig.json deleted file mode 100644 index 1f147cc4d..000000000 --- a/packages/plugin-better-auth/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "lib": ["ES2020"], - "declaration": true, - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "resolveJsonModule": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.test.ts"] -} diff --git a/packages/spec/json-schema/AccountLinkingConfig.json b/packages/spec/json-schema/AccountLinkingConfig.json new file mode 100644 index 000000000..f635ac7cc --- /dev/null +++ b/packages/spec/json-schema/AccountLinkingConfig.json @@ -0,0 +1,27 @@ +{ + "$ref": "#/definitions/AccountLinkingConfig", + "definitions": { + "AccountLinkingConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow account linking" + }, + "autoLink": { + "type": "boolean", + "default": false, + "description": "Automatically link accounts with same email" + }, + "requireVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before linking" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/AuthPluginConfig.json b/packages/spec/json-schema/AuthPluginConfig.json new file mode 100644 index 000000000..89e6aa218 --- /dev/null +++ b/packages/spec/json-schema/AuthPluginConfig.json @@ -0,0 +1,28 @@ +{ + "$ref": "#/definitions/AuthPluginConfig", + "definitions": { + "AuthPluginConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Plugin name" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "additionalProperties": {}, + "description": "Plugin-specific options" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/AuthStrategy.json b/packages/spec/json-schema/AuthStrategy.json new file mode 100644 index 000000000..05dd0c08d --- /dev/null +++ b/packages/spec/json-schema/AuthStrategy.json @@ -0,0 +1,17 @@ +{ + "$ref": "#/definitions/AuthStrategy", + "definitions": { + "AuthStrategy": { + "type": "string", + "enum": [ + "email_password", + "magic_link", + "oauth", + "passkey", + "otp", + "anonymous" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/AuthenticationConfig.json b/packages/spec/json-schema/AuthenticationConfig.json new file mode 100644 index 000000000..15e1effbe --- /dev/null +++ b/packages/spec/json-schema/AuthenticationConfig.json @@ -0,0 +1,601 @@ +{ + "$ref": "#/definitions/AuthenticationConfig", + "definitions": { + "AuthenticationConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Configuration name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label" + }, + "strategies": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email_password", + "magic_link", + "oauth", + "passkey", + "otp", + "anonymous" + ] + }, + "minItems": 1, + "description": "Enabled authentication strategies" + }, + "baseUrl": { + "type": "string", + "format": "uri", + "description": "Application base URL" + }, + "secret": { + "type": "string", + "minLength": 32, + "description": "Secret key for signing (min 32 chars)" + }, + "emailPassword": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "requireEmailVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before login" + }, + "minPasswordLength": { + "type": "number", + "minimum": 6, + "maximum": 128, + "default": 8, + "description": "Minimum password length" + }, + "requirePasswordComplexity": { + "type": "boolean", + "default": true, + "description": "Require uppercase, lowercase, numbers, symbols" + }, + "allowPasswordReset": { + "type": "boolean", + "default": true, + "description": "Enable password reset functionality" + }, + "passwordResetExpiry": { + "type": "number", + "default": 3600, + "description": "Password reset token expiry in seconds" + } + }, + "additionalProperties": false + }, + "magicLink": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "expiryTime": { + "type": "number", + "default": 900, + "description": "Magic link expiry time in seconds (default 15 min)" + } + }, + "additionalProperties": false + }, + "passkey": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "rpName": { + "type": "string", + "description": "Relying Party name" + }, + "rpId": { + "type": "string", + "description": "Relying Party ID (defaults to domain)" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Allowed origins for WebAuthn" + }, + "userVerification": { + "type": "string", + "enum": [ + "required", + "preferred", + "discouraged" + ], + "default": "preferred" + }, + "attestation": { + "type": "string", + "enum": [ + "none", + "indirect", + "direct", + "enterprise" + ], + "default": "none" + } + }, + "required": [ + "rpName" + ], + "additionalProperties": false + }, + "oauth": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google", + "github", + "facebook", + "twitter", + "linkedin", + "microsoft", + "apple", + "discord", + "gitlab", + "custom" + ], + "description": "OAuth provider type" + }, + "clientId": { + "type": "string", + "description": "OAuth client ID" + }, + "clientSecret": { + "type": "string", + "description": "OAuth client secret (typically from ENV)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Requested OAuth scopes" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "description": "OAuth callback URL" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this provider is enabled" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "provider", + "clientId", + "clientSecret" + ], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": [ + "providers" + ], + "additionalProperties": false + }, + "session": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number", + "default": 604800, + "description": "Session expiry in seconds (default 7 days)" + }, + "updateAge": { + "type": "number", + "default": 86400, + "description": "Session update interval in seconds (default 1 day)" + }, + "cookieName": { + "type": "string", + "default": "session_token", + "description": "Session cookie name" + }, + "cookieSecure": { + "type": "boolean", + "default": true, + "description": "Use secure cookies (HTTPS only)" + }, + "cookieSameSite": { + "type": "string", + "enum": [ + "strict", + "lax", + "none" + ], + "default": "lax", + "description": "SameSite cookie attribute" + }, + "cookieDomain": { + "type": "string", + "description": "Cookie domain" + }, + "cookiePath": { + "type": "string", + "default": "/", + "description": "Cookie path" + }, + "cookieHttpOnly": { + "type": "boolean", + "default": true, + "description": "HttpOnly cookie attribute" + } + }, + "additionalProperties": false, + "default": {} + }, + "rateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "maxAttempts": { + "type": "number", + "default": 5, + "description": "Maximum login attempts" + }, + "windowMs": { + "type": "number", + "default": 900000, + "description": "Time window in milliseconds (default 15 min)" + }, + "blockDuration": { + "type": "number", + "default": 900000, + "description": "Block duration after max attempts in ms" + }, + "skipSuccessfulRequests": { + "type": "boolean", + "default": false, + "description": "Only count failed requests" + } + }, + "additionalProperties": false, + "default": {} + }, + "csrf": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "tokenLength": { + "type": "number", + "default": 32, + "description": "CSRF token length" + }, + "cookieName": { + "type": "string", + "default": "csrf_token", + "description": "CSRF cookie name" + }, + "headerName": { + "type": "string", + "default": "X-CSRF-Token", + "description": "CSRF header name" + } + }, + "additionalProperties": false, + "default": {} + }, + "accountLinking": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow account linking" + }, + "autoLink": { + "type": "boolean", + "default": false, + "description": "Automatically link accounts with same email" + }, + "requireVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before linking" + } + }, + "additionalProperties": false, + "default": {} + }, + "twoFactor": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "description": "TOTP issuer name" + }, + "qrCodeSize": { + "type": "number", + "default": 200, + "description": "QR code size in pixels" + }, + "backupCodes": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "count": { + "type": "number", + "default": 10, + "description": "Number of backup codes to generate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "userFieldMapping": { + "type": "object", + "properties": { + "id": { + "type": "string", + "default": "id", + "description": "User ID field" + }, + "email": { + "type": "string", + "default": "email", + "description": "Email field" + }, + "name": { + "type": "string", + "default": "name", + "description": "Name field" + }, + "image": { + "type": "string", + "default": "image", + "description": "Profile image field" + }, + "emailVerified": { + "type": "string", + "default": "email_verified", + "description": "Email verification status field" + }, + "createdAt": { + "type": "string", + "default": "created_at", + "description": "Created timestamp field" + }, + "updatedAt": { + "type": "string", + "default": "updated_at", + "description": "Updated timestamp field" + } + }, + "additionalProperties": false, + "default": {} + }, + "database": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "prisma", + "drizzle", + "kysely", + "custom" + ], + "description": "Database adapter type" + }, + "connectionString": { + "type": "string", + "description": "Database connection string" + }, + "tablePrefix": { + "type": "string", + "default": "auth_", + "description": "Prefix for auth tables" + }, + "schema": { + "type": "string", + "description": "Database schema name" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "plugins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Plugin name" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "additionalProperties": {}, + "description": "Plugin-specific options" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "default": [] + }, + "hooks": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "description": "Authentication lifecycle hooks" + }, + "security": { + "type": "object", + "properties": { + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed origins" + }, + "trustProxy": { + "type": "boolean", + "default": false, + "description": "Trust proxy headers" + }, + "ipRateLimiting": { + "type": "boolean", + "default": true, + "description": "Enable IP-based rate limiting" + }, + "sessionFingerprinting": { + "type": "boolean", + "default": true, + "description": "Enable session fingerprinting" + }, + "maxSessions": { + "type": "number", + "default": 5, + "description": "Maximum concurrent sessions per user" + } + }, + "additionalProperties": false, + "description": "Advanced security settings" + }, + "email": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "email", + "description": "From email address" + }, + "fromName": { + "type": "string", + "description": "From name" + }, + "provider": { + "type": "string", + "enum": [ + "smtp", + "sendgrid", + "mailgun", + "ses", + "resend", + "custom" + ], + "description": "Email provider" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific configuration" + } + }, + "required": [ + "from", + "provider" + ], + "additionalProperties": false, + "description": "Email configuration" + }, + "ui": { + "type": "object", + "properties": { + "brandName": { + "type": "string", + "description": "Brand name displayed in auth UI" + }, + "logo": { + "type": "string", + "description": "Logo URL" + }, + "primaryColor": { + "type": "string", + "description": "Primary brand color (hex)" + }, + "customCss": { + "type": "string", + "description": "Custom CSS for auth pages" + } + }, + "additionalProperties": false, + "description": "UI customization" + }, + "active": { + "type": "boolean", + "default": true, + "description": "Whether this provider is active" + }, + "allowRegistration": { + "type": "boolean", + "default": true, + "description": "Allow new user registration" + } + }, + "required": [ + "name", + "label", + "strategies", + "baseUrl", + "secret" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/AuthenticationProvider.json b/packages/spec/json-schema/AuthenticationProvider.json new file mode 100644 index 000000000..394907760 --- /dev/null +++ b/packages/spec/json-schema/AuthenticationProvider.json @@ -0,0 +1,617 @@ +{ + "$ref": "#/definitions/AuthenticationProvider", + "definitions": { + "AuthenticationProvider": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authentication", + "description": "Provider type identifier" + }, + "config": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Configuration name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label" + }, + "strategies": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email_password", + "magic_link", + "oauth", + "passkey", + "otp", + "anonymous" + ] + }, + "minItems": 1, + "description": "Enabled authentication strategies" + }, + "baseUrl": { + "type": "string", + "format": "uri", + "description": "Application base URL" + }, + "secret": { + "type": "string", + "minLength": 32, + "description": "Secret key for signing (min 32 chars)" + }, + "emailPassword": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "requireEmailVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before login" + }, + "minPasswordLength": { + "type": "number", + "minimum": 6, + "maximum": 128, + "default": 8, + "description": "Minimum password length" + }, + "requirePasswordComplexity": { + "type": "boolean", + "default": true, + "description": "Require uppercase, lowercase, numbers, symbols" + }, + "allowPasswordReset": { + "type": "boolean", + "default": true, + "description": "Enable password reset functionality" + }, + "passwordResetExpiry": { + "type": "number", + "default": 3600, + "description": "Password reset token expiry in seconds" + } + }, + "additionalProperties": false + }, + "magicLink": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "expiryTime": { + "type": "number", + "default": 900, + "description": "Magic link expiry time in seconds (default 15 min)" + } + }, + "additionalProperties": false + }, + "passkey": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "rpName": { + "type": "string", + "description": "Relying Party name" + }, + "rpId": { + "type": "string", + "description": "Relying Party ID (defaults to domain)" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Allowed origins for WebAuthn" + }, + "userVerification": { + "type": "string", + "enum": [ + "required", + "preferred", + "discouraged" + ], + "default": "preferred" + }, + "attestation": { + "type": "string", + "enum": [ + "none", + "indirect", + "direct", + "enterprise" + ], + "default": "none" + } + }, + "required": [ + "rpName" + ], + "additionalProperties": false + }, + "oauth": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google", + "github", + "facebook", + "twitter", + "linkedin", + "microsoft", + "apple", + "discord", + "gitlab", + "custom" + ], + "description": "OAuth provider type" + }, + "clientId": { + "type": "string", + "description": "OAuth client ID" + }, + "clientSecret": { + "type": "string", + "description": "OAuth client secret (typically from ENV)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Requested OAuth scopes" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "description": "OAuth callback URL" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this provider is enabled" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "provider", + "clientId", + "clientSecret" + ], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": [ + "providers" + ], + "additionalProperties": false + }, + "session": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number", + "default": 604800, + "description": "Session expiry in seconds (default 7 days)" + }, + "updateAge": { + "type": "number", + "default": 86400, + "description": "Session update interval in seconds (default 1 day)" + }, + "cookieName": { + "type": "string", + "default": "session_token", + "description": "Session cookie name" + }, + "cookieSecure": { + "type": "boolean", + "default": true, + "description": "Use secure cookies (HTTPS only)" + }, + "cookieSameSite": { + "type": "string", + "enum": [ + "strict", + "lax", + "none" + ], + "default": "lax", + "description": "SameSite cookie attribute" + }, + "cookieDomain": { + "type": "string", + "description": "Cookie domain" + }, + "cookiePath": { + "type": "string", + "default": "/", + "description": "Cookie path" + }, + "cookieHttpOnly": { + "type": "boolean", + "default": true, + "description": "HttpOnly cookie attribute" + } + }, + "additionalProperties": false, + "default": {} + }, + "rateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "maxAttempts": { + "type": "number", + "default": 5, + "description": "Maximum login attempts" + }, + "windowMs": { + "type": "number", + "default": 900000, + "description": "Time window in milliseconds (default 15 min)" + }, + "blockDuration": { + "type": "number", + "default": 900000, + "description": "Block duration after max attempts in ms" + }, + "skipSuccessfulRequests": { + "type": "boolean", + "default": false, + "description": "Only count failed requests" + } + }, + "additionalProperties": false, + "default": {} + }, + "csrf": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "tokenLength": { + "type": "number", + "default": 32, + "description": "CSRF token length" + }, + "cookieName": { + "type": "string", + "default": "csrf_token", + "description": "CSRF cookie name" + }, + "headerName": { + "type": "string", + "default": "X-CSRF-Token", + "description": "CSRF header name" + } + }, + "additionalProperties": false, + "default": {} + }, + "accountLinking": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow account linking" + }, + "autoLink": { + "type": "boolean", + "default": false, + "description": "Automatically link accounts with same email" + }, + "requireVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before linking" + } + }, + "additionalProperties": false, + "default": {} + }, + "twoFactor": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "description": "TOTP issuer name" + }, + "qrCodeSize": { + "type": "number", + "default": 200, + "description": "QR code size in pixels" + }, + "backupCodes": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "count": { + "type": "number", + "default": 10, + "description": "Number of backup codes to generate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "userFieldMapping": { + "type": "object", + "properties": { + "id": { + "type": "string", + "default": "id", + "description": "User ID field" + }, + "email": { + "type": "string", + "default": "email", + "description": "Email field" + }, + "name": { + "type": "string", + "default": "name", + "description": "Name field" + }, + "image": { + "type": "string", + "default": "image", + "description": "Profile image field" + }, + "emailVerified": { + "type": "string", + "default": "email_verified", + "description": "Email verification status field" + }, + "createdAt": { + "type": "string", + "default": "created_at", + "description": "Created timestamp field" + }, + "updatedAt": { + "type": "string", + "default": "updated_at", + "description": "Updated timestamp field" + } + }, + "additionalProperties": false, + "default": {} + }, + "database": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "prisma", + "drizzle", + "kysely", + "custom" + ], + "description": "Database adapter type" + }, + "connectionString": { + "type": "string", + "description": "Database connection string" + }, + "tablePrefix": { + "type": "string", + "default": "auth_", + "description": "Prefix for auth tables" + }, + "schema": { + "type": "string", + "description": "Database schema name" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "plugins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Plugin name" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "additionalProperties": {}, + "description": "Plugin-specific options" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "default": [] + }, + "hooks": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "description": "Authentication lifecycle hooks" + }, + "security": { + "type": "object", + "properties": { + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed origins" + }, + "trustProxy": { + "type": "boolean", + "default": false, + "description": "Trust proxy headers" + }, + "ipRateLimiting": { + "type": "boolean", + "default": true, + "description": "Enable IP-based rate limiting" + }, + "sessionFingerprinting": { + "type": "boolean", + "default": true, + "description": "Enable session fingerprinting" + }, + "maxSessions": { + "type": "number", + "default": 5, + "description": "Maximum concurrent sessions per user" + } + }, + "additionalProperties": false, + "description": "Advanced security settings" + }, + "email": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "email", + "description": "From email address" + }, + "fromName": { + "type": "string", + "description": "From name" + }, + "provider": { + "type": "string", + "enum": [ + "smtp", + "sendgrid", + "mailgun", + "ses", + "resend", + "custom" + ], + "description": "Email provider" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific configuration" + } + }, + "required": [ + "from", + "provider" + ], + "additionalProperties": false, + "description": "Email configuration" + }, + "ui": { + "type": "object", + "properties": { + "brandName": { + "type": "string", + "description": "Brand name displayed in auth UI" + }, + "logo": { + "type": "string", + "description": "Logo URL" + }, + "primaryColor": { + "type": "string", + "description": "Primary brand color (hex)" + }, + "customCss": { + "type": "string", + "description": "Custom CSS for auth pages" + } + }, + "additionalProperties": false, + "description": "UI customization" + }, + "active": { + "type": "boolean", + "default": true, + "description": "Whether this provider is active" + }, + "allowRegistration": { + "type": "boolean", + "default": true, + "description": "Allow new user registration" + } + }, + "required": [ + "name", + "label", + "strategies", + "baseUrl", + "secret" + ], + "additionalProperties": false, + "description": "Authentication configuration" + } + }, + "required": [ + "type", + "config" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/CSRFConfig.json b/packages/spec/json-schema/CSRFConfig.json new file mode 100644 index 000000000..7c49ed70e --- /dev/null +++ b/packages/spec/json-schema/CSRFConfig.json @@ -0,0 +1,31 @@ +{ + "$ref": "#/definitions/CSRFConfig", + "definitions": { + "CSRFConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "tokenLength": { + "type": "number", + "default": 32, + "description": "CSRF token length" + }, + "cookieName": { + "type": "string", + "default": "csrf_token", + "description": "CSRF cookie name" + }, + "headerName": { + "type": "string", + "default": "X-CSRF-Token", + "description": "CSRF header name" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/DatabaseAdapter.json b/packages/spec/json-schema/DatabaseAdapter.json new file mode 100644 index 000000000..4695c1a23 --- /dev/null +++ b/packages/spec/json-schema/DatabaseAdapter.json @@ -0,0 +1,38 @@ +{ + "$ref": "#/definitions/DatabaseAdapter", + "definitions": { + "DatabaseAdapter": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "prisma", + "drizzle", + "kysely", + "custom" + ], + "description": "Database adapter type" + }, + "connectionString": { + "type": "string", + "description": "Database connection string" + }, + "tablePrefix": { + "type": "string", + "default": "auth_", + "description": "Prefix for auth tables" + }, + "schema": { + "type": "string", + "description": "Database schema name" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/EmailAlertAction.json b/packages/spec/json-schema/EmailAlertAction.json new file mode 100644 index 000000000..3462a3b7b --- /dev/null +++ b/packages/spec/json-schema/EmailAlertAction.json @@ -0,0 +1,37 @@ +{ + "$ref": "#/definitions/EmailAlertAction", + "definitions": { + "EmailAlertAction": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "email_alert" + }, + "template": { + "type": "string", + "description": "Email template ID/DevName" + }, + "recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of recipient emails or user IDs" + } + }, + "required": [ + "name", + "type", + "template", + "recipients" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/EmailPasswordConfig.json b/packages/spec/json-schema/EmailPasswordConfig.json new file mode 100644 index 000000000..9bb0c155b --- /dev/null +++ b/packages/spec/json-schema/EmailPasswordConfig.json @@ -0,0 +1,43 @@ +{ + "$ref": "#/definitions/EmailPasswordConfig", + "definitions": { + "EmailPasswordConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "requireEmailVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before login" + }, + "minPasswordLength": { + "type": "number", + "minimum": 6, + "maximum": 128, + "default": 8, + "description": "Minimum password length" + }, + "requirePasswordComplexity": { + "type": "boolean", + "default": true, + "description": "Require uppercase, lowercase, numbers, symbols" + }, + "allowPasswordReset": { + "type": "boolean", + "default": true, + "description": "Enable password reset functionality" + }, + "passwordResetExpiry": { + "type": "number", + "default": 3600, + "description": "Password reset token expiry in seconds" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/MagicLinkConfig.json b/packages/spec/json-schema/MagicLinkConfig.json new file mode 100644 index 000000000..937680a51 --- /dev/null +++ b/packages/spec/json-schema/MagicLinkConfig.json @@ -0,0 +1,21 @@ +{ + "$ref": "#/definitions/MagicLinkConfig", + "definitions": { + "MagicLinkConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "expiryTime": { + "type": "number", + "default": 900, + "description": "Magic link expiry time in seconds (default 15 min)" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/OAuthProvider.json b/packages/spec/json-schema/OAuthProvider.json new file mode 100644 index 000000000..738d846ad --- /dev/null +++ b/packages/spec/json-schema/OAuthProvider.json @@ -0,0 +1,66 @@ +{ + "$ref": "#/definitions/OAuthProvider", + "definitions": { + "OAuthProvider": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google", + "github", + "facebook", + "twitter", + "linkedin", + "microsoft", + "apple", + "discord", + "gitlab", + "custom" + ], + "description": "OAuth provider type" + }, + "clientId": { + "type": "string", + "description": "OAuth client ID" + }, + "clientSecret": { + "type": "string", + "description": "OAuth client secret (typically from ENV)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Requested OAuth scopes" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "description": "OAuth callback URL" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this provider is enabled" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "provider", + "clientId", + "clientSecret" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/PasskeyConfig.json b/packages/spec/json-schema/PasskeyConfig.json new file mode 100644 index 000000000..335a54344 --- /dev/null +++ b/packages/spec/json-schema/PasskeyConfig.json @@ -0,0 +1,54 @@ +{ + "$ref": "#/definitions/PasskeyConfig", + "definitions": { + "PasskeyConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "rpName": { + "type": "string", + "description": "Relying Party name" + }, + "rpId": { + "type": "string", + "description": "Relying Party ID (defaults to domain)" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Allowed origins for WebAuthn" + }, + "userVerification": { + "type": "string", + "enum": [ + "required", + "preferred", + "discouraged" + ], + "default": "preferred" + }, + "attestation": { + "type": "string", + "enum": [ + "none", + "indirect", + "direct", + "enterprise" + ], + "default": "none" + } + }, + "required": [ + "rpName" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/RateLimit.json b/packages/spec/json-schema/RateLimit.json new file mode 100644 index 000000000..8abd9506b --- /dev/null +++ b/packages/spec/json-schema/RateLimit.json @@ -0,0 +1,26 @@ +{ + "$ref": "#/definitions/RateLimit", + "definitions": { + "RateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "windowMs": { + "type": "number", + "default": 60000, + "description": "Time window in milliseconds" + }, + "maxRequests": { + "type": "number", + "default": 100, + "description": "Max requests per window" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/RateLimitConfig.json b/packages/spec/json-schema/RateLimitConfig.json new file mode 100644 index 000000000..d8620aadb --- /dev/null +++ b/packages/spec/json-schema/RateLimitConfig.json @@ -0,0 +1,36 @@ +{ + "$ref": "#/definitions/RateLimitConfig", + "definitions": { + "RateLimitConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "maxAttempts": { + "type": "number", + "default": 5, + "description": "Maximum login attempts" + }, + "windowMs": { + "type": "number", + "default": 900000, + "description": "Time window in milliseconds (default 15 min)" + }, + "blockDuration": { + "type": "number", + "default": 900000, + "description": "Block duration after max attempts in ms" + }, + "skipSuccessfulRequests": { + "type": "boolean", + "default": false, + "description": "Only count failed requests" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/SessionConfig.json b/packages/spec/json-schema/SessionConfig.json new file mode 100644 index 000000000..a1d9b75bf --- /dev/null +++ b/packages/spec/json-schema/SessionConfig.json @@ -0,0 +1,56 @@ +{ + "$ref": "#/definitions/SessionConfig", + "definitions": { + "SessionConfig": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number", + "default": 604800, + "description": "Session expiry in seconds (default 7 days)" + }, + "updateAge": { + "type": "number", + "default": 86400, + "description": "Session update interval in seconds (default 1 day)" + }, + "cookieName": { + "type": "string", + "default": "session_token", + "description": "Session cookie name" + }, + "cookieSecure": { + "type": "boolean", + "default": true, + "description": "Use secure cookies (HTTPS only)" + }, + "cookieSameSite": { + "type": "string", + "enum": [ + "strict", + "lax", + "none" + ], + "default": "lax", + "description": "SameSite cookie attribute" + }, + "cookieDomain": { + "type": "string", + "description": "Cookie domain" + }, + "cookiePath": { + "type": "string", + "default": "/", + "description": "Cookie path" + }, + "cookieHttpOnly": { + "type": "boolean", + "default": true, + "description": "HttpOnly cookie attribute" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/SessionPolicy.json b/packages/spec/json-schema/SessionPolicy.json new file mode 100644 index 000000000..80ddd1017 --- /dev/null +++ b/packages/spec/json-schema/SessionPolicy.json @@ -0,0 +1,27 @@ +{ + "$ref": "#/definitions/SessionPolicy", + "definitions": { + "SessionPolicy": { + "type": "object", + "properties": { + "idleTimeout": { + "type": "number", + "default": 30, + "description": "Minutes before idle session logout" + }, + "absoluteTimeout": { + "type": "number", + "default": 480, + "description": "Max session duration (minutes)" + }, + "forceMfa": { + "type": "boolean", + "default": false, + "description": "Require 2FA for all users" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/TwoFactorConfig.json b/packages/spec/json-schema/TwoFactorConfig.json new file mode 100644 index 000000000..488c80a3b --- /dev/null +++ b/packages/spec/json-schema/TwoFactorConfig.json @@ -0,0 +1,40 @@ +{ + "$ref": "#/definitions/TwoFactorConfig", + "definitions": { + "TwoFactorConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "description": "TOTP issuer name" + }, + "qrCodeSize": { + "type": "number", + "default": 200, + "description": "QR code size in pixels" + }, + "backupCodes": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "count": { + "type": "number", + "default": 10, + "description": "Number of backup codes to generate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/UserFieldMapping.json b/packages/spec/json-schema/UserFieldMapping.json new file mode 100644 index 000000000..a07a8458c --- /dev/null +++ b/packages/spec/json-schema/UserFieldMapping.json @@ -0,0 +1,47 @@ +{ + "$ref": "#/definitions/UserFieldMapping", + "definitions": { + "UserFieldMapping": { + "type": "object", + "properties": { + "id": { + "type": "string", + "default": "id", + "description": "User ID field" + }, + "email": { + "type": "string", + "default": "email", + "description": "Email field" + }, + "name": { + "type": "string", + "default": "name", + "description": "Name field" + }, + "image": { + "type": "string", + "default": "image", + "description": "Profile image field" + }, + "emailVerified": { + "type": "string", + "default": "email_verified", + "description": "Email verification status field" + }, + "createdAt": { + "type": "string", + "default": "created_at", + "description": "Created timestamp field" + }, + "updatedAt": { + "type": "string", + "default": "updated_at", + "description": "Updated timestamp field" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/src/index.ts b/packages/spec/src/index.ts index 80f8750d1..8e5262749 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -41,6 +41,7 @@ export * from './system/manifest.zod'; export * from './system/datasource.zod'; export * from './system/api.zod'; export * from './system/identity.zod'; +export * from './system/authentication.zod'; export * from './system/policy.zod'; export * from './system/role.zod'; export * from './system/territory.zod'; diff --git a/packages/spec/src/system/authentication.test.ts b/packages/spec/src/system/authentication.test.ts new file mode 100644 index 000000000..a7f66b723 --- /dev/null +++ b/packages/spec/src/system/authentication.test.ts @@ -0,0 +1,752 @@ +import { describe, it, expect } from 'vitest'; +import { + AuthStrategy, + OAuthProviderSchema, + EmailPasswordConfigSchema, + MagicLinkConfigSchema, + PasskeyConfigSchema, + SessionConfigSchema, + RateLimitConfigSchema, + CSRFConfigSchema, + AccountLinkingConfigSchema, + TwoFactorConfigSchema, + UserFieldMappingSchema, + DatabaseAdapterSchema, + AuthPluginConfigSchema, + AuthenticationConfigSchema, + AuthenticationProviderSchema, + type AuthenticationConfig, + type AuthenticationProvider, + type OAuthProvider, +} from './authentication.zod'; + +describe('AuthStrategy', () => { + it('should accept valid authentication strategies', () => { + const strategies = [ + 'email_password', + 'magic_link', + 'oauth', + 'passkey', + 'otp', + 'anonymous', + ]; + + strategies.forEach((strategy) => { + expect(() => AuthStrategy.parse(strategy)).not.toThrow(); + }); + }); + + it('should reject invalid strategies', () => { + expect(() => AuthStrategy.parse('invalid')).toThrow(); + }); +}); + +describe('OAuthProviderSchema', () => { + it('should accept valid OAuth provider configuration', () => { + const provider: OAuthProvider = { + provider: 'google', + clientId: 'test-client-id', + clientSecret: 'test-client-secret', + scopes: ['openid', 'profile', 'email'], + enabled: true, + }; + + expect(() => OAuthProviderSchema.parse(provider)).not.toThrow(); + }); + + it('should accept minimal OAuth provider configuration', () => { + const provider = { + provider: 'github', + clientId: 'test-client-id', + clientSecret: 'test-client-secret', + }; + + const result = OAuthProviderSchema.parse(provider); + expect(result.enabled).toBe(true); // default value + }); + + it('should accept all supported OAuth providers', () => { + const providers = [ + 'google', + 'github', + 'facebook', + 'twitter', + 'linkedin', + 'microsoft', + 'apple', + 'discord', + 'gitlab', + 'custom', + ]; + + providers.forEach((provider) => { + const config = { + provider, + clientId: 'test-id', + clientSecret: 'test-secret', + }; + expect(() => OAuthProviderSchema.parse(config)).not.toThrow(); + }); + }); + + it('should validate redirect URI is a valid URL', () => { + const provider = { + provider: 'google', + clientId: 'test-id', + clientSecret: 'test-secret', + redirectUri: 'not-a-url', + }; + + expect(() => OAuthProviderSchema.parse(provider)).toThrow(); + }); +}); + +describe('EmailPasswordConfigSchema', () => { + it('should accept valid email password configuration', () => { + const config = { + enabled: true, + requireEmailVerification: true, + minPasswordLength: 12, + requirePasswordComplexity: true, + allowPasswordReset: true, + passwordResetExpiry: 3600, + }; + + expect(() => EmailPasswordConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = EmailPasswordConfigSchema.parse(config); + + expect(result.enabled).toBe(true); + expect(result.minPasswordLength).toBe(8); + expect(result.requireEmailVerification).toBe(true); + }); + + it('should enforce password length constraints', () => { + const tooShort = { minPasswordLength: 5 }; + expect(() => EmailPasswordConfigSchema.parse(tooShort)).toThrow(); + + const tooLong = { minPasswordLength: 200 }; + expect(() => EmailPasswordConfigSchema.parse(tooLong)).toThrow(); + + const justRight = { minPasswordLength: 10 }; + expect(() => EmailPasswordConfigSchema.parse(justRight)).not.toThrow(); + }); +}); + +describe('MagicLinkConfigSchema', () => { + it('should accept valid magic link configuration', () => { + const config = { + enabled: true, + expiryTime: 1800, // 30 minutes + }; + + expect(() => MagicLinkConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default expiry time', () => { + const config = { enabled: true }; + const result = MagicLinkConfigSchema.parse(config); + + expect(result.expiryTime).toBe(900); // 15 minutes default + }); +}); + +describe('PasskeyConfigSchema', () => { + it('should accept valid passkey configuration', () => { + const config = { + enabled: true, + rpName: 'ObjectStack', + rpId: 'objectstack.com', + userVerification: 'required' as const, + attestation: 'direct' as const, + }; + + expect(() => PasskeyConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = { + rpName: 'Test App', + }; + const result = PasskeyConfigSchema.parse(config); + + expect(result.enabled).toBe(false); // disabled by default + expect(result.userVerification).toBe('preferred'); + expect(result.attestation).toBe('none'); + }); + + it('should validate allowed origins are URLs', () => { + const config = { + rpName: 'Test', + allowedOrigins: ['https://example.com', 'https://app.example.com'], + }; + + expect(() => PasskeyConfigSchema.parse(config)).not.toThrow(); + }); +}); + +describe('SessionConfigSchema', () => { + it('should accept valid session configuration', () => { + const config = { + expiresIn: 604800, // 7 days + updateAge: 86400, // 1 day + cookieName: 'my-session', + cookieSecure: true, + cookieSameSite: 'strict' as const, + cookieHttpOnly: true, + }; + + expect(() => SessionConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = SessionConfigSchema.parse(config); + + expect(result.expiresIn).toBe(86400 * 7); // 7 days + expect(result.cookieName).toBe('session_token'); + expect(result.cookieSecure).toBe(true); + expect(result.cookieSameSite).toBe('lax'); + }); + + it('should accept all SameSite options', () => { + const options = ['strict', 'lax', 'none'] as const; + + options.forEach((sameSite) => { + const config = { cookieSameSite: sameSite }; + expect(() => SessionConfigSchema.parse(config)).not.toThrow(); + }); + }); +}); + +describe('RateLimitConfigSchema', () => { + it('should accept valid rate limit configuration', () => { + const config = { + enabled: true, + maxAttempts: 10, + windowMs: 1800000, // 30 minutes + blockDuration: 3600000, // 1 hour + skipSuccessfulRequests: true, + }; + + expect(() => RateLimitConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = RateLimitConfigSchema.parse(config); + + expect(result.enabled).toBe(true); + expect(result.maxAttempts).toBe(5); + expect(result.windowMs).toBe(900000); // 15 minutes + }); +}); + +describe('CSRFConfigSchema', () => { + it('should accept valid CSRF configuration', () => { + const config = { + enabled: true, + tokenLength: 64, + cookieName: 'csrf-token', + headerName: 'X-CSRF-Token', + }; + + expect(() => CSRFConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = CSRFConfigSchema.parse(config); + + expect(result.enabled).toBe(true); + expect(result.tokenLength).toBe(32); + expect(result.cookieName).toBe('csrf_token'); + }); +}); + +describe('AccountLinkingConfigSchema', () => { + it('should accept valid account linking configuration', () => { + const config = { + enabled: true, + autoLink: true, + requireVerification: false, + }; + + expect(() => AccountLinkingConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = AccountLinkingConfigSchema.parse(config); + + expect(result.enabled).toBe(true); + expect(result.autoLink).toBe(false); + expect(result.requireVerification).toBe(true); + }); +}); + +describe('TwoFactorConfigSchema', () => { + it('should accept valid 2FA configuration', () => { + const config = { + enabled: true, + issuer: 'ObjectStack', + qrCodeSize: 256, + backupCodes: { + enabled: true, + count: 8, + }, + }; + + expect(() => TwoFactorConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = { enabled: false }; + const result = TwoFactorConfigSchema.parse(config); + + expect(result.qrCodeSize).toBe(200); + }); +}); + +describe('UserFieldMappingSchema', () => { + it('should accept custom field mappings', () => { + const config = { + id: 'user_id', + email: 'user_email', + name: 'full_name', + emailVerified: 'is_email_verified', + }; + + expect(() => UserFieldMappingSchema.parse(config)).not.toThrow(); + }); + + it('should use default field names', () => { + const config = {}; + const result = UserFieldMappingSchema.parse(config); + + expect(result.id).toBe('id'); + expect(result.email).toBe('email'); + expect(result.name).toBe('name'); + expect(result.emailVerified).toBe('email_verified'); + }); +}); + +describe('DatabaseAdapterSchema', () => { + it('should accept valid database adapter configuration', () => { + const config = { + type: 'prisma' as const, + connectionString: 'postgresql://localhost:5432/db', + tablePrefix: 'auth_', + schema: 'public', + }; + + expect(() => DatabaseAdapterSchema.parse(config)).not.toThrow(); + }); + + it('should accept all supported adapter types', () => { + const types = ['prisma', 'drizzle', 'kysely', 'custom'] as const; + + types.forEach((type) => { + const config = { type }; + expect(() => DatabaseAdapterSchema.parse(config)).not.toThrow(); + }); + }); + + it('should use default table prefix', () => { + const config = { type: 'drizzle' as const }; + const result = DatabaseAdapterSchema.parse(config); + + expect(result.tablePrefix).toBe('auth_'); + }); +}); + +describe('AuthPluginConfigSchema', () => { + it('should accept valid plugin configuration', () => { + const config = { + name: 'organization', + enabled: true, + options: { + maxOrganizations: 5, + allowInvites: true, + }, + }; + + expect(() => AuthPluginConfigSchema.parse(config)).not.toThrow(); + }); +}); + +describe('AuthenticationConfigSchema', () => { + it('should accept minimal valid configuration', () => { + const config: AuthenticationConfig = { + name: 'main_auth', + label: 'Main Authentication', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), // 32 character secret + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => AuthenticationConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept comprehensive configuration', () => { + const config: AuthenticationConfig = { + name: 'main_auth', + label: 'Main Authentication', + strategies: ['email_password', 'oauth', 'magic_link'], + baseUrl: 'https://app.example.com', + secret: 'super-secret-key-with-at-least-32-characters', + + emailPassword: { + enabled: true, + requireEmailVerification: true, + minPasswordLength: 10, + requirePasswordComplexity: true, + allowPasswordReset: true, + passwordResetExpiry: 7200, + }, + + magicLink: { + enabled: true, + expiryTime: 900, + }, + + oauth: { + providers: [ + { + provider: 'google', + clientId: 'google-client-id', + clientSecret: 'google-client-secret', + scopes: ['openid', 'profile', 'email'], + enabled: true, + }, + { + provider: 'github', + clientId: 'github-client-id', + clientSecret: 'github-client-secret', + enabled: true, + }, + ], + }, + + session: { + expiresIn: 604800, + cookieName: 'app-session', + cookieSecure: true, + cookieSameSite: 'lax', + }, + + rateLimit: { + enabled: true, + maxAttempts: 5, + windowMs: 900000, + }, + + csrf: { + enabled: true, + tokenLength: 32, + }, + + accountLinking: { + enabled: true, + autoLink: false, + requireVerification: true, + }, + + twoFactor: { + enabled: true, + issuer: 'My App', + }, + + userFieldMapping: {}, + + database: { + type: 'prisma', + tablePrefix: 'auth_', + }, + + plugins: [], + + active: true, + allowRegistration: true, + }; + + expect(() => AuthenticationConfigSchema.parse(config)).not.toThrow(); + }); + + it('should enforce snake_case for name field', () => { + const invalidConfig = { + name: 'mainAuth', // camelCase - invalid + label: 'Main Auth', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => AuthenticationConfigSchema.parse(invalidConfig)).toThrow(); + + const validConfig = { + ...invalidConfig, + name: 'main_auth', // snake_case - valid + }; + + expect(() => AuthenticationConfigSchema.parse(validConfig)).not.toThrow(); + }); + + it('should require at least one strategy', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: [], // empty - invalid + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => AuthenticationConfigSchema.parse(config)).toThrow(); + }); + + it('should require secret to be at least 32 characters', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'short', // too short + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => AuthenticationConfigSchema.parse(config)).toThrow(); + }); + + it('should validate baseUrl is a valid URL', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'not-a-url', // invalid URL + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => AuthenticationConfigSchema.parse(config)).toThrow(); + }); + + it('should accept configuration with hooks', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + hooks: { + beforeSignIn: async ({ email }: { email: string }) => { + console.log('Before sign in:', email); + }, + afterSignIn: async ({ user, session }: { user: any; session: any }) => { + console.log('After sign in:', user.id); + }, + }, + }; + + expect(() => AuthenticationConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept configuration with security settings', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + security: { + allowedOrigins: ['https://app.example.com'], + trustProxy: true, + ipRateLimiting: true, + sessionFingerprinting: true, + maxSessions: 3, + }, + }; + + expect(() => AuthenticationConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept configuration with email settings', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + email: { + from: 'noreply@example.com', + fromName: 'My App', + provider: 'sendgrid' as const, + config: { + apiKey: 'sendgrid-api-key', + }, + }, + }; + + expect(() => AuthenticationConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept configuration with UI customization', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + ui: { + brandName: 'My Brand', + logo: 'https://example.com/logo.png', + primaryColor: '#007bff', + customCss: '.button { color: blue; }', + }, + }; + + expect(() => AuthenticationConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values for optional fields', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + }; + + const result = AuthenticationConfigSchema.parse(config); + + expect(result.active).toBe(true); + expect(result.allowRegistration).toBe(true); + expect(result.plugins).toEqual([]); + }); +}); + +describe('AuthenticationProviderSchema', () => { + it('should accept valid authentication provider', () => { + const provider: AuthenticationProvider = { + type: 'authentication', + config: { + name: 'main_auth', + label: 'Main Auth', + strategies: ['email_password', 'oauth'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + oauth: { + providers: [ + { + provider: 'google', + clientId: 'test-id', + clientSecret: 'test-secret', + }, + ], + }, + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }, + }; + + expect(() => AuthenticationProviderSchema.parse(provider)).not.toThrow(); + }); + + it('should require type to be "authentication"', () => { + const provider = { + type: 'other_auth', // invalid + config: { + name: 'test', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }, + }; + + expect(() => AuthenticationProviderSchema.parse(provider)).toThrow(); + }); +}); + +describe('Type inference', () => { + it('should correctly infer AuthenticationConfig type', () => { + const config: AuthenticationConfig = { + name: 'test_auth', + label: 'Test Auth', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + // This test passes if TypeScript compiles without errors + expect(config.name).toBe('test_auth'); + expect(config.strategies).toContain('email_password'); + }); + + it('should correctly infer AuthenticationProvider type', () => { + const provider: AuthenticationProvider = { + type: 'authentication', + config: { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }, + }; + + // This test passes if TypeScript compiles without errors + expect(provider.type).toBe('authentication'); + expect(provider.config.name).toBe('test_auth'); + }); +}); diff --git a/packages/spec/src/system/authentication.zod.ts b/packages/spec/src/system/authentication.zod.ts new file mode 100644 index 000000000..9aa407f64 --- /dev/null +++ b/packages/spec/src/system/authentication.zod.ts @@ -0,0 +1,484 @@ +import { z } from 'zod'; + +/** + * Authentication Protocol + * + * Defines the standard authentication specification for the ObjectStack ecosystem. + * This protocol supports multiple authentication strategies, session management, + * and comprehensive security features. + * + * This is a framework-agnostic specification that can be implemented with any + * authentication library (better-auth, Auth.js, Passport, etc.) + */ + +/** + * Supported authentication strategies + */ +export const AuthStrategy = z.enum([ + 'email_password', // Traditional email & password authentication + 'magic_link', // Passwordless email magic link + 'oauth', // OAuth2 providers (Google, GitHub, etc.) + 'passkey', // WebAuthn / FIDO2 passkeys + 'otp', // One-time password (SMS, Email) + 'anonymous', // Anonymous/guest sessions +]); + +export type AuthStrategy = z.infer; + +/** + * OAuth Provider Configuration + * Supports popular OAuth2 providers + */ +export const OAuthProviderSchema = z.object({ + provider: z.enum([ + 'google', + 'github', + 'facebook', + 'twitter', + 'linkedin', + 'microsoft', + 'apple', + 'discord', + 'gitlab', + 'custom', + ]).describe('OAuth provider type'), + + clientId: z.string().describe('OAuth client ID'), + clientSecret: z.string().describe('OAuth client secret (typically from ENV)'), + + scopes: z.array(z.string()).optional().describe('Requested OAuth scopes'), + + redirectUri: z.string().url().optional().describe('OAuth callback URL'), + + enabled: z.boolean().default(true).describe('Whether this provider is enabled'), + + displayName: z.string().optional().describe('Display name for the provider button'), + + icon: z.string().optional().describe('Icon URL or identifier'), +}); + +export type OAuthProvider = z.infer; + +/** + * Email & Password Strategy Configuration + */ +export const EmailPasswordConfigSchema = z.object({ + enabled: z.boolean().default(true), + + requireEmailVerification: z.boolean().default(true).describe('Require email verification before login'), + + minPasswordLength: z.number().min(6).max(128).default(8).describe('Minimum password length'), + + requirePasswordComplexity: z.boolean().default(true).describe('Require uppercase, lowercase, numbers, symbols'), + + allowPasswordReset: z.boolean().default(true).describe('Enable password reset functionality'), + + passwordResetExpiry: z.number().default(3600).describe('Password reset token expiry in seconds'), +}); + +export type EmailPasswordConfig = z.infer; + +/** + * Magic Link Strategy Configuration + */ +export const MagicLinkConfigSchema = z.object({ + enabled: z.boolean().default(true), + + expiryTime: z.number().default(900).describe('Magic link expiry time in seconds (default 15 min)'), + + sendEmail: z.function() + .args(z.object({ + to: z.string().email(), + link: z.string().url(), + token: z.string(), + })) + .returns(z.promise(z.void())) + .optional() + .describe('Custom email sending function'), +}); + +export type MagicLinkConfig = z.infer; + +/** + * Passkey (WebAuthn) Strategy Configuration + */ +export const PasskeyConfigSchema = z.object({ + enabled: z.boolean().default(false), + + rpName: z.string().describe('Relying Party name'), + + rpId: z.string().optional().describe('Relying Party ID (defaults to domain)'), + + allowedOrigins: z.array(z.string().url()).optional().describe('Allowed origins for WebAuthn'), + + userVerification: z.enum(['required', 'preferred', 'discouraged']).default('preferred'), + + attestation: z.enum(['none', 'indirect', 'direct', 'enterprise']).default('none'), +}); + +export type PasskeyConfig = z.infer; + +/** + * Session Configuration + */ +export const SessionConfigSchema = z.object({ + expiresIn: z.number().default(86400 * 7).describe('Session expiry in seconds (default 7 days)'), + + updateAge: z.number().default(86400).describe('Session update interval in seconds (default 1 day)'), + + cookieName: z.string().default('session_token').describe('Session cookie name'), + + cookieSecure: z.boolean().default(true).describe('Use secure cookies (HTTPS only)'), + + cookieSameSite: z.enum(['strict', 'lax', 'none']).default('lax').describe('SameSite cookie attribute'), + + cookieDomain: z.string().optional().describe('Cookie domain'), + + cookiePath: z.string().default('/').describe('Cookie path'), + + cookieHttpOnly: z.boolean().default(true).describe('HttpOnly cookie attribute'), +}); + +export type SessionConfig = z.infer; + +/** + * Rate Limiting Configuration + */ +export const RateLimitConfigSchema = z.object({ + enabled: z.boolean().default(true), + + maxAttempts: z.number().default(5).describe('Maximum login attempts'), + + windowMs: z.number().default(900000).describe('Time window in milliseconds (default 15 min)'), + + blockDuration: z.number().default(900000).describe('Block duration after max attempts in ms'), + + skipSuccessfulRequests: z.boolean().default(false).describe('Only count failed requests'), +}); + +export type RateLimitConfig = z.infer; + +/** + * CSRF Protection Configuration + */ +export const CSRFConfigSchema = z.object({ + enabled: z.boolean().default(true), + + tokenLength: z.number().default(32).describe('CSRF token length'), + + cookieName: z.string().default('csrf_token').describe('CSRF cookie name'), + + headerName: z.string().default('X-CSRF-Token').describe('CSRF header name'), +}); + +export type CSRFConfig = z.infer; + +/** + * Account Linking Configuration + * Allows linking multiple auth methods to a single user account + */ +export const AccountLinkingConfigSchema = z.object({ + enabled: z.boolean().default(true).describe('Allow account linking'), + + autoLink: z.boolean().default(false).describe('Automatically link accounts with same email'), + + requireVerification: z.boolean().default(true).describe('Require email verification before linking'), +}); + +export type AccountLinkingConfig = z.infer; + +/** + * Two-Factor Authentication (2FA) Configuration + */ +export const TwoFactorConfigSchema = z.object({ + enabled: z.boolean().default(false), + + issuer: z.string().optional().describe('TOTP issuer name'), + + qrCodeSize: z.number().default(200).describe('QR code size in pixels'), + + backupCodes: z.object({ + enabled: z.boolean().default(true), + count: z.number().default(10).describe('Number of backup codes to generate'), + }).optional(), +}); + +export type TwoFactorConfig = z.infer; + +/** + * User Field Mapping Configuration + * Maps authentication user fields to ObjectStack user object fields + */ +export const UserFieldMappingSchema = z.object({ + id: z.string().default('id').describe('User ID field'), + email: z.string().default('email').describe('Email field'), + name: z.string().default('name').describe('Name field'), + image: z.string().default('image').optional().describe('Profile image field'), + emailVerified: z.string().default('email_verified').describe('Email verification status field'), + createdAt: z.string().default('created_at').describe('Created timestamp field'), + updatedAt: z.string().default('updated_at').describe('Updated timestamp field'), +}); + +export type UserFieldMapping = z.infer; + +/** + * Database Adapter Configuration + */ +export const DatabaseAdapterSchema = z.object({ + type: z.enum(['prisma', 'drizzle', 'kysely', 'custom']).describe('Database adapter type'), + + connectionString: z.string().optional().describe('Database connection string'), + + tablePrefix: z.string().default('auth_').describe('Prefix for auth tables'), + + schema: z.string().optional().describe('Database schema name'), +}); + +export type DatabaseAdapter = z.infer; + +/** + * Authentication Plugin Configuration + * Extends authentication with additional features + */ +export const AuthPluginConfigSchema = z.object({ + name: z.string().describe('Plugin name'), + + enabled: z.boolean().default(true), + + options: z.record(z.any()).optional().describe('Plugin-specific options'), +}); + +export type AuthPluginConfig = z.infer; + +/** + * Complete Authentication Configuration Schema + * + * This is the main configuration object for authentication + * in an ObjectStack application. + * + * @example + * ```typescript + * const authConfig: AuthenticationConfig = { + * name: 'main_auth', + * label: 'Main Authentication', + * strategies: ['email_password', 'oauth'], + * baseUrl: 'https://app.example.com', + * secret: process.env.AUTH_SECRET, + * emailPassword: { + * enabled: true, + * minPasswordLength: 8, + * }, + * oauth: { + * providers: [{ + * provider: 'google', + * clientId: process.env.GOOGLE_CLIENT_ID, + * clientSecret: process.env.GOOGLE_CLIENT_SECRET, + * }], + * }, + * session: { + * expiresIn: 604800, // 7 days + * }, + * }; + * ``` + */ +export const AuthenticationConfigSchema = z.object({ + /** + * Unique identifier for this auth configuration + * Must be in snake_case following ObjectStack conventions + */ + name: z.string() + .regex(/^[a-z_][a-z0-9_]*$/) + .describe('Configuration name (snake_case)'), + + /** + * Human-readable label + */ + label: z.string().describe('Display label'), + + /** + * Enabled authentication strategies + */ + strategies: z.array(AuthStrategy).min(1).describe('Enabled authentication strategies'), + + /** + * Base URL for the application + */ + baseUrl: z.string().url().describe('Application base URL'), + + /** + * Secret key for signing tokens and cookies + * Should be loaded from environment variables + */ + secret: z.string().min(32).describe('Secret key for signing (min 32 chars)'), + + /** + * Email & Password configuration + */ + emailPassword: EmailPasswordConfigSchema.optional(), + + /** + * Magic Link configuration + */ + magicLink: MagicLinkConfigSchema.optional(), + + /** + * Passkey (WebAuthn) configuration + */ + passkey: PasskeyConfigSchema.optional(), + + /** + * OAuth configuration + */ + oauth: z.object({ + providers: z.array(OAuthProviderSchema).min(1), + }).optional(), + + /** + * Session configuration + */ + session: SessionConfigSchema.default({}), + + /** + * Rate limiting configuration + */ + rateLimit: RateLimitConfigSchema.default({}), + + /** + * CSRF protection configuration + */ + csrf: CSRFConfigSchema.default({}), + + /** + * Account linking configuration + */ + accountLinking: AccountLinkingConfigSchema.default({}), + + /** + * Two-factor authentication configuration + */ + twoFactor: TwoFactorConfigSchema.optional(), + + /** + * User field mapping + */ + userFieldMapping: UserFieldMappingSchema.default({}), + + /** + * Database adapter configuration + */ + database: DatabaseAdapterSchema.optional(), + + /** + * Additional authentication plugins + */ + plugins: z.array(AuthPluginConfigSchema).default([]), + + /** + * Custom hooks for authentication events + */ + hooks: z.object({ + beforeSignIn: z.function() + .args(z.object({ email: z.string() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called before user sign in'), + + afterSignIn: z.function() + .args(z.object({ user: z.any(), session: z.any() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called after user sign in'), + + beforeSignUp: z.function() + .args(z.object({ email: z.string(), name: z.string().optional() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called before user registration'), + + afterSignUp: z.function() + .args(z.object({ user: z.any() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called after user registration'), + + beforeSignOut: z.function() + .args(z.object({ sessionId: z.string() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called before user sign out'), + + afterSignOut: z.function() + .args(z.object({ sessionId: z.string() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called after user sign out'), + }).optional().describe('Authentication lifecycle hooks'), + + /** + * Advanced security settings + */ + security: z.object({ + allowedOrigins: z.array(z.string()).optional().describe('CORS allowed origins'), + + trustProxy: z.boolean().default(false).describe('Trust proxy headers'), + + ipRateLimiting: z.boolean().default(true).describe('Enable IP-based rate limiting'), + + sessionFingerprinting: z.boolean().default(true).describe('Enable session fingerprinting'), + + maxSessions: z.number().default(5).describe('Maximum concurrent sessions per user'), + }).optional().describe('Advanced security settings'), + + /** + * Email configuration for transactional emails + */ + email: z.object({ + from: z.string().email().describe('From email address'), + + fromName: z.string().optional().describe('From name'), + + provider: z.enum(['smtp', 'sendgrid', 'mailgun', 'ses', 'resend', 'custom']).describe('Email provider'), + + config: z.record(z.any()).optional().describe('Provider-specific configuration'), + }).optional().describe('Email configuration'), + + /** + * UI customization options + */ + ui: z.object({ + brandName: z.string().optional().describe('Brand name displayed in auth UI'), + + logo: z.string().optional().describe('Logo URL'), + + primaryColor: z.string().optional().describe('Primary brand color (hex)'), + + customCss: z.string().optional().describe('Custom CSS for auth pages'), + }).optional().describe('UI customization'), + + /** + * Whether this auth provider is active + */ + active: z.boolean().default(true).describe('Whether this provider is active'), + + /** + * Whether to allow new user registration + */ + allowRegistration: z.boolean().default(true).describe('Allow new user registration'), +}); + +/** + * TypeScript type inferred from AuthenticationConfigSchema + */ +export type AuthenticationConfig = z.infer; + +/** + * Authentication Provider Schema + * Wraps the configuration for use in the identity system + */ +export const AuthenticationProviderSchema = z.object({ + type: z.literal('authentication').describe('Provider type identifier'), + + config: AuthenticationConfigSchema.describe('Authentication configuration'), +}); + +export type AuthenticationProvider = z.infer; From ad7a87be488637a5888e1f62b20598ec9ece29d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 01:07:51 +0000 Subject: [PATCH 6/7] Refactor: Rename to auth.zod.ts with shorter AuthConfig names and add driver field Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../references/misc/AuthenticationConfig.mdx | 32 + .../misc/AuthenticationProvider.mdx | 11 + content/docs/references/system/AuthConfig.mdx | 33 + .../system/StandardAuthProvider.mdx | 11 + ...TICATION.md => AUTHENTICATION_STANDARD.md} | 21 +- packages/spec/json-schema/AuthConfig.json | 606 +++++++++++++++++ .../json-schema/StandardAuthProvider.json | 622 ++++++++++++++++++ packages/spec/src/index.ts | 2 +- .../{authentication.test.ts => auth.test.ts} | 54 +- .../{authentication.zod.ts => auth.zod.ts} | 26 +- 10 files changed, 1375 insertions(+), 43 deletions(-) create mode 100644 content/docs/references/misc/AuthenticationConfig.mdx create mode 100644 content/docs/references/misc/AuthenticationProvider.mdx create mode 100644 content/docs/references/system/AuthConfig.mdx create mode 100644 content/docs/references/system/StandardAuthProvider.mdx rename docs/{AUTHENTICATION.md => AUTHENTICATION_STANDARD.md} (91%) create mode 100644 packages/spec/json-schema/AuthConfig.json create mode 100644 packages/spec/json-schema/StandardAuthProvider.json rename packages/spec/src/system/{authentication.test.ts => auth.test.ts} (92%) rename packages/spec/src/system/{authentication.zod.ts => auth.zod.ts} (94%) diff --git a/content/docs/references/misc/AuthenticationConfig.mdx b/content/docs/references/misc/AuthenticationConfig.mdx new file mode 100644 index 000000000..eb7e2f280 --- /dev/null +++ b/content/docs/references/misc/AuthenticationConfig.mdx @@ -0,0 +1,32 @@ +--- +title: AuthenticationConfig +description: AuthenticationConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Configuration name (snake_case) | +| **label** | `string` | ✅ | Display label | +| **strategies** | `Enum<'email_password' \| 'magic_link' \| 'oauth' \| 'passkey' \| 'otp' \| 'anonymous'>[]` | ✅ | Enabled authentication strategies | +| **baseUrl** | `string` | ✅ | Application base URL | +| **secret** | `string` | ✅ | Secret key for signing (min 32 chars) | +| **emailPassword** | `object` | optional | | +| **magicLink** | `object` | optional | | +| **passkey** | `object` | optional | | +| **oauth** | `object` | optional | | +| **session** | `object` | optional | | +| **rateLimit** | `object` | optional | | +| **csrf** | `object` | optional | | +| **accountLinking** | `object` | optional | | +| **twoFactor** | `object` | optional | | +| **userFieldMapping** | `object` | optional | | +| **database** | `object` | optional | | +| **plugins** | `object[]` | optional | | +| **hooks** | `object` | optional | Authentication lifecycle hooks | +| **security** | `object` | optional | Advanced security settings | +| **email** | `object` | optional | Email configuration | +| **ui** | `object` | optional | UI customization | +| **active** | `boolean` | optional | Whether this provider is active | +| **allowRegistration** | `boolean` | optional | Allow new user registration | diff --git a/content/docs/references/misc/AuthenticationProvider.mdx b/content/docs/references/misc/AuthenticationProvider.mdx new file mode 100644 index 000000000..e5cc9803b --- /dev/null +++ b/content/docs/references/misc/AuthenticationProvider.mdx @@ -0,0 +1,11 @@ +--- +title: AuthenticationProvider +description: AuthenticationProvider Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Provider type identifier | +| **config** | `object` | ✅ | Authentication configuration | diff --git a/content/docs/references/system/AuthConfig.mdx b/content/docs/references/system/AuthConfig.mdx new file mode 100644 index 000000000..1f7c710da --- /dev/null +++ b/content/docs/references/system/AuthConfig.mdx @@ -0,0 +1,33 @@ +--- +title: AuthConfig +description: AuthConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Configuration name (snake_case) | +| **label** | `string` | ✅ | Display label | +| **driver** | `string` | optional | The underlying authentication implementation driver | +| **strategies** | `Enum<'email_password' \| 'magic_link' \| 'oauth' \| 'passkey' \| 'otp' \| 'anonymous'>[]` | ✅ | Enabled authentication strategies | +| **baseUrl** | `string` | ✅ | Application base URL | +| **secret** | `string` | ✅ | Secret key for signing (min 32 chars) | +| **emailPassword** | `object` | optional | | +| **magicLink** | `object` | optional | | +| **passkey** | `object` | optional | | +| **oauth** | `object` | optional | | +| **session** | `object` | optional | | +| **rateLimit** | `object` | optional | | +| **csrf** | `object` | optional | | +| **accountLinking** | `object` | optional | | +| **twoFactor** | `object` | optional | | +| **userFieldMapping** | `object` | optional | | +| **database** | `object` | optional | | +| **plugins** | `object[]` | optional | | +| **hooks** | `object` | optional | Authentication lifecycle hooks | +| **security** | `object` | optional | Advanced security settings | +| **email** | `object` | optional | Email configuration | +| **ui** | `object` | optional | UI customization | +| **active** | `boolean` | optional | Whether this provider is active | +| **allowRegistration** | `boolean` | optional | Allow new user registration | diff --git a/content/docs/references/system/StandardAuthProvider.mdx b/content/docs/references/system/StandardAuthProvider.mdx new file mode 100644 index 000000000..fdc46136f --- /dev/null +++ b/content/docs/references/system/StandardAuthProvider.mdx @@ -0,0 +1,11 @@ +--- +title: StandardAuthProvider +description: StandardAuthProvider Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Provider type identifier | +| **config** | `object` | ✅ | Standard authentication configuration | diff --git a/docs/AUTHENTICATION.md b/docs/AUTHENTICATION_STANDARD.md similarity index 91% rename from docs/AUTHENTICATION.md rename to docs/AUTHENTICATION_STANDARD.md index 756f900e5..41974da7d 100644 --- a/docs/AUTHENTICATION.md +++ b/docs/AUTHENTICATION_STANDARD.md @@ -1,12 +1,20 @@ -# Authentication Protocol +# ObjectStack Authentication Standard -A comprehensive authentication specification for the ObjectStack ecosystem. +The standard authentication protocol specification for the ObjectStack ecosystem. ## Overview -This specification defines the standard authentication protocol for ObjectStack applications. It supports multiple authentication strategies, session management, and comprehensive security features. +This document defines the **ObjectStack Authentication Standard**, a comprehensive, framework-agnostic authentication protocol for ObjectStack applications. This standard supports multiple authentication strategies, session management, and comprehensive security features. -The specification is framework-agnostic and can be implemented with any authentication library (better-auth, Auth.js, Passport, etc.) +The specification is designed as an **interface** that can be implemented by any authentication library. **better-auth** serves as the **Reference Implementation** (default driver) for this standard. + +### Implementation Drivers + +The authentication standard can be implemented using various drivers: +- **better-auth** (default/reference implementation) +- Auth.js +- Passport +- Custom implementations ## Features @@ -54,11 +62,12 @@ pnpm add @objectstack/plugin-better-auth ### Basic Example ```typescript -import type { AuthenticationConfig } from '@objectstack/spec'; +import type { AuthConfig } from '@objectstack/spec'; -const authConfig: AuthenticationConfig = { +const authConfig: AuthConfig = { name: 'main_auth', label: 'Main Authentication', + driver: 'better-auth', // Optional, defaults to 'better-auth' strategies: ['email_password'], baseUrl: 'https://app.example.com', secret: process.env.AUTH_SECRET!, diff --git a/packages/spec/json-schema/AuthConfig.json b/packages/spec/json-schema/AuthConfig.json new file mode 100644 index 000000000..cc76540b5 --- /dev/null +++ b/packages/spec/json-schema/AuthConfig.json @@ -0,0 +1,606 @@ +{ + "$ref": "#/definitions/AuthConfig", + "definitions": { + "AuthConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Configuration name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label" + }, + "driver": { + "type": "string", + "default": "better-auth", + "description": "The underlying authentication implementation driver" + }, + "strategies": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email_password", + "magic_link", + "oauth", + "passkey", + "otp", + "anonymous" + ] + }, + "minItems": 1, + "description": "Enabled authentication strategies" + }, + "baseUrl": { + "type": "string", + "format": "uri", + "description": "Application base URL" + }, + "secret": { + "type": "string", + "minLength": 32, + "description": "Secret key for signing (min 32 chars)" + }, + "emailPassword": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "requireEmailVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before login" + }, + "minPasswordLength": { + "type": "number", + "minimum": 6, + "maximum": 128, + "default": 8, + "description": "Minimum password length" + }, + "requirePasswordComplexity": { + "type": "boolean", + "default": true, + "description": "Require uppercase, lowercase, numbers, symbols" + }, + "allowPasswordReset": { + "type": "boolean", + "default": true, + "description": "Enable password reset functionality" + }, + "passwordResetExpiry": { + "type": "number", + "default": 3600, + "description": "Password reset token expiry in seconds" + } + }, + "additionalProperties": false + }, + "magicLink": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "expiryTime": { + "type": "number", + "default": 900, + "description": "Magic link expiry time in seconds (default 15 min)" + } + }, + "additionalProperties": false + }, + "passkey": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "rpName": { + "type": "string", + "description": "Relying Party name" + }, + "rpId": { + "type": "string", + "description": "Relying Party ID (defaults to domain)" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Allowed origins for WebAuthn" + }, + "userVerification": { + "type": "string", + "enum": [ + "required", + "preferred", + "discouraged" + ], + "default": "preferred" + }, + "attestation": { + "type": "string", + "enum": [ + "none", + "indirect", + "direct", + "enterprise" + ], + "default": "none" + } + }, + "required": [ + "rpName" + ], + "additionalProperties": false + }, + "oauth": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google", + "github", + "facebook", + "twitter", + "linkedin", + "microsoft", + "apple", + "discord", + "gitlab", + "custom" + ], + "description": "OAuth provider type" + }, + "clientId": { + "type": "string", + "description": "OAuth client ID" + }, + "clientSecret": { + "type": "string", + "description": "OAuth client secret (typically from ENV)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Requested OAuth scopes" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "description": "OAuth callback URL" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this provider is enabled" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "provider", + "clientId", + "clientSecret" + ], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": [ + "providers" + ], + "additionalProperties": false + }, + "session": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number", + "default": 604800, + "description": "Session expiry in seconds (default 7 days)" + }, + "updateAge": { + "type": "number", + "default": 86400, + "description": "Session update interval in seconds (default 1 day)" + }, + "cookieName": { + "type": "string", + "default": "session_token", + "description": "Session cookie name" + }, + "cookieSecure": { + "type": "boolean", + "default": true, + "description": "Use secure cookies (HTTPS only)" + }, + "cookieSameSite": { + "type": "string", + "enum": [ + "strict", + "lax", + "none" + ], + "default": "lax", + "description": "SameSite cookie attribute" + }, + "cookieDomain": { + "type": "string", + "description": "Cookie domain" + }, + "cookiePath": { + "type": "string", + "default": "/", + "description": "Cookie path" + }, + "cookieHttpOnly": { + "type": "boolean", + "default": true, + "description": "HttpOnly cookie attribute" + } + }, + "additionalProperties": false, + "default": {} + }, + "rateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "maxAttempts": { + "type": "number", + "default": 5, + "description": "Maximum login attempts" + }, + "windowMs": { + "type": "number", + "default": 900000, + "description": "Time window in milliseconds (default 15 min)" + }, + "blockDuration": { + "type": "number", + "default": 900000, + "description": "Block duration after max attempts in ms" + }, + "skipSuccessfulRequests": { + "type": "boolean", + "default": false, + "description": "Only count failed requests" + } + }, + "additionalProperties": false, + "default": {} + }, + "csrf": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "tokenLength": { + "type": "number", + "default": 32, + "description": "CSRF token length" + }, + "cookieName": { + "type": "string", + "default": "csrf_token", + "description": "CSRF cookie name" + }, + "headerName": { + "type": "string", + "default": "X-CSRF-Token", + "description": "CSRF header name" + } + }, + "additionalProperties": false, + "default": {} + }, + "accountLinking": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow account linking" + }, + "autoLink": { + "type": "boolean", + "default": false, + "description": "Automatically link accounts with same email" + }, + "requireVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before linking" + } + }, + "additionalProperties": false, + "default": {} + }, + "twoFactor": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "description": "TOTP issuer name" + }, + "qrCodeSize": { + "type": "number", + "default": 200, + "description": "QR code size in pixels" + }, + "backupCodes": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "count": { + "type": "number", + "default": 10, + "description": "Number of backup codes to generate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "userFieldMapping": { + "type": "object", + "properties": { + "id": { + "type": "string", + "default": "id", + "description": "User ID field" + }, + "email": { + "type": "string", + "default": "email", + "description": "Email field" + }, + "name": { + "type": "string", + "default": "name", + "description": "Name field" + }, + "image": { + "type": "string", + "default": "image", + "description": "Profile image field" + }, + "emailVerified": { + "type": "string", + "default": "email_verified", + "description": "Email verification status field" + }, + "createdAt": { + "type": "string", + "default": "created_at", + "description": "Created timestamp field" + }, + "updatedAt": { + "type": "string", + "default": "updated_at", + "description": "Updated timestamp field" + } + }, + "additionalProperties": false, + "default": {} + }, + "database": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "prisma", + "drizzle", + "kysely", + "custom" + ], + "description": "Database adapter type" + }, + "connectionString": { + "type": "string", + "description": "Database connection string" + }, + "tablePrefix": { + "type": "string", + "default": "auth_", + "description": "Prefix for auth tables" + }, + "schema": { + "type": "string", + "description": "Database schema name" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "plugins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Plugin name" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "additionalProperties": {}, + "description": "Plugin-specific options" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "default": [] + }, + "hooks": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "description": "Authentication lifecycle hooks" + }, + "security": { + "type": "object", + "properties": { + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed origins" + }, + "trustProxy": { + "type": "boolean", + "default": false, + "description": "Trust proxy headers" + }, + "ipRateLimiting": { + "type": "boolean", + "default": true, + "description": "Enable IP-based rate limiting" + }, + "sessionFingerprinting": { + "type": "boolean", + "default": true, + "description": "Enable session fingerprinting" + }, + "maxSessions": { + "type": "number", + "default": 5, + "description": "Maximum concurrent sessions per user" + } + }, + "additionalProperties": false, + "description": "Advanced security settings" + }, + "email": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "email", + "description": "From email address" + }, + "fromName": { + "type": "string", + "description": "From name" + }, + "provider": { + "type": "string", + "enum": [ + "smtp", + "sendgrid", + "mailgun", + "ses", + "resend", + "custom" + ], + "description": "Email provider" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific configuration" + } + }, + "required": [ + "from", + "provider" + ], + "additionalProperties": false, + "description": "Email configuration" + }, + "ui": { + "type": "object", + "properties": { + "brandName": { + "type": "string", + "description": "Brand name displayed in auth UI" + }, + "logo": { + "type": "string", + "description": "Logo URL" + }, + "primaryColor": { + "type": "string", + "description": "Primary brand color (hex)" + }, + "customCss": { + "type": "string", + "description": "Custom CSS for auth pages" + } + }, + "additionalProperties": false, + "description": "UI customization" + }, + "active": { + "type": "boolean", + "default": true, + "description": "Whether this provider is active" + }, + "allowRegistration": { + "type": "boolean", + "default": true, + "description": "Allow new user registration" + } + }, + "required": [ + "name", + "label", + "strategies", + "baseUrl", + "secret" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/StandardAuthProvider.json b/packages/spec/json-schema/StandardAuthProvider.json new file mode 100644 index 000000000..67753239b --- /dev/null +++ b/packages/spec/json-schema/StandardAuthProvider.json @@ -0,0 +1,622 @@ +{ + "$ref": "#/definitions/StandardAuthProvider", + "definitions": { + "StandardAuthProvider": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "standard_auth", + "description": "Provider type identifier" + }, + "config": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Configuration name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label" + }, + "driver": { + "type": "string", + "default": "better-auth", + "description": "The underlying authentication implementation driver" + }, + "strategies": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email_password", + "magic_link", + "oauth", + "passkey", + "otp", + "anonymous" + ] + }, + "minItems": 1, + "description": "Enabled authentication strategies" + }, + "baseUrl": { + "type": "string", + "format": "uri", + "description": "Application base URL" + }, + "secret": { + "type": "string", + "minLength": 32, + "description": "Secret key for signing (min 32 chars)" + }, + "emailPassword": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "requireEmailVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before login" + }, + "minPasswordLength": { + "type": "number", + "minimum": 6, + "maximum": 128, + "default": 8, + "description": "Minimum password length" + }, + "requirePasswordComplexity": { + "type": "boolean", + "default": true, + "description": "Require uppercase, lowercase, numbers, symbols" + }, + "allowPasswordReset": { + "type": "boolean", + "default": true, + "description": "Enable password reset functionality" + }, + "passwordResetExpiry": { + "type": "number", + "default": 3600, + "description": "Password reset token expiry in seconds" + } + }, + "additionalProperties": false + }, + "magicLink": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "expiryTime": { + "type": "number", + "default": 900, + "description": "Magic link expiry time in seconds (default 15 min)" + } + }, + "additionalProperties": false + }, + "passkey": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "rpName": { + "type": "string", + "description": "Relying Party name" + }, + "rpId": { + "type": "string", + "description": "Relying Party ID (defaults to domain)" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Allowed origins for WebAuthn" + }, + "userVerification": { + "type": "string", + "enum": [ + "required", + "preferred", + "discouraged" + ], + "default": "preferred" + }, + "attestation": { + "type": "string", + "enum": [ + "none", + "indirect", + "direct", + "enterprise" + ], + "default": "none" + } + }, + "required": [ + "rpName" + ], + "additionalProperties": false + }, + "oauth": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google", + "github", + "facebook", + "twitter", + "linkedin", + "microsoft", + "apple", + "discord", + "gitlab", + "custom" + ], + "description": "OAuth provider type" + }, + "clientId": { + "type": "string", + "description": "OAuth client ID" + }, + "clientSecret": { + "type": "string", + "description": "OAuth client secret (typically from ENV)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Requested OAuth scopes" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "description": "OAuth callback URL" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this provider is enabled" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "provider", + "clientId", + "clientSecret" + ], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": [ + "providers" + ], + "additionalProperties": false + }, + "session": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number", + "default": 604800, + "description": "Session expiry in seconds (default 7 days)" + }, + "updateAge": { + "type": "number", + "default": 86400, + "description": "Session update interval in seconds (default 1 day)" + }, + "cookieName": { + "type": "string", + "default": "session_token", + "description": "Session cookie name" + }, + "cookieSecure": { + "type": "boolean", + "default": true, + "description": "Use secure cookies (HTTPS only)" + }, + "cookieSameSite": { + "type": "string", + "enum": [ + "strict", + "lax", + "none" + ], + "default": "lax", + "description": "SameSite cookie attribute" + }, + "cookieDomain": { + "type": "string", + "description": "Cookie domain" + }, + "cookiePath": { + "type": "string", + "default": "/", + "description": "Cookie path" + }, + "cookieHttpOnly": { + "type": "boolean", + "default": true, + "description": "HttpOnly cookie attribute" + } + }, + "additionalProperties": false, + "default": {} + }, + "rateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "maxAttempts": { + "type": "number", + "default": 5, + "description": "Maximum login attempts" + }, + "windowMs": { + "type": "number", + "default": 900000, + "description": "Time window in milliseconds (default 15 min)" + }, + "blockDuration": { + "type": "number", + "default": 900000, + "description": "Block duration after max attempts in ms" + }, + "skipSuccessfulRequests": { + "type": "boolean", + "default": false, + "description": "Only count failed requests" + } + }, + "additionalProperties": false, + "default": {} + }, + "csrf": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "tokenLength": { + "type": "number", + "default": 32, + "description": "CSRF token length" + }, + "cookieName": { + "type": "string", + "default": "csrf_token", + "description": "CSRF cookie name" + }, + "headerName": { + "type": "string", + "default": "X-CSRF-Token", + "description": "CSRF header name" + } + }, + "additionalProperties": false, + "default": {} + }, + "accountLinking": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow account linking" + }, + "autoLink": { + "type": "boolean", + "default": false, + "description": "Automatically link accounts with same email" + }, + "requireVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before linking" + } + }, + "additionalProperties": false, + "default": {} + }, + "twoFactor": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "description": "TOTP issuer name" + }, + "qrCodeSize": { + "type": "number", + "default": 200, + "description": "QR code size in pixels" + }, + "backupCodes": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "count": { + "type": "number", + "default": 10, + "description": "Number of backup codes to generate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "userFieldMapping": { + "type": "object", + "properties": { + "id": { + "type": "string", + "default": "id", + "description": "User ID field" + }, + "email": { + "type": "string", + "default": "email", + "description": "Email field" + }, + "name": { + "type": "string", + "default": "name", + "description": "Name field" + }, + "image": { + "type": "string", + "default": "image", + "description": "Profile image field" + }, + "emailVerified": { + "type": "string", + "default": "email_verified", + "description": "Email verification status field" + }, + "createdAt": { + "type": "string", + "default": "created_at", + "description": "Created timestamp field" + }, + "updatedAt": { + "type": "string", + "default": "updated_at", + "description": "Updated timestamp field" + } + }, + "additionalProperties": false, + "default": {} + }, + "database": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "prisma", + "drizzle", + "kysely", + "custom" + ], + "description": "Database adapter type" + }, + "connectionString": { + "type": "string", + "description": "Database connection string" + }, + "tablePrefix": { + "type": "string", + "default": "auth_", + "description": "Prefix for auth tables" + }, + "schema": { + "type": "string", + "description": "Database schema name" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "plugins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Plugin name" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "additionalProperties": {}, + "description": "Plugin-specific options" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "default": [] + }, + "hooks": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "description": "Authentication lifecycle hooks" + }, + "security": { + "type": "object", + "properties": { + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed origins" + }, + "trustProxy": { + "type": "boolean", + "default": false, + "description": "Trust proxy headers" + }, + "ipRateLimiting": { + "type": "boolean", + "default": true, + "description": "Enable IP-based rate limiting" + }, + "sessionFingerprinting": { + "type": "boolean", + "default": true, + "description": "Enable session fingerprinting" + }, + "maxSessions": { + "type": "number", + "default": 5, + "description": "Maximum concurrent sessions per user" + } + }, + "additionalProperties": false, + "description": "Advanced security settings" + }, + "email": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "email", + "description": "From email address" + }, + "fromName": { + "type": "string", + "description": "From name" + }, + "provider": { + "type": "string", + "enum": [ + "smtp", + "sendgrid", + "mailgun", + "ses", + "resend", + "custom" + ], + "description": "Email provider" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific configuration" + } + }, + "required": [ + "from", + "provider" + ], + "additionalProperties": false, + "description": "Email configuration" + }, + "ui": { + "type": "object", + "properties": { + "brandName": { + "type": "string", + "description": "Brand name displayed in auth UI" + }, + "logo": { + "type": "string", + "description": "Logo URL" + }, + "primaryColor": { + "type": "string", + "description": "Primary brand color (hex)" + }, + "customCss": { + "type": "string", + "description": "Custom CSS for auth pages" + } + }, + "additionalProperties": false, + "description": "UI customization" + }, + "active": { + "type": "boolean", + "default": true, + "description": "Whether this provider is active" + }, + "allowRegistration": { + "type": "boolean", + "default": true, + "description": "Allow new user registration" + } + }, + "required": [ + "name", + "label", + "strategies", + "baseUrl", + "secret" + ], + "additionalProperties": false, + "description": "Standard authentication configuration" + } + }, + "required": [ + "type", + "config" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/src/index.ts b/packages/spec/src/index.ts index 8e5262749..c82e5fd33 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -41,7 +41,7 @@ export * from './system/manifest.zod'; export * from './system/datasource.zod'; export * from './system/api.zod'; export * from './system/identity.zod'; -export * from './system/authentication.zod'; +export * from './system/auth.zod'; export * from './system/policy.zod'; export * from './system/role.zod'; export * from './system/territory.zod'; diff --git a/packages/spec/src/system/authentication.test.ts b/packages/spec/src/system/auth.test.ts similarity index 92% rename from packages/spec/src/system/authentication.test.ts rename to packages/spec/src/system/auth.test.ts index a7f66b723..9a949fce0 100644 --- a/packages/spec/src/system/authentication.test.ts +++ b/packages/spec/src/system/auth.test.ts @@ -13,12 +13,12 @@ import { UserFieldMappingSchema, DatabaseAdapterSchema, AuthPluginConfigSchema, - AuthenticationConfigSchema, - AuthenticationProviderSchema, + AuthConfigSchema, + StandardAuthProviderSchema, type AuthenticationConfig, type AuthenticationProvider, type OAuthProvider, -} from './authentication.zod'; +} from "./auth.zod"; describe('AuthStrategy', () => { it('should accept valid authentication strategies', () => { @@ -378,9 +378,9 @@ describe('AuthPluginConfigSchema', () => { }); }); -describe('AuthenticationConfigSchema', () => { +describe('AuthConfigSchema', () => { it('should accept minimal valid configuration', () => { - const config: AuthenticationConfig = { + const config: AuthConfig = { name: 'main_auth', label: 'Main Authentication', strategies: ['email_password'], @@ -392,11 +392,11 @@ describe('AuthenticationConfigSchema', () => { accountLinking: {}, }; - expect(() => AuthenticationConfigSchema.parse(config)).not.toThrow(); + expect(() => AuthConfigSchema.parse(config)).not.toThrow(); }); it('should accept comprehensive configuration', () => { - const config: AuthenticationConfig = { + const config: AuthConfig = { name: 'main_auth', label: 'Main Authentication', strategies: ['email_password', 'oauth', 'magic_link'], @@ -477,7 +477,7 @@ describe('AuthenticationConfigSchema', () => { allowRegistration: true, }; - expect(() => AuthenticationConfigSchema.parse(config)).not.toThrow(); + expect(() => AuthConfigSchema.parse(config)).not.toThrow(); }); it('should enforce snake_case for name field', () => { @@ -493,14 +493,14 @@ describe('AuthenticationConfigSchema', () => { accountLinking: {}, }; - expect(() => AuthenticationConfigSchema.parse(invalidConfig)).toThrow(); + expect(() => AuthConfigSchema.parse(invalidConfig)).toThrow(); const validConfig = { ...invalidConfig, name: 'main_auth', // snake_case - valid }; - expect(() => AuthenticationConfigSchema.parse(validConfig)).not.toThrow(); + expect(() => AuthConfigSchema.parse(validConfig)).not.toThrow(); }); it('should require at least one strategy', () => { @@ -516,7 +516,7 @@ describe('AuthenticationConfigSchema', () => { accountLinking: {}, }; - expect(() => AuthenticationConfigSchema.parse(config)).toThrow(); + expect(() => AuthConfigSchema.parse(config)).toThrow(); }); it('should require secret to be at least 32 characters', () => { @@ -532,7 +532,7 @@ describe('AuthenticationConfigSchema', () => { accountLinking: {}, }; - expect(() => AuthenticationConfigSchema.parse(config)).toThrow(); + expect(() => AuthConfigSchema.parse(config)).toThrow(); }); it('should validate baseUrl is a valid URL', () => { @@ -548,7 +548,7 @@ describe('AuthenticationConfigSchema', () => { accountLinking: {}, }; - expect(() => AuthenticationConfigSchema.parse(config)).toThrow(); + expect(() => AuthConfigSchema.parse(config)).toThrow(); }); it('should accept configuration with hooks', () => { @@ -572,7 +572,7 @@ describe('AuthenticationConfigSchema', () => { }, }; - expect(() => AuthenticationConfigSchema.parse(config)).not.toThrow(); + expect(() => AuthConfigSchema.parse(config)).not.toThrow(); }); it('should accept configuration with security settings', () => { @@ -595,7 +595,7 @@ describe('AuthenticationConfigSchema', () => { }, }; - expect(() => AuthenticationConfigSchema.parse(config)).not.toThrow(); + expect(() => AuthConfigSchema.parse(config)).not.toThrow(); }); it('should accept configuration with email settings', () => { @@ -619,7 +619,7 @@ describe('AuthenticationConfigSchema', () => { }, }; - expect(() => AuthenticationConfigSchema.parse(config)).not.toThrow(); + expect(() => AuthConfigSchema.parse(config)).not.toThrow(); }); it('should accept configuration with UI customization', () => { @@ -641,7 +641,7 @@ describe('AuthenticationConfigSchema', () => { }, }; - expect(() => AuthenticationConfigSchema.parse(config)).not.toThrow(); + expect(() => AuthConfigSchema.parse(config)).not.toThrow(); }); it('should use default values for optional fields', () => { @@ -653,7 +653,7 @@ describe('AuthenticationConfigSchema', () => { secret: 'a'.repeat(32), }; - const result = AuthenticationConfigSchema.parse(config); + const result = AuthConfigSchema.parse(config); expect(result.active).toBe(true); expect(result.allowRegistration).toBe(true); @@ -661,10 +661,10 @@ describe('AuthenticationConfigSchema', () => { }); }); -describe('AuthenticationProviderSchema', () => { +describe('StandardAuthProviderSchema', () => { it('should accept valid authentication provider', () => { const provider: AuthenticationProvider = { - type: 'authentication', + type: 'standard_auth', config: { name: 'main_auth', label: 'Main Auth', @@ -687,10 +687,10 @@ describe('AuthenticationProviderSchema', () => { }, }; - expect(() => AuthenticationProviderSchema.parse(provider)).not.toThrow(); + expect(() => StandardAuthProviderSchema.parse(provider)).not.toThrow(); }); - it('should require type to be "authentication"', () => { + it('should require type to be "standard_auth"', () => { const provider = { type: 'other_auth', // invalid config: { @@ -706,13 +706,13 @@ describe('AuthenticationProviderSchema', () => { }, }; - expect(() => AuthenticationProviderSchema.parse(provider)).toThrow(); + expect(() => StandardAuthProviderSchema.parse(provider)).toThrow(); }); }); describe('Type inference', () => { it('should correctly infer AuthenticationConfig type', () => { - const config: AuthenticationConfig = { + const config: AuthConfig = { name: 'test_auth', label: 'Test Auth', strategies: ['email_password'], @@ -729,9 +729,9 @@ describe('Type inference', () => { expect(config.strategies).toContain('email_password'); }); - it('should correctly infer AuthenticationProvider type', () => { + it('should correctly infer StandardAuthProvider type', () => { const provider: AuthenticationProvider = { - type: 'authentication', + type: 'standard_auth', config: { name: 'test_auth', label: 'Test', @@ -746,7 +746,7 @@ describe('Type inference', () => { }; // This test passes if TypeScript compiles without errors - expect(provider.type).toBe('authentication'); + expect(provider.type).toBe('standard_auth'); expect(provider.config.name).toBe('test_auth'); }); }); diff --git a/packages/spec/src/system/authentication.zod.ts b/packages/spec/src/system/auth.zod.ts similarity index 94% rename from packages/spec/src/system/authentication.zod.ts rename to packages/spec/src/system/auth.zod.ts index 9aa407f64..e00b12d90 100644 --- a/packages/spec/src/system/authentication.zod.ts +++ b/packages/spec/src/system/auth.zod.ts @@ -258,12 +258,13 @@ export type AuthPluginConfig = z.infer; * * @example * ```typescript - * const authConfig: AuthenticationConfig = { + * const authConfig: AuthConfig = { * name: 'main_auth', * label: 'Main Authentication', * strategies: ['email_password', 'oauth'], * baseUrl: 'https://app.example.com', * secret: process.env.AUTH_SECRET, + * driver: 'better-auth', // Optional, defaults to 'better-auth' * emailPassword: { * enabled: true, * minPasswordLength: 8, @@ -281,7 +282,7 @@ export type AuthPluginConfig = z.infer; * }; * ``` */ -export const AuthenticationConfigSchema = z.object({ +export const AuthConfigSchema = z.object({ /** * Unique identifier for this auth configuration * Must be in snake_case following ObjectStack conventions @@ -295,6 +296,13 @@ export const AuthenticationConfigSchema = z.object({ */ label: z.string().describe('Display label'), + /** + * The underlying authentication implementation driver + * Default: 'better-auth' (the reference implementation) + * Can be: 'better-auth', 'auth-js', 'passport', or custom driver name + */ + driver: z.string().default('better-auth').describe('The underlying authentication implementation driver'), + /** * Enabled authentication strategies */ @@ -467,18 +475,18 @@ export const AuthenticationConfigSchema = z.object({ }); /** - * TypeScript type inferred from AuthenticationConfigSchema + * TypeScript type inferred from AuthConfigSchema */ -export type AuthenticationConfig = z.infer; +export type AuthConfig = z.infer; /** - * Authentication Provider Schema + * Standard Authentication Provider Schema * Wraps the configuration for use in the identity system */ -export const AuthenticationProviderSchema = z.object({ - type: z.literal('authentication').describe('Provider type identifier'), +export const StandardAuthProviderSchema = z.object({ + type: z.literal('standard_auth').describe('Provider type identifier'), - config: AuthenticationConfigSchema.describe('Authentication configuration'), + config: AuthConfigSchema.describe('Standard authentication configuration'), }); -export type AuthenticationProvider = z.infer; +export type StandardAuthProvider = z.infer; From 62543243c6f30c8a125aa28051f3f137e474965b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 01:31:31 +0000 Subject: [PATCH 7/7] Fix: Correct type names and references in tests and documentation Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- docs/AUTHENTICATION_STANDARD.md | 24 ++++++++++++------------ packages/spec/src/system/auth.test.ts | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/AUTHENTICATION_STANDARD.md b/docs/AUTHENTICATION_STANDARD.md index 41974da7d..0c7582c8b 100644 --- a/docs/AUTHENTICATION_STANDARD.md +++ b/docs/AUTHENTICATION_STANDARD.md @@ -88,7 +88,7 @@ const authConfig: AuthConfig = { ### OAuth Example ```typescript -const oauthConfig: BetterAuthConfig = { +const oauthConfig: AuthConfig = { name: 'social_auth', label: 'Social Login', strategies: ['oauth'], @@ -116,7 +116,7 @@ const oauthConfig: BetterAuthConfig = { ### Multi-Strategy Example ```typescript -const multiAuthConfig: BetterAuthConfig = { +const multiAuthConfig: AuthConfig = { name: 'multi_auth', label: 'Multi-Strategy Auth', strategies: ['email_password', 'oauth', 'magic_link'], @@ -168,7 +168,7 @@ const multiAuthConfig: BetterAuthConfig = { |----------|------|----------|-------------| | `name` | `string` | ✅ | Configuration identifier (snake_case) | | `label` | `string` | ✅ | Human-readable label | -| `strategies` | `BetterAuthStrategy[]` | ✅ | Enabled authentication strategies | +| `strategies` | `AuthStrategy[]` | ✅ | Enabled authentication strategies | | `baseUrl` | `string` | ✅ | Application base URL | | `secret` | `string` | ✅ | Secret key for signing (min 32 chars) | @@ -337,10 +337,10 @@ See [examples/auth-better-examples.ts](../examples/auth-better-examples.ts) for ## Schema Files -- **Zod Schema**: `packages/spec/src/system/auth-better.zod.ts` -- **Tests**: `packages/spec/src/system/auth-better.test.ts` -- **JSON Schema**: `packages/spec/json-schema/BetterAuthConfig.json` -- **Documentation**: `content/docs/references/system/BetterAuthConfig.mdx` +- **Zod Schema**: `packages/spec/src/system/auth.zod.ts` +- **Tests**: `packages/spec/src/system/auth.test.ts` +- **JSON Schema**: `packages/spec/json-schema/AuthConfig.json` +- **Documentation**: `content/docs/references/system/AuthConfig.mdx` ## Type Safety @@ -348,9 +348,9 @@ All schemas are defined using Zod and TypeScript types are inferred automaticall ```typescript import type { - BetterAuthConfig, - BetterAuthProvider, - BetterAuthStrategy, + AuthConfig, + StandardAuthProvider, + AuthStrategy, OAuthProvider, SessionConfig, // ... and more @@ -362,13 +362,13 @@ import type { Following ObjectStack conventions: - **Configuration Keys** (TypeScript properties): `camelCase` (e.g., `maxAttempts`, `emailPassword`) -- **Machine Names** (Data values): `snake_case` (e.g., `name: 'better_auth'`, `strategy: 'email_password'`) +- **Machine Names** (Data values): `snake_case` (e.g., `name: 'main_auth'`, `strategy: 'email_password'`) ## Resources - [Better-Auth Documentation](https://better-auth.com) - [ObjectStack Documentation](https://objectstack.ai) -- [JSON Schema Reference](../packages/spec/json-schema/BetterAuthConfig.json) +- [JSON Schema Reference](../packages/spec/json-schema/AuthConfig.json) ## License diff --git a/packages/spec/src/system/auth.test.ts b/packages/spec/src/system/auth.test.ts index 9a949fce0..e63d1c4f7 100644 --- a/packages/spec/src/system/auth.test.ts +++ b/packages/spec/src/system/auth.test.ts @@ -15,8 +15,8 @@ import { AuthPluginConfigSchema, AuthConfigSchema, StandardAuthProviderSchema, - type AuthenticationConfig, - type AuthenticationProvider, + type AuthConfig, + type StandardAuthProvider, type OAuthProvider, } from "./auth.zod";