Skip to content

Commit e00c489

Browse files
authored
Merge pull request #584 from objectstack-ai/copilot/verify-plugin-auth-registration
2 parents 8f98dcc + 106d98f commit e00c489

6 files changed

Lines changed: 361 additions & 45 deletions

File tree

examples/minimal-auth/README.md

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ This will:
6767
4. Get the current session
6868
5. Test password reset flow
6969

70+
### 5. Test Dynamic Discovery (Optional)
71+
72+
```bash
73+
pnpm tsx src/test-discovery.ts
74+
```
75+
76+
This test demonstrates how the auth service automatically appears in the API discovery response when `plugin-auth` is registered. Before the plugin is registered, `discovery.services.auth.status` is "unavailable". After registration, it becomes "available" with the proper route information.
77+
7078
## Usage
7179

7280
### Using the ObjectStack Client
@@ -122,12 +130,59 @@ curl http://localhost:3000/api/v1/auth/get-session \
122130
```
123131
minimal-auth/
124132
├── src/
125-
│ ├── server.ts # Server setup with AuthPlugin
126-
│ └── test-auth.ts # Authentication flow test
133+
│ ├── server.ts # Server setup with AuthPlugin
134+
│ ├── test-auth.ts # Authentication flow test
135+
│ └── test-discovery.ts # Discovery API test (dynamic service detection)
127136
├── package.json
128137
└── README.md
129138
```
130139

140+
## Dynamic Service Discovery
141+
142+
ObjectStack features a **dynamic service discovery** system that automatically reflects which plugins are registered. This is particularly useful for clients that need to adapt their UI or behavior based on available services.
143+
144+
**Discovery Response Without Auth Plugin:**
145+
```json
146+
{
147+
"services": {
148+
"auth": {
149+
"enabled": false,
150+
"status": "unavailable",
151+
"message": "Install plugin-auth to enable"
152+
}
153+
}
154+
}
155+
```
156+
157+
**Discovery Response With Auth Plugin:**
158+
```json
159+
{
160+
"services": {
161+
"auth": {
162+
"enabled": true,
163+
"status": "available",
164+
"route": "/api/v1/auth",
165+
"provider": "plugin-auth"
166+
}
167+
},
168+
"endpoints": {
169+
"auth": "/api/v1/auth"
170+
}
171+
}
172+
```
173+
174+
Clients can use this to check service availability:
175+
```typescript
176+
const discovery = await client.getDiscovery();
177+
if (discovery.services.auth?.enabled) {
178+
// Auth is available - show login UI
179+
await client.auth.login({ ... });
180+
} else {
181+
// Auth not available - hide login UI
182+
console.log(discovery.services.auth?.message);
183+
}
184+
```
185+
131186
## Advanced Configuration
132187

133188
See `src/server.ts` for examples of enabling advanced features:
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Discovery Test
5+
*
6+
* This test verifies that the discovery endpoint correctly reflects
7+
* the availability of the auth service when plugin-auth is registered.
8+
*/
9+
10+
import { ObjectKernel } from '@objectstack/core';
11+
import { ObjectQL } from '@objectstack/objectql';
12+
import { ObjectQLPlugin } from '@objectstack/objectql';
13+
import { InMemoryDriver } from '@objectstack/driver-memory';
14+
import { AuthPlugin } from '@objectstack/plugin-auth';
15+
16+
async function testDiscovery() {
17+
console.log('🧪 Testing Discovery with Auth Plugin...\n');
18+
19+
// 1. Create ObjectQL instance with in-memory driver
20+
const objectql = new ObjectQL();
21+
await objectql.registerDriver(new InMemoryDriver());
22+
23+
// 2. Create kernel
24+
const kernel = new ObjectKernel();
25+
26+
// 3. Register ObjectQL plugin (which provides protocol service)
27+
await kernel.use(new ObjectQLPlugin(objectql));
28+
29+
// 4. Get discovery BEFORE auth is registered
30+
console.log('📋 Discovery BEFORE auth plugin:');
31+
let protocol = kernel.getService('protocol');
32+
let discovery = await protocol.getDiscovery();
33+
console.log(' - Auth service enabled:', discovery.services?.auth?.enabled);
34+
console.log(' - Auth service status:', discovery.services?.auth?.status);
35+
console.log(' - Auth endpoint:', discovery.endpoints?.auth || 'undefined');
36+
console.log('');
37+
38+
// 5. Register auth plugin
39+
await kernel.use(new AuthPlugin({
40+
secret: 'test-secret-min-32-characters-long-for-jwt-signing',
41+
baseUrl: 'http://localhost:3000',
42+
}));
43+
44+
// 6. Get discovery AFTER auth is registered
45+
console.log('📋 Discovery AFTER auth plugin:');
46+
protocol = kernel.getService('protocol');
47+
discovery = await protocol.getDiscovery();
48+
console.log(' - Auth service enabled:', discovery.services?.auth?.enabled);
49+
console.log(' - Auth service status:', discovery.services?.auth?.status);
50+
console.log(' - Auth service route:', discovery.services?.auth?.route);
51+
console.log(' - Auth service provider:', discovery.services?.auth?.provider);
52+
console.log(' - Auth endpoint:', discovery.endpoints?.auth);
53+
console.log('');
54+
55+
// 7. Verify the results
56+
if (discovery.services?.auth?.enabled && discovery.services?.auth?.status === 'available') {
57+
console.log('✅ SUCCESS: Auth service is correctly shown as available!');
58+
console.log('✅ Discovery endpoint is working correctly!');
59+
} else {
60+
console.log('❌ FAILED: Auth service should be available but is not!');
61+
console.log(' Actual status:', discovery.services?.auth);
62+
process.exit(1);
63+
}
64+
65+
// 8. Clean up
66+
await kernel.shutdown();
67+
console.log('\n🎉 All tests passed!');
68+
}
69+
70+
testDiscovery().catch((error) => {
71+
console.error('❌ Error:', error);
72+
process.exit(1);
73+
});

packages/objectql/src/plugin.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ export class ObjectQLPlugin implements Plugin {
6161
});
6262

6363
// Register Protocol Implementation
64-
const protocolShim = new ObjectStackProtocolImplementation(this.ql);
64+
const protocolShim = new ObjectStackProtocolImplementation(
65+
this.ql,
66+
() => ctx.getServices ? ctx.getServices() : new Map()
67+
);
6568

