@objectstack/plugin-hono-server 4.0.4 → 4.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,240 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { ObjectStackManifest } from '@objectstack/spec/system';
4
-
5
- /**
6
- * Hono Server Plugin Manifest
7
- *
8
- * HTTP server adapter plugin using the Hono framework.
9
- * Provides northbound HTTP/REST API gateway capabilities.
10
- */
11
- const HonoServerPlugin: ObjectStackManifest = {
12
- id: 'com.objectstack.server.hono',
13
- name: 'Hono Server Adapter',
14
- version: '1.0.0',
15
- type: 'adapter',
16
- description: 'HTTP server adapter using Hono framework. Exposes ObjectStack Runtime Protocol via REST API endpoints.',
17
-
18
- configuration: {
19
- title: 'Hono Server Configuration',
20
- properties: {
21
- port: {
22
- type: 'number',
23
- default: 3000,
24
- description: 'HTTP server port',
25
- },
26
- staticRoot: {
27
- type: 'string',
28
- description: 'Path to static files directory (optional)',
29
- },
30
- },
31
- },
32
-
33
- // Plugin Capability Declaration
34
- capabilities: {
35
- // Protocols This Plugin Implements
36
- implements: [
37
- {
38
- protocol: {
39
- id: 'com.objectstack.protocol.http.v1',
40
- label: 'HTTP Server Protocol v1',
41
- version: { major: 1, minor: 0, patch: 0 },
42
- description: 'Standard HTTP server capabilities',
43
- },
44
- conformance: 'full',
45
- certified: false,
46
- },
47
- {
48
- protocol: {
49
- id: 'com.objectstack.protocol.api.rest.v1',
50
- label: 'REST API Protocol v1',
51
- version: { major: 1, minor: 0, patch: 0 },
52
- description: 'RESTful API endpoint implementation',
53
- },
54
- conformance: 'full',
55
- features: [
56
- {
57
- name: 'meta_protocol',
58
- enabled: true,
59
- description: 'Metadata discovery endpoints',
60
- },
61
- {
62
- name: 'data_protocol',
63
- enabled: true,
64
- description: 'CRUD data operations',
65
- },
66
- {
67
- name: 'ui_protocol',
68
- enabled: true,
69
- description: 'UI view metadata endpoints',
70
- },
71
- ],
72
- certified: false,
73
- },
74
- ],
75
-
76
- // Interfaces This Plugin Provides
77
- provides: [
78
- {
79
- id: 'com.objectstack.server.hono.interface.http_server',
80
- name: 'IHttpServer',
81
- description: 'HTTP server service interface',
82
- version: { major: 1, minor: 0, patch: 0 },
83
- stability: 'stable',
84
- methods: [
85
- {
86
- name: 'get',
87
- description: 'Register GET route handler',
88
- parameters: [
89
- {
90
- name: 'path',
91
- type: 'string',
92
- required: true,
93
- description: 'Route path pattern',
94
- },
95
- {
96
- name: 'handler',
97
- type: 'Function',
98
- required: true,
99
- description: 'Route handler function',
100
- },
101
- ],
102
- returnType: 'void',
103
- async: false,
104
- },
105
- {
106
- name: 'post',
107
- description: 'Register POST route handler',
108
- parameters: [
109
- {
110
- name: 'path',
111
- type: 'string',
112
- required: true,
113
- description: 'Route path pattern',
114
- },
115
- {
116
- name: 'handler',
117
- type: 'Function',
118
- required: true,
119
- description: 'Route handler function',
120
- },
121
- ],
122
- returnType: 'void',
123
- async: false,
124
- },
125
- {
126
- name: 'patch',
127
- description: 'Register PATCH route handler',
128
- parameters: [
129
- {
130
- name: 'path',
131
- type: 'string',
132
- required: true,
133
- description: 'Route path pattern',
134
- },
135
- {
136
- name: 'handler',
137
- type: 'Function',
138
- required: true,
139
- description: 'Route handler function',
140
- },
141
- ],
142
- returnType: 'void',
143
- async: false,
144
- },
145
- {
146
- name: 'delete',
147
- description: 'Register DELETE route handler',
148
- parameters: [
149
- {
150
- name: 'path',
151
- type: 'string',
152
- required: true,
153
- description: 'Route path pattern',
154
- },
155
- {
156
- name: 'handler',
157
- type: 'Function',
158
- required: true,
159
- description: 'Route handler function',
160
- },
161
- ],
162
- returnType: 'void',
163
- async: false,
164
- },
165
- {
166
- name: 'listen',
167
- description: 'Start the HTTP server',
168
- parameters: [
169
- {
170
- name: 'port',
171
- type: 'number',
172
- required: true,
173
- description: 'Port number',
174
- },
175
- ],
176
- returnType: 'Promise<void>',
177
- async: true,
178
- },
179
- {
180
- name: 'close',
181
- description: 'Stop the HTTP server',
182
- parameters: [],
183
- returnType: 'void',
184
- async: false,
185
- },
186
- ],
187
- },
188
- ],
189
-
190
- // Dependencies on Other Plugins/Services
191
- requires: [
192
- {
193
- pluginId: 'com.objectstack.engine.objectql',
194
- version: '^0.6.0',
195
- optional: true,
196
- reason: 'ObjectStack Runtime Protocol implementation service',
197
- requiredCapabilities: [
198
- 'com.objectstack.protocol.runtime.v1',
199
- ],
200
- },
201
- ],
202
-
203
- // Extension Points This Plugin Defines
204
- extensionPoints: [
205
- {
206
- id: 'com.objectstack.server.hono.extension.middleware',
207
- name: 'HTTP Middleware',
208
- description: 'Register custom HTTP middleware',
209
- type: 'hook',
210
- cardinality: 'multiple',
211
- contract: {
212
- signature: '(req: Request, res: Response, next: Function) => void | Promise<void>',
213
- },
214
- },
215
- {
216
- id: 'com.objectstack.server.hono.extension.route',
217
- name: 'Custom Routes',
218
- description: 'Register custom API routes',
219
- type: 'action',
220
- cardinality: 'multiple',
221
- contract: {
222
- input: 'RouteDefinition',
223
- signature: '(app: HonoApp) => void',
224
- },
225
- },
226
- ],
227
-
228
- // No extensions contributed to other plugins
229
- extensions: [],
230
- },
231
-
232
- contributes: {
233
- // System Events
234
- events: [
235
- 'kernel:ready',
236
- ],
237
- },
238
- };
239
-
240
- export default HonoServerPlugin;
package/src/adapter.ts DELETED
@@ -1,228 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- // Export IHttpServer from core
4
- export * from '@objectstack/core';
5
-
6
- import {
7
- IHttpServer,
8
- RouteHandler,
9
- Middleware
10
- } from '@objectstack/core';
11
- import { Hono } from 'hono';
12
- import { serve } from '@hono/node-server';
13
- import { serveStatic } from '@hono/node-server/serve-static';
14
-
15
- export interface HonoCorsOptions {
16
- enabled?: boolean;
17
- origins?: string | string[];
18
- methods?: string[];
19
- credentials?: boolean;
20
- maxAge?: number;
21
- }
22
-
23
- /**
24
- * Hono Implementation of IHttpServer
25
- */
26
- export class HonoHttpServer implements IHttpServer {
27
- private app: Hono;
28
- private server: any;
29
- private listeningPort: number | undefined;
30
-
31
- constructor(
32
- private port: number = 3000,
33
- private staticRoot?: string
34
- ) {
35
- this.app = new Hono();
36
- }
37
-
38
- // internal helper to convert standard handler to Hono handler
39
- private wrap(handler: RouteHandler) {
40
- return async (c: any) => {
41
- let body: any = {};
42
-
43
- // Try to parse JSON body first if content-type is JSON
44
- if (c.req.header('content-type')?.includes('application/json')) {
45
- try {
46
- body = await c.req.json();
47
- } catch(e) {
48
- // If JSON parsing fails, try parseBody
49
- try {
50
- body = await c.req.parseBody();
51
- } catch(e2) {}
52
- }
53
- } else {
54
- // For non-JSON content types, use parseBody
55
- try {
56
- body = await c.req.parseBody();
57
- } catch(e) {}
58
- }
59
-
60
- const req = {
61
- params: c.req.param(),
62
- query: c.req.query(),
63
- body,
64
- headers: c.req.header(),
65
- method: c.req.method,
66
- path: c.req.path
67
- };
68
-
69
- let capturedResponse: any;
70
- let streamController: ReadableStreamDefaultController | null = null;
71
- let streamEncoder: TextEncoder | null = null;
72
- let streamHeaders: Record<string, string> = {};
73
- let isStreaming = false;
74
-
75
- const res = {
76
- json: (data: any) => { capturedResponse = c.json(data); },
77
- send: (data: string) => { capturedResponse = c.html(data); },
78
- status: (code: number) => { c.status(code); return res; },
79
- header: (name: string, value: string) => {
80
- c.header(name, value);
81
- streamHeaders[name] = value;
82
- return res;
83
- },
84
- write: (chunk: string | Uint8Array) => {
85
- isStreaming = true;
86
- if (streamController && streamEncoder) {
87
- const data = typeof chunk === 'string' ? streamEncoder.encode(chunk) : chunk;
88
- streamController.enqueue(data);
89
- }
90
- },
91
- end: () => {
92
- if (streamController) {
93
- streamController.close();
94
- }
95
- },
96
- };
97
-
98
- // Create a streaming response wrapper — if handler calls res.write(),
99
- // we return a ReadableStream; otherwise fall back to capturedResponse.
100
- const streamPromise = new Promise<Response | null>((resolve) => {
101
- const stream = new ReadableStream({
102
- start(controller) {
103
- streamController = controller;
104
- streamEncoder = new TextEncoder();
105
- },
106
- });
107
-
108
- // Run the handler; once it's done, check if streaming was used
109
- const result = handler(req as any, res as any);
110
- const done = result instanceof Promise ? result : Promise.resolve(result);
111
- done.then(() => {
112
- if (isStreaming) {
113
- resolve(new Response(stream, {
114
- status: 200,
115
- headers: streamHeaders,
116
- }));
117
- } else {
118
- // Not streaming — close the unused stream and return null
119
- streamController?.close();
120
- resolve(null);
121
- }
122
- }).catch(() => {
123
- streamController?.close();
124
- resolve(null);
125
- });
126
- });
127
-
128
- const streamResponse = await streamPromise;
129
- return streamResponse ?? capturedResponse;
130
- };
131
- }
132
-
133
- get(path: string, handler: RouteHandler) {
134
- this.app.get(path, this.wrap(handler));
135
- }
136
- post(path: string, handler: RouteHandler) {
137
- this.app.post(path, this.wrap(handler));
138
- }
139
- put(path: string, handler: RouteHandler) {
140
- this.app.put(path, this.wrap(handler));
141
- }
142
- delete(path: string, handler: RouteHandler) {
143
- this.app.delete(path, this.wrap(handler));
144
- }
145
- patch(path: string, handler: RouteHandler) {
146
- this.app.patch(path, this.wrap(handler));
147
- }
148
-
149
- use(pathOrHandler: string | Middleware, handler?: Middleware) {
150
- if (typeof pathOrHandler === 'string' && handler) {
151
- // Path based middleware
152
- // Hono middleware signature is different (c, next) => ...
153
- this.app.use(pathOrHandler, async (c, next) => {
154
- // Simplistic conversion
155
- await handler({} as any, {} as any, next);
156
- });
157
- } else if (typeof pathOrHandler === 'function') {
158
- // Global middleware
159
- this.app.use('*', async (c, next) => {
160
- await pathOrHandler({} as any, {} as any, next);
161
- });
162
- }
163
- }
164
-
165
- /**
166
- * Mount a sub-application or router
167
- */
168
- mount(path: string, subApp: Hono) {
169
- this.app.route(path, subApp);
170
- }
171
-
172
-
173
- async listen(port: number) {
174
- if (this.staticRoot) {
175
- this.app.get('/*', serveStatic({ root: this.staticRoot }));
176
- }
177
-
178
- const targetPort = port || this.port;
179
- const maxRetries = 20;
180
-
181
- for (let attempt = 0; attempt < maxRetries; attempt++) {
182
- const tryPort = targetPort + attempt;
183
- try {
184
- await this.tryListen(tryPort);
185
- return;
186
- } catch (err: any) {
187
- if (err.code === 'EADDRINUSE' && attempt < maxRetries - 1) {
188
- if (this.server && typeof this.server.close === 'function') {
189
- this.server.close();
190
- }
191
- continue;
192
- }
193
- throw err;
194
- }
195
- }
196
- }
197
-
198
- private tryListen(port: number): Promise<void> {
199
- return new Promise<void>((resolve, reject) => {
200
- const server = serve({
201
- fetch: this.app.fetch,
202
- port
203
- }, (info) => {
204
- this.listeningPort = info.port;
205
- resolve();
206
- });
207
- this.server = server;
208
- server.on('error', (err: any) => {
209
- reject(err);
210
- });
211
- });
212
- }
213
-
214
- getPort() {
215
- return this.listeningPort || this.port;
216
- }
217
-
218
- // Expose raw app for scenarios where standard interface is not enough
219
- getRawApp() {
220
- return this.app;
221
- }
222
-
223
- async close() {
224
- if (this.server && typeof this.server.close === 'function') {
225
- this.server.close();
226
- }
227
- }
228
- }
@@ -1,236 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { HonoServerPlugin } from './hono-plugin';
3
- import { PluginContext } from '@objectstack/core';
4
- import { HonoHttpServer } from './adapter';
5
-
6
- vi.mock('fs', async (importOriginal) => {
7
- const actual = await importOriginal<typeof import('fs')>();
8
- return {
9
- ...actual,
10
- existsSync: vi.fn().mockReturnValue(true)
11
- };
12
- });
13
-
14
- vi.mock('@hono/node-server/serve-static', () => ({
15
- serveStatic: vi.fn(() => (c: any, next: any) => next())
16
- }));
17
-
18
- vi.mock('./adapter', () => ({
19
- HonoHttpServer: vi.fn(function() {
20
- return {
21
- mount: vi.fn(),
22
- start: vi.fn(),
23
- stop: vi.fn(),
24
- getApp: vi.fn(),
25
- listen: vi.fn(),
26
- getPort: vi.fn().mockReturnValue(3000),
27
- close: vi.fn(),
28
- getRawApp: vi.fn().mockReturnValue({
29
- get: vi.fn(),
30
- use: vi.fn(),
31
- })
32
- };
33
- })
34
- }));
35
-
36
- describe('HonoServerPlugin', () => {
37
- let context: any;
38
- let logger: any;
39
- let kernel: any;
40
-
41
- beforeEach(() => {
42
- vi.clearAllMocks();
43
-
44
- logger = {
45
- info: vi.fn(),
46
- debug: vi.fn(),
47
- warn: vi.fn(),
48
- error: vi.fn()
49
- };
50
-
51
- kernel = {
52
- getService: vi.fn(),
53
- };
54
-
55
- context = {
56
- logger,
57
- getKernel: vi.fn().mockReturnValue(kernel),
58
- registerService: vi.fn(),
59
- hook: vi.fn(),
60
- getService: vi.fn()
61
- };
62
- });
63
-
64
- it('should initialize and register server', async () => {
65
- const plugin = new HonoServerPlugin();
66
- await plugin.init(context as PluginContext);
67
-
68
- expect(context.registerService).toHaveBeenCalledWith('http-server', expect.any(Object));
69
- expect(HonoHttpServer).toHaveBeenCalled();
70
- });
71
-
72
- it('should register IHttpServer service on init', async () => {
73
- const plugin = new HonoServerPlugin();
74
- await plugin.init(context as PluginContext);
75
-
76
- expect(context.registerService).toHaveBeenCalledWith('http.server', expect.any(Object));
77
- expect(context.registerService).toHaveBeenCalledWith('http-server', expect.any(Object));
78
- });
79
-
80
- it('should start without errors', async () => {
81
- const plugin = new HonoServerPlugin();
82
- await plugin.init(context as PluginContext);
83
- await plugin.start(context as PluginContext);
84
-
85
- // Plugin should register kernel:ready hook to start listening
86
- expect(context.hook).toHaveBeenCalledWith('kernel:ready', expect.any(Function));
87
- });
88
-
89
- it('should handle errors gracefully on start', async () => {
90
- // Simulate a start that doesn't crash even without routes
91
- const plugin = new HonoServerPlugin();
92
- await plugin.init(context as PluginContext);
93
- await expect(plugin.start(context as PluginContext)).resolves.not.toThrow();
94
- });
95
-
96
- it('should configure static files and SPA fallback when enabled', async () => {
97
- const plugin = new HonoServerPlugin({
98
- staticRoot: './public',
99
- spaFallback: true
100
- });
101
-
102
- await plugin.init(context as PluginContext);
103
- await plugin.start(context as PluginContext);
104
-
105
- const serverInstance = (HonoHttpServer as any).mock.instances[0];
106
- const rawApp = serverInstance.getRawApp();
107
-
108
- expect(serverInstance.getRawApp).toHaveBeenCalled();
109
- // Should register static files middleware
110
- expect(rawApp.get).toHaveBeenCalledWith('/*', expect.anything());
111
- // Should register SPA fallback middleware
112
- expect(rawApp.get).toHaveBeenCalledWith('/*', expect.anything());
113
- });
114
-
115
- describe('CORS wildcard pattern matching', () => {
116
- beforeEach(() => {
117
- vi.clearAllMocks();
118
- });
119
-
120
- it('should enable CORS middleware with wildcard subdomain patterns', async () => {
121
- const plugin = new HonoServerPlugin({
122
- cors: {
123
- origins: ['https://*.objectui.org', 'https://*.objectstack.ai'],
124
- credentials: true
125
- }
126
- });
127
-
128
- await plugin.init(context as PluginContext);
129
-
130
- const serverInstance = (HonoHttpServer as any).mock.instances[0];
131
- const rawApp = serverInstance.getRawApp();
132
-
133
- // CORS middleware should be registered
134
- expect(rawApp.use).toHaveBeenCalledWith('*', expect.any(Function));
135
- });
136
-
137
- it('should enable CORS middleware with port wildcard patterns', async () => {
138
- const plugin = new HonoServerPlugin({
139
- cors: {
140
- origins: 'http://localhost:*',
141
- }
142
- });
143
-
144
- await plugin.init(context as PluginContext);
145
-
146
- const serverInstance = (HonoHttpServer as any).mock.instances[0];
147
- const rawApp = serverInstance.getRawApp();
148
-
149
- expect(rawApp.use).toHaveBeenCalledWith('*', expect.any(Function));
150
- });
151
-
152
- it('should support comma-separated wildcard patterns', async () => {
153
- const plugin = new HonoServerPlugin({
154
- cors: {
155
- origins: 'https://*.objectui.org,https://*.objectstack.ai',
156
- }
157
- });
158
-
159
- await plugin.init(context as PluginContext);
160
-
161
- const serverInstance = (HonoHttpServer as any).mock.instances[0];
162
- const rawApp = serverInstance.getRawApp();
163
-
164
- expect(rawApp.use).toHaveBeenCalledWith('*', expect.any(Function));
165
- });
166
-
167
- it('should support exact origins without wildcards', async () => {
168
- const plugin = new HonoServerPlugin({
169
- cors: {
170
- origins: ['https://app.example.com', 'https://api.example.com'],
171
- }
172
- });
173
-
174
- await plugin.init(context as PluginContext);
175
-
176
- const serverInstance = (HonoHttpServer as any).mock.instances[0];
177
- const rawApp = serverInstance.getRawApp();
178
-
179
- expect(rawApp.use).toHaveBeenCalledWith('*', expect.any(Function));
180
- });
181
-
182
- it('should support CORS_ORIGIN environment variable with wildcards', async () => {
183
- const originalEnv = process.env.CORS_ORIGIN;
184
- process.env.CORS_ORIGIN = 'https://*.objectui.org,https://*.objectstack.ai';
185
-
186
- const plugin = new HonoServerPlugin();
187
- await plugin.init(context as PluginContext);
188
-
189
- const serverInstance = (HonoHttpServer as any).mock.instances[0];
190
- const rawApp = serverInstance.getRawApp();
191
-
192
- expect(rawApp.use).toHaveBeenCalledWith('*', expect.any(Function));
193
-
194
- // Restore environment
195
- if (originalEnv !== undefined) {
196
- process.env.CORS_ORIGIN = originalEnv;
197
- } else {
198
- delete process.env.CORS_ORIGIN;
199
- }
200
- });
201
-
202
- it('should disable CORS when cors option is false', async () => {
203
- const plugin = new HonoServerPlugin({
204
- cors: false
205
- });
206
-
207
- await plugin.init(context as PluginContext);
208
-
209
- const serverInstance = (HonoHttpServer as any).mock.instances[0];
210
- const rawApp = serverInstance.getRawApp();
211
-
212
- // CORS middleware should NOT be registered
213
- expect(rawApp.use).not.toHaveBeenCalled();
214
- });
215
-
216
- it('should disable CORS when CORS_ENABLED env is false', async () => {
217
- const originalEnv = process.env.CORS_ENABLED;
218
- process.env.CORS_ENABLED = 'false';
219
-
220
- const plugin = new HonoServerPlugin();
221
- await plugin.init(context as PluginContext);
222
-
223
- const serverInstance = (HonoHttpServer as any).mock.instances[0];
224
- const rawApp = serverInstance.getRawApp();
225
-
226
- expect(rawApp.use).not.toHaveBeenCalled();
227
-
228
- // Restore environment
229
- if (originalEnv !== undefined) {
230
- process.env.CORS_ENABLED = originalEnv;
231
- } else {
232
- delete process.env.CORS_ENABLED;
233
- }
234
- });
235
- });
236
- });