Skip to content

Add Better-Auth plugin for ObjectOS authentication#132

Merged
hotlong merged 6 commits intomainfrom
copilot/add-plugin-better-auth
Jan 29, 2026
Merged

Add Better-Auth plugin for ObjectOS authentication#132
hotlong merged 6 commits intomainfrom
copilot/add-plugin-better-auth

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 28, 2026

Implements @objectos/plugin-better-auth as a standalone authentication module based on Better-Auth library. Provides email/password auth, organization/team management, and RBAC with multi-database support (PostgreSQL, MongoDB, SQLite).

Architecture

  • Plugin lifecycle: Conforms to @objectstack/spec v0.6.0 with full lifecycle hooks
  • Resource management: Database connections properly closed on disable/uninstall
  • Route registration: Auto-mounts Better-Auth handlers at /api/auth/* during onEnable
  • Database detection: Automatic driver selection from connection string
  • First-user bootstrap: Initial user receives super_admin role

Implementation

// src/auth-client.ts - Better-Auth configuration with connection pooling
// src/plugin.ts - PluginDefinition with lifecycle hooks
// src/index.ts - Public API exports

Usage

import { createBetterAuthPlugin } from '@objectos/plugin-better-auth';

const authPlugin = createBetterAuthPlugin({
  databaseUrl: 'postgres://localhost:5432/mydb',
  baseURL: 'https://api.example.com/auth',
  trustedOrigins: ['https://example.com']
});

const os = new ObjectOS({ plugins: [authPlugin] });

Plugin Manifest

{
  id: 'com.objectos.auth.better-auth',
  version: '0.1.0',
  type: 'plugin',
  contributes: {
    events: ['auth.user.created', 'auth.user.login', 'auth.user.logout', ...]
  }
}

Key Design Decisions

  • Connection lifecycle: Database connections stored and closed in resetAuthInstance() to prevent leaks
  • Type safety: ExtendedAppContext interface for router/eventBus access instead of unsafe casts
  • Error resilience: Default role fallback ensures users always assigned a role on creation errors
  • API compatibility: Drop-in replacement for existing @objectos/server/auth implementation

Files

  • Core: auth-client.ts, plugin.ts, index.ts
  • Tests: 7 passing (structure validation)
  • Docs: README, INTEGRATION, CHANGELOG, usage examples
Original prompt

编写plugin-better-auth插件,基于better-auth,作为os的登录api


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@vercel
Copy link
Copy Markdown

vercel bot commented Jan 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
objectos Error Error Jan 28, 2026 4:27pm

Request Review

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Add plugin better-auth for OS login API Add Better-Auth plugin for ObjectOS authentication Jan 28, 2026
Copilot AI requested a review from hotlong January 28, 2026 16:30
@hotlong hotlong marked this pull request as ready for review January 29, 2026 00:37
Copilot AI review requested due to automatic review settings January 29, 2026 00:37
@hotlong hotlong merged commit c90a47e into main Jan 29, 2026
1 of 2 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements @objectos/plugin-better-auth, a standalone authentication plugin for ObjectOS based on the Better-Auth library. The plugin aims to provide email/password authentication, organization/team management, and RBAC with multi-database support, conforming to the @objectstack/spec v0.6.0 plugin lifecycle protocol.

Changes:

  • Adds a new plugin package at packages/plugins/better-auth/ with authentication capabilities using the Better-Auth library
  • Implements full plugin lifecycle hooks (onInstall, onEnable, onDisable, onUninstall) following the @objectstack/spec protocol
  • Provides multi-database support for PostgreSQL, MongoDB, and SQLite with automatic driver selection
  • Includes comprehensive documentation (README, INTEGRATION guide, CHANGELOG, usage examples)

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
packages/plugins/better-auth/src/auth-client.ts Better-Auth client configuration with database connection management and user role initialization
packages/plugins/better-auth/src/plugin.ts Plugin lifecycle implementation with route registration and event emission
packages/plugins/better-auth/src/index.ts Public API exports for the plugin
packages/plugins/better-auth/package.json Package configuration with dependencies for all supported database drivers
packages/plugins/better-auth/tsconfig.json TypeScript configuration for the plugin
packages/plugins/better-auth/jest.config.js Jest test configuration
packages/plugins/better-auth/test/plugin.test.ts Basic structural tests validating exports and manifest
packages/plugins/better-auth/examples/usage.ts Usage examples demonstrating plugin integration
packages/plugins/better-auth/README.md Plugin documentation
packages/plugins/better-auth/INTEGRATION.md Integration guide for ObjectOS Server
packages/plugins/better-auth/CHANGELOG.md Version history
packages/plugins/better-auth/SUMMARY.md Implementation summary
pnpm-lock.yaml Dependency lockfile updates for new plugin dependencies
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +100 to +104
app.router.all('/api/auth/*', async (req: any, res: any) => {
// Pass the request to Better-Auth handler
return handler(req, res);
});
context.logger.info('[Better-Auth Plugin] Routes registered at /api/auth/*');
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Route registration uses wildcard pattern that may conflict with other plugins. Registering all routes under /api/auth/* with router.all could prevent other plugins from registering more specific auth-related routes. Consider using a more specific mount point or documenting this limitation in the plugin manifest to prevent conflicts.

Suggested change
app.router.all('/api/auth/*', async (req: any, res: any) => {
// Pass the request to Better-Auth handler
return handler(req, res);
});
context.logger.info('[Better-Auth Plugin] Routes registered at /api/auth/*');
app.router.all('/api/auth/better-auth/*', async (req: any, res: any) => {
// Pass the request to Better-Auth handler
return handler(req, res);
});
context.logger.info('[Better-Auth Plugin] Routes registered at /api/auth/better-auth/*');

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +115
if (app.eventBus && typeof app.eventBus.emit === 'function') {
app.eventBus.emit('plugin.enabled', {
pluginId: BetterAuthManifest.id,
timestamp: new Date().toISOString()
});
}
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent usage of eventBus.emit. The code checks for app.eventBus.emit on lines 110 and 140, but according to the PluginContextData interface defined in the kernel, the eventBus is accessible via context.events.emit, not context.app.eventBus.emit. This code will not work as intended because it's checking for properties on the wrong object. Use context.events.emit instead of accessing eventBus through the app property.

Copilot uses AI. Check for mistakes.
const sqlite3Import = await import("better-sqlite3");
// Handle both ESM/Interop (default export) and CJS (direct export)
const Database = (sqlite3Import.default || sqlite3Import) as any;
const filename = (dbUrl?.replace('sqlite:', '')) || 'objectos.db';
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent SQLite file path handling compared to existing implementation. In the plugin version (line 47), the code uses dbUrl?.replace('sqlite:', '') which would leave an empty string if dbUrl is just 'sqlite:', while the server implementation (auth.client.ts:30) uses a conditional check with fallback. The plugin should use the same logic: (dbUrl && dbUrl.replace('sqlite:', '')) ? (dbUrl && dbUrl.replace('sqlite:', '')) : 'objectos.db' to ensure consistent behavior.

Suggested change
const filename = (dbUrl?.replace('sqlite:', '')) || 'objectos.db';
const filename = (dbUrl && dbUrl.replace('sqlite:', ''))
? (dbUrl && dbUrl.replace('sqlite:', ''))
: 'objectos.db';

Copilot uses AI. Check for mistakes.
console.log('[Better-Auth Plugin] Initialized successfully');
return authInstance;
} catch (e: any) {
console.error("[Better-Auth Plugin] Initialization Error:", e);
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console logging prefix inconsistency within the same module. The code uses both "[Better-Auth Plugin]" (lines 48, 102, 111, 156, 180) and plain console.error without prefix (line 159). For consistency and better log filtering, all console.log and console.error calls should use the same "[Better-Auth Plugin]" prefix format.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +9
let authInstance: any;
let dbConnection: any; // Store database connection for cleanup
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module-level singleton state without proper encapsulation creates potential issues in testing and concurrent usage scenarios. The authInstance and dbConnection variables are shared across all calls to getBetterAuth, which could lead to unexpected behavior if the function is called with different configurations. Consider encapsulating this state in a class or using a Map keyed by configuration to support multiple auth instances.

Copilot uses AI. Check for mistakes.
Comment on lines +138 to +146
// Emit plugin disabled event
const app = context.app as ExtendedAppContext;
if (app.eventBus && typeof app.eventBus.emit === 'function') {
app.eventBus.emit('plugin.disabled', {
pluginId: BetterAuthManifest.id,
timestamp: new Date().toISOString()
});
}

Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent usage of eventBus.emit. The code checks for app.eventBus.emit on lines 140-145, but according to the PluginContextData interface defined in the kernel, the eventBus is accessible via context.events.emit, not context.app.eventBus.emit. This code will not work as intended because it's checking for properties on the wrong object. Use context.events.emit instead of accessing eventBus through the app property.

Suggested change
// Emit plugin disabled event
const app = context.app as ExtendedAppContext;
if (app.eventBus && typeof app.eventBus.emit === 'function') {
app.eventBus.emit('plugin.disabled', {
pluginId: BetterAuthManifest.id,
timestamp: new Date().toISOString()
});
}
// Emit plugin disabled event via kernel event bus
if (context.events && typeof context.events.emit === 'function') {
context.events.emit('plugin.disabled', {
pluginId: BetterAuthManifest.id,
timestamp: new Date().toISOString()
});
}

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +118
} catch (e) {
console.error("[Better-Auth Plugin] Error in user create hook:", e);
// Ensure a default role is set even in error cases
return {
data: {
...user,
role: 'user'
}
};
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handler returns a user without a role assignment, which could create security issues. When the user creation hook encounters an error (line 110), it returns the user object without ensuring a default role is set. This deviates from the documented behavior and could result in users without proper access controls. The fallback should ensure the 'user' role is assigned, as shown in the comment on line 112.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +99
const app = context.app as ExtendedAppContext;
if (app.router && typeof app.router.all === 'function') {
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type casting with unsafe assertions bypasses TypeScript's type safety. The code casts context.app to ExtendedAppContext and then uses optional chaining to check for properties. This pattern is error-prone because it doesn't guarantee that the properties exist at runtime. Consider defining a type guard function to validate that the required properties exist before casting, or update the PluginContextData interface to properly type the app property.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +103
app.router.all('/api/auth/*', async (req: any, res: any) => {
// Pass the request to Better-Auth handler
return handler(req, res);
});
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The router.all method is not defined in the PluginContextData interface. Looking at the kernel implementation in plugin-context.ts, the router only has get, post, and use methods defined. The use of router.all on line 100 will fail at runtime because this method doesn't exist on the provided router interface. Consider using router.use instead, or request that the router.all method be added to the kernel's router interface.

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +85
### First User Setup

The first user to register automatically receives the `super_admin` role. Subsequent users get the `user` role by default.

Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior described here means the databaseHooks.user.create.before hook assigns the super_admin role to the first account created solely based on there being zero existing users, with no additional bootstrap secret or restriction. On a fresh deployment where the /api/auth/sign-up endpoint is exposed to untrusted networks, an attacker can simply register the first account and gain permanent super_admin privileges (and concurrent sign-ups can result in multiple super_admin users). Consider changing the bootstrap flow so that initial administrator creation is gated by an explicit one-time secret or out-of-band setup step rather than an unauthenticated first-signup heuristic.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants