From 8a4beec310e264414a843cd2facddf8afa9fb4b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 05:42:40 +0000 Subject: [PATCH 1/9] Initial plan From 2eddaa8c514d4424b34ecd45ec7a4abbaa1af881 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 05:47:46 +0000 Subject: [PATCH 2/9] feat: make AuthPlugin universal - graceful HTTP server handling and mock auth fallback AuthPlugin no longer requires HonoServerPlugin as a hard dependency. When http-server is unavailable (MSW/mock mode), it logs a warning and continues, keeping the auth service registered. HttpDispatcher.handleAuth() now provides mock fallback responses for core better-auth endpoints (sign-up, sign-in, sign-out, get-session) when no auth service or broker is available, preventing 404 errors in MSW/browser-only environments. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- ROADMAP.md | 1 + .../plugin-auth/src/auth-plugin.test.ts | 28 +++++++- .../plugins/plugin-auth/src/auth-plugin.ts | 19 +++-- packages/runtime/src/http-dispatcher.test.ts | 53 ++++++++++++++ packages/runtime/src/http-dispatcher.ts | 70 +++++++++++++++++-- 5 files changed, 160 insertions(+), 11 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 10b65dc764..23c4765f00 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -49,6 +49,7 @@ the ecosystem for enterprise workloads. | Client SDK (TypeScript) | ✅ | `@objectstack/client` | | React Hooks | ✅ | `@objectstack/client-react` | | Authentication (better-auth) | ✅ | `@objectstack/plugin-auth` | +| Auth in MSW/Mock Mode | ✅ | `@objectstack/plugin-auth` + `@objectstack/runtime` | | RBAC / RLS / FLS Security | ✅ | `@objectstack/plugin-security` | | CLI (16 commands) | ✅ | `@objectstack/cli` | | Dev Mode Plugin | ✅ | `@objectstack/plugin-dev` | diff --git a/packages/plugins/plugin-auth/src/auth-plugin.test.ts b/packages/plugins/plugin-auth/src/auth-plugin.test.ts index 44e20bae88..7dbdb8c3da 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.test.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.test.ts @@ -34,7 +34,7 @@ describe('AuthPlugin', () => { expect(authPlugin.name).toBe('com.objectstack.auth'); expect(authPlugin.type).toBe('standard'); expect(authPlugin.version).toBe('1.0.0'); - expect(authPlugin.dependencies).toContain('com.objectstack.server.hono'); + expect(authPlugin.dependencies).toEqual([]); }); }); @@ -149,6 +149,32 @@ describe('AuthPlugin', () => { expect(mockContext.getService).not.toHaveBeenCalledWith('http-server'); }); + it('should gracefully skip routes when http-server is not available', async () => { + mockContext.getService = vi.fn(() => null); + + await authPlugin.start(mockContext); + + expect(mockContext.getService).toHaveBeenCalledWith('http-server'); + expect(mockContext.logger.warn).toHaveBeenCalledWith( + expect.stringContaining('No HTTP server available') + ); + // Should NOT throw — auth service is still registered from init() + }); + + it('should gracefully handle http-server getService throwing', async () => { + mockContext.getService = vi.fn(() => { + throw new Error('Service not found: http-server'); + }); + + await authPlugin.start(mockContext); + + expect(mockContext.logger.warn).toHaveBeenCalledWith( + expect.stringContaining('HTTP server not available'), + expect.any(String) + ); + // Should NOT throw + }); + it('should throw error if auth not initialized', async () => { const uninitializedPlugin = new AuthPlugin({ secret: 'test-secret', diff --git a/packages/plugins/plugin-auth/src/auth-plugin.ts b/packages/plugins/plugin-auth/src/auth-plugin.ts index 0b827feedb..a70caf021c 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.ts @@ -46,7 +46,7 @@ export class AuthPlugin implements Plugin { name = 'com.objectstack.auth'; type = 'standard'; version = '1.0.0'; - dependencies = ['com.objectstack.server.hono']; // Requires HTTP server + dependencies: string[] = []; // HTTP server is optional; routes are registered only when available private options: AuthPluginOptions; private authManager: AuthManager | null = null; @@ -92,16 +92,23 @@ export class AuthPlugin implements Plugin { throw new Error('Auth manager not initialized'); } - // Register HTTP routes if enabled + // Register HTTP routes if enabled and HTTP server is available if (this.options.registerRoutes) { try { const httpServer = ctx.getService('http-server'); - this.registerAuthRoutes(httpServer, ctx); - ctx.logger.info(`Auth routes registered at ${this.options.basePath}`); + if (httpServer) { + this.registerAuthRoutes(httpServer, ctx); + ctx.logger.info(`Auth routes registered at ${this.options.basePath}`); + } else { + ctx.logger.warn( + 'No HTTP server available — auth routes not registered. ' + + 'Auth service is still available for MSW/mock environments via HttpDispatcher.' + ); + } } catch (error) { + // Gracefully handle missing HTTP server (e.g. MSW/mock mode) const err = error instanceof Error ? error : new Error(String(error)); - ctx.logger.error('Failed to register auth routes:', err); - throw err; + ctx.logger.warn('HTTP server not available, skipping auth route registration:', err.message); } } diff --git a/packages/runtime/src/http-dispatcher.test.ts b/packages/runtime/src/http-dispatcher.test.ts index 0e1d0b3128..5a3f4aa631 100644 --- a/packages/runtime/src/http-dispatcher.test.ts +++ b/packages/runtime/src/http-dispatcher.test.ts @@ -305,6 +305,59 @@ describe('HttpDispatcher', () => { }); }); + describe('handleAuth mock fallback (MSW/test mode)', () => { + beforeEach(() => { + // No auth service, no broker — simulates MSW/mock mode + (kernel as any).getService = vi.fn().mockResolvedValue(null); + (kernel as any).services = new Map(); + (kernel as any).broker = null; + }); + + it('should mock sign-up/email endpoint', async () => { + const result = await dispatcher.handleAuth('/sign-up/email', 'POST', { email: 'test@example.com', name: 'Test' }, { request: {} }); + expect(result.handled).toBe(true); + expect(result.response?.status).toBe(200); + expect(result.response?.body.user).toBeDefined(); + expect(result.response?.body.user.email).toBe('test@example.com'); + expect(result.response?.body.session).toBeDefined(); + }); + + it('should mock sign-in/email endpoint', async () => { + const result = await dispatcher.handleAuth('/sign-in/email', 'POST', { email: 'test@example.com' }, { request: {} }); + expect(result.handled).toBe(true); + expect(result.response?.status).toBe(200); + expect(result.response?.body.user).toBeDefined(); + expect(result.response?.body.session).toBeDefined(); + }); + + it('should mock get-session endpoint', async () => { + const result = await dispatcher.handleAuth('/get-session', 'GET', {}, { request: {} }); + expect(result.handled).toBe(true); + expect(result.response?.status).toBe(200); + expect(result.response?.body).toEqual({ session: null, user: null }); + }); + + it('should mock sign-out endpoint', async () => { + const result = await dispatcher.handleAuth('/sign-out', 'POST', {}, { request: {} }); + expect(result.handled).toBe(true); + expect(result.response?.status).toBe(200); + expect(result.response?.body).toEqual({ success: true }); + }); + + it('should mock login fallback when broker unavailable', async () => { + const result = await dispatcher.handleAuth('/login', 'POST', { email: 'test@example.com' }, { request: {} }); + expect(result.handled).toBe(true); + expect(result.response?.status).toBe(200); + expect(result.response?.body.user).toBeDefined(); + expect(result.response?.body.session).toBeDefined(); + }); + + it('should return unhandled for unknown auth path in mock mode', async () => { + const result = await dispatcher.handleAuth('/unknown', 'GET', {}, { request: {} }); + expect(result.handled).toBe(false); + }); + }); + describe('handleStorage with async service', () => { it('should resolve storage service from Promise', async () => { const mockStorage = { diff --git a/packages/runtime/src/http-dispatcher.ts b/packages/runtime/src/http-dispatcher.ts index fd66b36675..eb219af4c2 100644 --- a/packages/runtime/src/http-dispatcher.ts +++ b/packages/runtime/src/http-dispatcher.ts @@ -179,12 +179,74 @@ export class HttpDispatcher { return { handled: true, result: response }; } - // 2. Legacy Login + // 2. Legacy Login via broker const normalizedPath = path.replace(/^\/+/, ''); if (normalizedPath === 'login' && method.toUpperCase() === 'POST') { - const broker = this.ensureBroker(); - const data = await broker.call('auth.login', body, { request: context.request }); - return { handled: true, response: { status: 200, body: data } }; + try { + const broker = this.ensureBroker(); + const data = await broker.call('auth.login', body, { request: context.request }); + return { handled: true, response: { status: 200, body: data } }; + } catch { + // Broker not available — fall through to mock fallback + } + } + + // 3. Mock fallback for MSW/test environments when no auth service is registered + return this.mockAuthFallback(normalizedPath, method, body); + } + + /** + * Provides mock auth responses for core better-auth endpoints when + * AuthPlugin is not loaded (e.g. MSW/browser-only environments). + * This ensures registration/sign-in flows do not 404 in mock mode. + */ + private mockAuthFallback(path: string, method: string, body: any): HttpDispatcherResult { + const m = method.toUpperCase(); + + // POST sign-up/email + if ((path === 'sign-up/email' || path === 'register') && m === 'POST') { + const id = `mock_${Date.now()}`; + return { + handled: true, + response: { + status: 200, + body: { + user: { id, name: body?.name || 'Mock User', email: body?.email || 'mock@test.local', emailVerified: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, + session: { id: `session_${id}`, userId: id, token: `mock_token_${id}`, expiresAt: new Date(Date.now() + 86400000).toISOString() }, + }, + }, + }; + } + + // POST sign-in/email or login + if ((path === 'sign-in/email' || path === 'login') && m === 'POST') { + const id = `mock_${Date.now()}`; + return { + handled: true, + response: { + status: 200, + body: { + user: { id, name: 'Mock User', email: body?.email || 'mock@test.local', emailVerified: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, + session: { id: `session_${id}`, userId: id, token: `mock_token_${id}`, expiresAt: new Date(Date.now() + 86400000).toISOString() }, + }, + }, + }; + } + + // GET get-session + if (path === 'get-session' && m === 'GET') { + return { + handled: true, + response: { status: 200, body: { session: null, user: null } }, + }; + } + + // POST sign-out + if (path === 'sign-out' && m === 'POST') { + return { + handled: true, + response: { status: 200, body: { success: true } }, + }; } return { handled: false }; From e8335c171a6f38c39cf54441a781f1a0e70e7e7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 05:49:12 +0000 Subject: [PATCH 3/9] refactor: address code review - use randomUUID, named constant, and improve JSDoc Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/plugins/plugin-auth/src/auth-plugin.ts | 11 +++++++++-- packages/runtime/src/http-dispatcher.ts | 10 ++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/plugins/plugin-auth/src/auth-plugin.ts b/packages/plugins/plugin-auth/src/auth-plugin.ts index a70caf021c..c4c485342c 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.ts @@ -27,6 +27,13 @@ export interface AuthPluginOptions extends Partial { * * Provides authentication and identity services for ObjectStack applications. * + * **Dual-Mode Operation:** + * - **Server mode** (HonoServerPlugin active): Registers HTTP routes at basePath, + * forwarding all auth requests to better-auth's universal handler. + * - **MSW/Mock mode** (no HTTP server): Gracefully skips route registration but + * still registers the `auth` service, allowing HttpDispatcher.handleAuth() to + * simulate auth flows (sign-up, sign-in, etc.) for development and testing. + * * Features: * - Session management * - User registration/login @@ -35,8 +42,8 @@ export interface AuthPluginOptions extends Partial { * - 2FA, passkeys, magic links * * This plugin registers: - * - `auth` service (auth manager instance) - * - HTTP routes for authentication endpoints + * - `auth` service (auth manager instance) — always + * - HTTP routes for authentication endpoints — only when HTTP server is available * * Integrates with better-auth library to provide comprehensive * authentication capabilities including email/password, OAuth, 2FA, diff --git a/packages/runtime/src/http-dispatcher.ts b/packages/runtime/src/http-dispatcher.ts index eb219af4c2..69d1bad062 100644 --- a/packages/runtime/src/http-dispatcher.ts +++ b/packages/runtime/src/http-dispatcher.ts @@ -2,6 +2,7 @@ import { ObjectKernel, getEnv } from '@objectstack/core'; import { CoreServiceName } from '@objectstack/spec/system'; +import { randomUUID } from 'crypto'; export interface HttpProtocolContext { request: any; @@ -202,17 +203,18 @@ export class HttpDispatcher { */ private mockAuthFallback(path: string, method: string, body: any): HttpDispatcherResult { const m = method.toUpperCase(); + const MOCK_SESSION_EXPIRY_MS = 86_400_000; // 24 hours // POST sign-up/email if ((path === 'sign-up/email' || path === 'register') && m === 'POST') { - const id = `mock_${Date.now()}`; + const id = `mock_${randomUUID()}`; return { handled: true, response: { status: 200, body: { user: { id, name: body?.name || 'Mock User', email: body?.email || 'mock@test.local', emailVerified: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, - session: { id: `session_${id}`, userId: id, token: `mock_token_${id}`, expiresAt: new Date(Date.now() + 86400000).toISOString() }, + session: { id: `session_${id}`, userId: id, token: `mock_token_${id}`, expiresAt: new Date(Date.now() + MOCK_SESSION_EXPIRY_MS).toISOString() }, }, }, }; @@ -220,14 +222,14 @@ export class HttpDispatcher { // POST sign-in/email or login if ((path === 'sign-in/email' || path === 'login') && m === 'POST') { - const id = `mock_${Date.now()}`; + const id = `mock_${randomUUID()}`; return { handled: true, response: { status: 200, body: { user: { id, name: 'Mock User', email: body?.email || 'mock@test.local', emailVerified: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, - session: { id: `session_${id}`, userId: id, token: `mock_token_${id}`, expiresAt: new Date(Date.now() + 86400000).toISOString() }, + session: { id: `session_${id}`, userId: id, token: `mock_token_${id}`, expiresAt: new Date(Date.now() + MOCK_SESSION_EXPIRY_MS).toISOString() }, }, }, }; From aa1c4e22f181175144ad6f2a856bc5e13b6e6956 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 06:02:35 +0000 Subject: [PATCH 4/9] feat: add AuthPlugin to studio kernel factory and document MSW/mock mode - Add AuthPlugin to apps/studio createKernel.ts for default auth in MSW mode - Add @objectstack/plugin-auth dependency to studio package.json - Update authentication.mdx: HTTP server is optional, add MSW/Mock Mode section with minimal config example, mock fallback endpoints table, and studio usage Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- apps/studio/package.json | 1 + apps/studio/src/mocks/createKernel.ts | 7 +++ content/docs/guides/authentication.mdx | 67 +++++++++++++++++++++++++- pnpm-lock.yaml | 3 ++ 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/apps/studio/package.json b/apps/studio/package.json index 875b3a9b56..a4232eeba9 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -22,6 +22,7 @@ "@objectstack/driver-memory": "workspace:*", "@objectstack/metadata": "workspace:*", "@objectstack/objectql": "workspace:*", + "@objectstack/plugin-auth": "workspace:*", "@objectstack/plugin-msw": "workspace:*", "@objectstack/runtime": "workspace:*", "@objectstack/spec": "workspace:*", diff --git a/apps/studio/src/mocks/createKernel.ts b/apps/studio/src/mocks/createKernel.ts index df1eb96fb2..eabe0a94b9 100644 --- a/apps/studio/src/mocks/createKernel.ts +++ b/apps/studio/src/mocks/createKernel.ts @@ -4,6 +4,7 @@ import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime'; import { ObjectQLPlugin, SchemaRegistry } from '@objectstack/objectql'; import { InMemoryDriver } from '@objectstack/driver-memory'; import { MSWPlugin } from '@objectstack/plugin-msw'; +import { AuthPlugin } from '@objectstack/plugin-auth'; export interface KernelOptions { appConfigs?: any[]; // Multiple app configs @@ -42,6 +43,12 @@ export async function createKernel(options: KernelOptions) { // ObjectQLPlugin's ctx.registerService('protocol', ...) during bootstrap. console.log('[KernelFactory] Protocol service will be registered by ObjectQLPlugin'); + // Register AuthPlugin for MSW/mock mode (gracefully skips HTTP route registration) + await kernel.use(new AuthPlugin({ + secret: 'mock-dev-secret-at-least-32-characters-long', + baseUrl: 'http://localhost:5173', + })); + // --- BROKER SHIM (MUST be registered BEFORE MSWPlugin) --- // HttpDispatcher requires a broker to function. We inject a shim. (kernel as any).broker = { diff --git a/content/docs/guides/authentication.mdx b/content/docs/guides/authentication.mdx index fe22d97bc5..b05b156dad 100644 --- a/content/docs/guides/authentication.mdx +++ b/content/docs/guides/authentication.mdx @@ -18,6 +18,7 @@ Complete guide to implementing authentication in ObjectStack applications using 7. [Client Integration](#client-integration) 8. [API Reference](#api-reference) 9. [Best Practices](#best-practices) +10. [MSW/Mock Mode](#mswmock-mode) --- @@ -89,7 +90,7 @@ import { HonoServerPlugin } from '@objectstack/plugin-hono-server'; const kernel = new ObjectKernel({ plugins: [ - // HTTP server (required for auth routes) + // HTTP server (optional — auth works without it in MSW/mock mode) new HonoServerPlugin({ port: 3000, }), @@ -107,6 +108,10 @@ await kernel.start(); That's it! Your authentication endpoints are now available at `/api/v1/auth/*`. +> **MSW/Mock Mode:** AuthPlugin does **not** require an HTTP server. When HonoServerPlugin +> is absent, the plugin gracefully skips route registration and still registers the `auth` +> service. See [MSW/Mock Mode](#mswmock-mode) below for details. + ### 3. ObjectQL Data Persistence The plugin automatically uses ObjectQL for data persistence. No additional database configuration is required - it works with your existing ObjectQL setup. @@ -586,6 +591,66 @@ better-auth internally uses model names like `user` and `session`. The ObjectQL --- +## MSW/Mock Mode + +AuthPlugin is designed to work in **both** server and MSW/mock (browser-only) environments. This means you can develop and test authentication flows without running a real HTTP server. + +### How It Works + +- **Server mode** (HonoServerPlugin active): AuthPlugin registers HTTP routes at `/api/v1/auth/*` and forwards all requests to better-auth. +- **MSW/mock mode** (no HTTP server): AuthPlugin gracefully skips route registration but still registers the `auth` service. The `HttpDispatcher` provides mock fallback responses for core auth endpoints. + +### Minimal Configuration for Mock Mode + +```typescript +import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime'; +import { ObjectQLPlugin } from '@objectstack/objectql'; +import { InMemoryDriver } from '@objectstack/driver-memory'; +import { AuthPlugin } from '@objectstack/plugin-auth'; + +const kernel = new ObjectKernel(); + +await kernel.use(new ObjectQLPlugin()); +await kernel.use(new DriverPlugin(new InMemoryDriver(), 'memory')); + +// AuthPlugin works without HonoServerPlugin — no HTTP server needed +await kernel.use(new AuthPlugin({ + secret: 'mock-dev-secret-at-least-32-characters-long', + baseUrl: 'http://localhost:5173', +})); + +await kernel.bootstrap(); +``` + +### Mock Fallback Endpoints + +When no auth service handler or broker is available, `HttpDispatcher.handleAuth()` automatically provides mock responses for: + +| Endpoint | Method | Description | +|:---|:---:|:---| +| `sign-up/email` | POST | Returns mock user + session | +| `sign-in/email` | POST | Returns mock user + session | +| `login` | POST | Legacy login — returns mock user + session | +| `register` | POST | Alias for sign-up | +| `get-session` | GET | Returns `{ session: null, user: null }` | +| `sign-out` | POST | Returns `{ success: true }` | + +This ensures that registration and sign-in flows do not return 404 errors in MSW/browser-only environments. + +### Studio Kernel Factory + +The Studio app's `createKernel()` factory includes AuthPlugin by default: + +```typescript +// apps/studio/src/mocks/createKernel.ts +await kernel.use(new AuthPlugin({ + secret: 'mock-dev-secret-at-least-32-characters-long', + baseUrl: 'http://localhost:5173', +})); +``` + +--- + ## Next Steps - See [Security Guide](/docs/guides/security) for authorization and permissions diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e25bdb701..85a4953f82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,6 +133,9 @@ importers: '@objectstack/objectql': specifier: workspace:* version: link:../../packages/objectql + '@objectstack/plugin-auth': + specifier: workspace:* + version: link:../../packages/plugins/plugin-auth '@objectstack/plugin-msw': specifier: workspace:* version: link:../../packages/plugins/plugin-msw From aba0323e33903317dcd4deea74ca0aca7c444b04 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 06:03:32 +0000 Subject: [PATCH 5/9] security: use explicit INSECURE_DEV_ONLY secret naming and add warning callouts Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- apps/studio/src/mocks/createKernel.ts | 3 ++- content/docs/guides/authentication.mdx | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/studio/src/mocks/createKernel.ts b/apps/studio/src/mocks/createKernel.ts index eabe0a94b9..074f576514 100644 --- a/apps/studio/src/mocks/createKernel.ts +++ b/apps/studio/src/mocks/createKernel.ts @@ -44,8 +44,9 @@ export async function createKernel(options: KernelOptions) { console.log('[KernelFactory] Protocol service will be registered by ObjectQLPlugin'); // Register AuthPlugin for MSW/mock mode (gracefully skips HTTP route registration) + // WARNING: This secret is for local development only — never use in production await kernel.use(new AuthPlugin({ - secret: 'mock-dev-secret-at-least-32-characters-long', + secret: 'INSECURE_DEV_ONLY_mock_secret_do_not_use_in_production', baseUrl: 'http://localhost:5173', })); diff --git a/content/docs/guides/authentication.mdx b/content/docs/guides/authentication.mdx index b05b156dad..80ca0545dd 100644 --- a/content/docs/guides/authentication.mdx +++ b/content/docs/guides/authentication.mdx @@ -615,13 +615,15 @@ await kernel.use(new DriverPlugin(new InMemoryDriver(), 'memory')); // AuthPlugin works without HonoServerPlugin — no HTTP server needed await kernel.use(new AuthPlugin({ - secret: 'mock-dev-secret-at-least-32-characters-long', + secret: 'INSECURE_DEV_ONLY_mock_secret_do_not_use_in_production', baseUrl: 'http://localhost:5173', })); await kernel.bootstrap(); ``` +> ⚠️ **Warning:** The secret above is for **local development only**. In production, always use a strong random secret from an environment variable (`process.env.AUTH_SECRET`). + ### Mock Fallback Endpoints When no auth service handler or broker is available, `HttpDispatcher.handleAuth()` automatically provides mock responses for: @@ -643,8 +645,9 @@ The Studio app's `createKernel()` factory includes AuthPlugin by default: ```typescript // apps/studio/src/mocks/createKernel.ts +// WARNING: This secret is for local development only await kernel.use(new AuthPlugin({ - secret: 'mock-dev-secret-at-least-32-characters-long', + secret: 'INSECURE_DEV_ONLY_mock_secret_do_not_use_in_production', baseUrl: 'http://localhost:5173', })); ``` From 12fdc1fbb28d7b0935475ade648b2e40428a27c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 06:42:46 +0000 Subject: [PATCH 6/9] fix: prefix unused context param with underscore in handleI18n to fix TS6133 build error Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/runtime/src/http-dispatcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/src/http-dispatcher.ts b/packages/runtime/src/http-dispatcher.ts index 69d1bad062..c6636f4d05 100644 --- a/packages/runtime/src/http-dispatcher.ts +++ b/packages/runtime/src/http-dispatcher.ts @@ -550,7 +550,7 @@ export class HttpDispatcher { * GET /labels/:object/:locale → getFieldLabels (both from path) * GET /labels/:object?locale=xx → getFieldLabels (locale from query) */ - async handleI18n(path: string, method: string, query: any, context: HttpProtocolContext): Promise { + async handleI18n(path: string, method: string, query: any, _context: HttpProtocolContext): Promise { const i18nService = await this.getService(CoreServiceName.enum.i18n); if (!i18nService) return { handled: true, response: this.error('i18n service not available', 501) }; From cefcee245d4baab29c53fbdeb52093120219293e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 07:00:31 +0000 Subject: [PATCH 7/9] fix: pass error as Record to logger.warn to fix TS2345 in auth-plugin Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/plugins/plugin-auth/src/auth-plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugins/plugin-auth/src/auth-plugin.ts b/packages/plugins/plugin-auth/src/auth-plugin.ts index c4c485342c..1a53e73dc8 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.ts @@ -115,7 +115,7 @@ export class AuthPlugin implements Plugin { } catch (error) { // Gracefully handle missing HTTP server (e.g. MSW/mock mode) const err = error instanceof Error ? error : new Error(String(error)); - ctx.logger.warn('HTTP server not available, skipping auth route registration:', err.message); + ctx.logger.warn('HTTP server not available, skipping auth route registration', { error: err.message }); } } From 18b3e845f8c69794f70425880d4440fc75e38ecf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 07:43:38 +0000 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20address=20all=20review=20comments=20?= =?UTF-8?q?=E2=80=94=20browser-safe=20UUID,=20selective=20error=20handling?= =?UTF-8?q?,=20fix=20Studio=20build,=20update=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- apps/studio/package.json | 1 - apps/studio/src/mocks/createKernel.ts | 8 ----- content/docs/guides/authentication.mdx | 18 ++++++----- .../plugin-auth/src/auth-plugin.test.ts | 3 +- .../plugins/plugin-auth/src/auth-plugin.ts | 29 +++++++++--------- packages/runtime/src/http-dispatcher.ts | 30 +++++++++++++++++-- pnpm-lock.yaml | 3 -- 7 files changed, 54 insertions(+), 38 deletions(-) diff --git a/apps/studio/package.json b/apps/studio/package.json index a4232eeba9..875b3a9b56 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -22,7 +22,6 @@ "@objectstack/driver-memory": "workspace:*", "@objectstack/metadata": "workspace:*", "@objectstack/objectql": "workspace:*", - "@objectstack/plugin-auth": "workspace:*", "@objectstack/plugin-msw": "workspace:*", "@objectstack/runtime": "workspace:*", "@objectstack/spec": "workspace:*", diff --git a/apps/studio/src/mocks/createKernel.ts b/apps/studio/src/mocks/createKernel.ts index 074f576514..df1eb96fb2 100644 --- a/apps/studio/src/mocks/createKernel.ts +++ b/apps/studio/src/mocks/createKernel.ts @@ -4,7 +4,6 @@ import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime'; import { ObjectQLPlugin, SchemaRegistry } from '@objectstack/objectql'; import { InMemoryDriver } from '@objectstack/driver-memory'; import { MSWPlugin } from '@objectstack/plugin-msw'; -import { AuthPlugin } from '@objectstack/plugin-auth'; export interface KernelOptions { appConfigs?: any[]; // Multiple app configs @@ -43,13 +42,6 @@ export async function createKernel(options: KernelOptions) { // ObjectQLPlugin's ctx.registerService('protocol', ...) during bootstrap. console.log('[KernelFactory] Protocol service will be registered by ObjectQLPlugin'); - // Register AuthPlugin for MSW/mock mode (gracefully skips HTTP route registration) - // WARNING: This secret is for local development only — never use in production - await kernel.use(new AuthPlugin({ - secret: 'INSECURE_DEV_ONLY_mock_secret_do_not_use_in_production', - baseUrl: 'http://localhost:5173', - })); - // --- BROKER SHIM (MUST be registered BEFORE MSWPlugin) --- // HttpDispatcher requires a broker to function. We inject a shim. (kernel as any).broker = { diff --git a/content/docs/guides/authentication.mdx b/content/docs/guides/authentication.mdx index 80ca0545dd..78e16fb3a3 100644 --- a/content/docs/guides/authentication.mdx +++ b/content/docs/guides/authentication.mdx @@ -626,7 +626,7 @@ await kernel.bootstrap(); ### Mock Fallback Endpoints -When no auth service handler or broker is available, `HttpDispatcher.handleAuth()` automatically provides mock responses for: +When no auth service handler is registered and the legacy broker login is unavailable, `HttpDispatcher.handleAuth()` automatically provides mock responses for: | Endpoint | Method | Description | |:---|:---:|:---| @@ -639,17 +639,21 @@ When no auth service handler or broker is available, `HttpDispatcher.handleAuth( This ensures that registration and sign-in flows do not return 404 errors in MSW/browser-only environments. +> **Note:** In server mode with AuthPlugin loaded, the auth service handler takes priority and the mock fallback is never reached. The mock fallback only activates when AuthPlugin is not loaded (e.g. browser-only Studio builds where `better-auth` is unavailable). + ### Studio Kernel Factory -The Studio app's `createKernel()` factory includes AuthPlugin by default: +The Studio app runs in the browser, where the Node-only `better-auth` library cannot be bundled. Instead of loading `AuthPlugin` directly, Studio relies on `HttpDispatcher`'s built-in mock fallback to handle auth endpoints in MSW mode: ```typescript // apps/studio/src/mocks/createKernel.ts -// WARNING: This secret is for local development only -await kernel.use(new AuthPlugin({ - secret: 'INSECURE_DEV_ONLY_mock_secret_do_not_use_in_production', - baseUrl: 'http://localhost:5173', -})); +// No AuthPlugin needed — HttpDispatcher provides mock auth endpoints automatically +const kernel = new ObjectKernel(); +await kernel.use(new ObjectQLPlugin()); +await kernel.use(new DriverPlugin(driver, 'memory')); +// ... +await kernel.use(new MSWPlugin({ /* ... */ })); +await kernel.bootstrap(); ``` --- diff --git a/packages/plugins/plugin-auth/src/auth-plugin.test.ts b/packages/plugins/plugin-auth/src/auth-plugin.test.ts index 7dbdb8c3da..9cd5782f4c 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.test.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.test.ts @@ -169,8 +169,7 @@ describe('AuthPlugin', () => { await authPlugin.start(mockContext); expect(mockContext.logger.warn).toHaveBeenCalledWith( - expect.stringContaining('HTTP server not available'), - expect.any(String) + expect.stringContaining('No HTTP server available') ); // Should NOT throw }); diff --git a/packages/plugins/plugin-auth/src/auth-plugin.ts b/packages/plugins/plugin-auth/src/auth-plugin.ts index 1a53e73dc8..2bae195c0e 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.ts @@ -101,21 +101,22 @@ export class AuthPlugin implements Plugin { // Register HTTP routes if enabled and HTTP server is available if (this.options.registerRoutes) { + let httpServer: IHttpServer | null = null; try { - const httpServer = ctx.getService('http-server'); - if (httpServer) { - this.registerAuthRoutes(httpServer, ctx); - ctx.logger.info(`Auth routes registered at ${this.options.basePath}`); - } else { - ctx.logger.warn( - 'No HTTP server available — auth routes not registered. ' + - 'Auth service is still available for MSW/mock environments via HttpDispatcher.' - ); - } - } catch (error) { - // Gracefully handle missing HTTP server (e.g. MSW/mock mode) - const err = error instanceof Error ? error : new Error(String(error)); - ctx.logger.warn('HTTP server not available, skipping auth route registration', { error: err.message }); + httpServer = ctx.getService('http-server'); + } catch { + // Service not found — expected in MSW/mock mode + } + + if (httpServer) { + // Route registration errors should propagate (server misconfiguration) + this.registerAuthRoutes(httpServer, ctx); + ctx.logger.info(`Auth routes registered at ${this.options.basePath}`); + } else { + ctx.logger.warn( + 'No HTTP server available — auth routes not registered. ' + + 'Auth service is still available for MSW/mock environments via HttpDispatcher.' + ); } } diff --git a/packages/runtime/src/http-dispatcher.ts b/packages/runtime/src/http-dispatcher.ts index c6636f4d05..645598e454 100644 --- a/packages/runtime/src/http-dispatcher.ts +++ b/packages/runtime/src/http-dispatcher.ts @@ -2,7 +2,19 @@ import { ObjectKernel, getEnv } from '@objectstack/core'; import { CoreServiceName } from '@objectstack/spec/system'; -import { randomUUID } from 'crypto'; + +/** Browser-safe UUID generator — prefers Web Crypto, falls back to RFC 4122 v4 */ +function randomUUID(): string { + const cryptoObj = typeof globalThis !== 'undefined' ? (globalThis as any).crypto : undefined; + if (cryptoObj && typeof cryptoObj.randomUUID === 'function') { + return cryptoObj.randomUUID(); + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} export interface HttpProtocolContext { request: any; @@ -187,8 +199,20 @@ export class HttpDispatcher { const broker = this.ensureBroker(); const data = await broker.call('auth.login', body, { request: context.request }); return { handled: true, response: { status: 200, body: data } }; - } catch { - // Broker not available — fall through to mock fallback + } catch (error: any) { + // Only fall through to mock when the broker is truly unavailable. + const msg = error?.message || ''; + const statusCode = error?.statusCode ?? error?.status; + const isBrokerUnavailable = + msg.includes('not available') || + msg.includes('not found') || + msg.includes('not registered') || + statusCode === 503; + + if (!isBrokerUnavailable) { + // Propagate real auth failures so callers see the correct error + throw error; + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85a4953f82..2e25bdb701 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,9 +133,6 @@ importers: '@objectstack/objectql': specifier: workspace:* version: link:../../packages/objectql - '@objectstack/plugin-auth': - specifier: workspace:* - version: link:../../packages/plugins/plugin-auth '@objectstack/plugin-msw': specifier: workspace:* version: link:../../packages/plugins/plugin-msw From 711e696dd64e2dbd215144b9121026be145ad350 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 07:45:34 +0000 Subject: [PATCH 9/9] fix: simplify randomUUID guard, tighten broker error check, add auth service assertion in test Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../plugin-auth/src/auth-plugin.test.ts | 2 ++ packages/runtime/src/http-dispatcher.ts | 18 +++++------------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/plugins/plugin-auth/src/auth-plugin.test.ts b/packages/plugins/plugin-auth/src/auth-plugin.test.ts index 9cd5782f4c..f8ee2678c0 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.test.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.test.ts @@ -171,6 +171,8 @@ describe('AuthPlugin', () => { expect(mockContext.logger.warn).toHaveBeenCalledWith( expect.stringContaining('No HTTP server available') ); + // Auth service should still be registered from init() + expect(mockContext.registerService).toHaveBeenCalledWith('auth', expect.anything()); // Should NOT throw }); diff --git a/packages/runtime/src/http-dispatcher.ts b/packages/runtime/src/http-dispatcher.ts index 645598e454..1d54e04dc2 100644 --- a/packages/runtime/src/http-dispatcher.ts +++ b/packages/runtime/src/http-dispatcher.ts @@ -5,9 +5,8 @@ import { CoreServiceName } from '@objectstack/spec/system'; /** Browser-safe UUID generator — prefers Web Crypto, falls back to RFC 4122 v4 */ function randomUUID(): string { - const cryptoObj = typeof globalThis !== 'undefined' ? (globalThis as any).crypto : undefined; - if (cryptoObj && typeof cryptoObj.randomUUID === 'function') { - return cryptoObj.randomUUID(); + if (globalThis.crypto && typeof globalThis.crypto.randomUUID === 'function') { + return globalThis.crypto.randomUUID(); } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = (Math.random() * 16) | 0; @@ -200,17 +199,10 @@ export class HttpDispatcher { const data = await broker.call('auth.login', body, { request: context.request }); return { handled: true, response: { status: 200, body: data } }; } catch (error: any) { - // Only fall through to mock when the broker is truly unavailable. - const msg = error?.message || ''; + // Only fall through to mock when the broker is truly unavailable + // (ensureBroker throws statusCode 500 when kernel.broker is null) const statusCode = error?.statusCode ?? error?.status; - const isBrokerUnavailable = - msg.includes('not available') || - msg.includes('not found') || - msg.includes('not registered') || - statusCode === 503; - - if (!isBrokerUnavailable) { - // Propagate real auth failures so callers see the correct error + if (statusCode !== 500 || !error?.message?.includes('Broker not available')) { throw error; } }