6669
ctx.registerService('protocol', protocolShim);
6770
ctx.logger.info('Protocol service registered');
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect, beforeEach } from 'vitest';
4+
import { ObjectStackProtocolImplementation } from './protocol.js';
5+
import { ObjectQL } from './engine.js';
6+
7+
describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () => {
8+
let protocol: ObjectStackProtocolImplementation;
9+
let engine: ObjectQL;
10+
11+
beforeEach(() => {
12+
engine = new ObjectQL();
13+
});
14+
15+
it('should return unavailable auth service when no services registered', async () => {
16+
// Create protocol without service registry
17+
protocol = new ObjectStackProtocolImplementation(engine);
18+
19+
const discovery = await protocol.getDiscovery();
20+
21+
expect(discovery.services.auth).toBeDefined();
22+
expect(discovery.services.auth.enabled).toBe(false);
23+
expect(discovery.services.auth.status).toBe('unavailable');
24+
expect(discovery.services.auth.message).toContain('plugin-auth');
25+
expect(discovery.capabilities.workflow).toBe(false);
26+
});
27+
28+
it('should return available auth service when auth is registered', async () => {
29+
// Mock service registry with auth service
30+
const mockServices = new Map<string, any>();
31+
mockServices.set('auth', { /* mock auth service */ });
32+
33+
protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
34+
35+
const discovery = await protocol.getDiscovery();
36+
37+
expect(discovery.services.auth).toBeDefined();
38+
expect(discovery.services.auth.enabled).toBe(true);
39+
expect(discovery.services.auth.status).toBe('available');
40+
expect(discovery.services.auth.route).toBe('/api/v1/auth');
41+
expect(discovery.services.auth.provider).toBe('plugin-auth');
42+
expect(discovery.endpoints.auth).toBe('/api/v1/auth');
43+
});
44+
45+
it('should return available workflow when automation service is registered', async () => {
46+
const mockServices = new Map<string, any>();
47+
mockServices.set('automation', { /* mock automation service */ });
48+
49+
protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
50+
51+
const discovery = await protocol.getDiscovery();
52+
53+
expect(discovery.services.automation).toBeDefined();
54+
expect(discovery.services.automation.enabled).toBe(true);
55+
expect(discovery.services.automation.status).toBe('available');
56+
expect(discovery.capabilities.workflow).toBe(true);
57+
});
58+
59+
it('should return multiple available services when registered', async () => {
60+
const mockServices = new Map<string, any>();
61+
mockServices.set('auth', {});
62+
mockServices.set('realtime', {});
63+
mockServices.set('ai', {});
64+
65+
protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
66+
67+
const discovery = await protocol.getDiscovery();
68+
69+
// Check auth
70+
expect(discovery.services.auth.enabled).toBe(true);
71+
expect(discovery.services.auth.status).toBe('available');
72+
73+
// Check realtime
74+
expect(discovery.services.realtime.enabled).toBe(true);
75+
expect(discovery.services.realtime.status).toBe('available');
76+
expect(discovery.capabilities.websockets).toBe(true);
77+
78+
// Check AI
79+
expect(discovery.services.ai.enabled).toBe(true);
80+
expect(discovery.services.ai.status).toBe('available');
81+
expect(discovery.capabilities.ai).toBe(true);
82+
83+
// Endpoints should include available services
84+
expect(discovery.endpoints.auth).toBe('/api/v1/auth');
85+
expect(discovery.endpoints.realtime).toBe('/api/v1/realtime');
86+
expect(discovery.endpoints.ai).toBe('/api/v1/ai');
87+
});
88+
89+
it('should always show core services as available', async () => {
90+
protocol = new ObjectStackProtocolImplementation(engine);
91+
92+
const discovery = await protocol.getDiscovery();
93+
94+
// Core services should always be available
95+
expect(discovery.services.metadata.enabled).toBe(true);
96+
expect(discovery.services.metadata.status).toBe('degraded');
97+
expect(discovery.services.data.enabled).toBe(true);
98+
expect(discovery.services.data.status).toBe('available');
99+
expect(discovery.services.analytics.enabled).toBe(true);
100+
expect(discovery.services.analytics.status).toBe('available');
101+
102+
// Core capabilities
103+
expect(discovery.capabilities.analytics).toBe(true);
104+
});
105+
106+
it('should map file-storage service to storage endpoint', async () => {
107+
const mockServices = new Map<string, any>();
108+
mockServices.set('file-storage', {});
109+
110+
protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
111+
112+
const discovery = await protocol.getDiscovery();
113+
114+
expect(discovery.services['file-storage'].enabled).toBe(true);
115+
expect(discovery.services['file-storage'].status).toBe('available');
116+
expect(discovery.endpoints.storage).toBe('/api/v1/storage');
117+
expect(discovery.capabilities.files).toBe(true);
118+
});
119+
120+
it('should handle workflow capability from either automation or workflow service', async () => {
121+
// Test with workflow service
122+
const mockServicesWithWorkflow = new Map<string, any>();
123+
mockServicesWithWorkflow.set('workflow', {});
124+
125+
protocol = new ObjectStackProtocolImplementation(engine, () => mockServicesWithWorkflow);
126+
let discovery = await protocol.getDiscovery();
127+
expect(discovery.capabilities.workflow).toBe(true);
128+
129+
// Test with automation service
130+
const mockServicesWithAutomation = new Map<string, any>();
131+
mockServicesWithAutomation.set('automation', {});
132+
133+
protocol = new ObjectStackProtocolImplementation(engine, () => mockServicesWithAutomation);
134+
discovery = await protocol.getDiscovery();
135+
expect(discovery.capabilities.workflow).toBe(true);
136+
});
137+
});

0 commit comments

Comments
 (0)