@objectstack/hono 4.0.0 → 4.0.2

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,22 +1,22 @@
1
1
 
2
- > @objectstack/hono@4.0.0 build /home/runner/work/spec/spec/packages/adapters/hono
2
+ > @objectstack/hono@4.0.2 build /home/runner/work/framework/framework/packages/adapters/hono
3
3
  > tsup --config ../../../tsup.config.ts
4
4
 
5
5
  CLI Building entry: src/index.ts
6
6
  CLI Using tsconfig: tsconfig.json
7
7
  CLI tsup v8.5.1
8
- CLI Using tsup config: /home/runner/work/spec/spec/tsup.config.ts
8
+ CLI Using tsup config: /home/runner/work/framework/framework/tsup.config.ts
9
9
  CLI Target: es2020
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- CJS dist/index.js 5.40 KB
14
- CJS dist/index.js.map 9.92 KB
15
- CJS ⚡️ Build success in 34ms
16
- ESM dist/index.mjs 4.33 KB
17
- ESM dist/index.mjs.map 9.88 KB
18
- ESM ⚡️ Build success in 39ms
13
+ ESM dist/index.mjs 5.37 KB
14
+ ESM dist/index.mjs.map 11.82 KB
15
+ ESM ⚡️ Build success in 76ms
16
+ CJS dist/index.js 6.44 KB
17
+ CJS dist/index.js.map 11.87 KB
18
+ CJS ⚡️ Build success in 76ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 13818ms
20
+ DTS ⚡️ Build success in 13532ms
21
21
  DTS dist/index.d.mts 1.17 KB
22
22
  DTS dist/index.d.ts 1.17 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @objectstack/hono
2
2
 
3
+ ## 4.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 5f659e9: fix ai
8
+ - @objectstack/runtime@4.0.2
9
+
3
10
  ## 4.0.0
4
11
 
5
12
  ### Patch Changes
package/dist/index.js CHANGED
@@ -52,6 +52,31 @@ function createHonoApp(options) {
52
52
  if (res.type === "redirect" && res.url) {
53
53
  return c.redirect(res.url);
54
54
  }
55
+ if (res.type === "stream" && res.events) {
56
+ const headers = {
57
+ "Content-Type": res.contentType || "text/event-stream",
58
+ "Cache-Control": "no-cache",
59
+ "Connection": "keep-alive",
60
+ ...res.headers || {}
61
+ };
62
+ const stream = new ReadableStream({
63
+ async start(controller) {
64
+ try {
65
+ const encoder = new TextEncoder();
66
+ for await (const event of res.events) {
67
+ const chunk = res.vercelDataStream ? typeof event === "string" ? event : JSON.stringify(event) + "\n" : `data: ${JSON.stringify(event)}
68
+
69
+ `;
70
+ controller.enqueue(encoder.encode(chunk));
71
+ }
72
+ } catch (err) {
73
+ } finally {
74
+ controller.close();
75
+ }
76
+ }
77
+ });
78
+ return new Response(stream, { status: 200, headers });
79
+ }
55
80
  if (res.type === "stream" && res.stream) {
56
81
  if (res.headers) {
57
82
  Object.entries(res.headers).forEach(([k, v]) => c.header(k, v));
@@ -66,6 +91,9 @@ function createHonoApp(options) {
66
91
  app.get(prefix, async (c) => {
67
92
  return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
68
93
  });
94
+ app.get(`${prefix}/discovery`, async (c) => {
95
+ return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
96
+ });
69
97
  app.get("/.well-known/objectstack", (c) => {
70
98
  return c.redirect(prefix);
71
99
  });
@@ -134,7 +162,7 @@ function createHonoApp(options) {
134
162
  url.searchParams.forEach((val, key) => {
135
163
  queryParams[key] = val;
136
164
  });
137
- const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });
165
+ const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw }, prefix);
138
166
  return toResponse(c, result);
139
167
  } catch (err) {
140
168
  return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Hono } from 'hono';\nimport { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime';\n\nexport interface ObjectStackHonoOptions {\n kernel: ObjectKernel;\n prefix?: string;\n}\n\n/**\n * Auth service interface with handleRequest method\n */\ninterface AuthService {\n handleRequest(request: Request): Promise<Response>;\n}\n\n/**\n * Middleware mode for existing Hono apps\n */\nexport function objectStackMiddleware(kernel: ObjectKernel) {\n return async (c: any, next: any) => {\n c.set('objectStack', kernel);\n await next();\n };\n}\n\n/**\n * Creates a full-featured Hono app with all ObjectStack route dispatchers.\n *\n * Only routes that need framework-specific handling (auth service, storage\n * formData, GraphQL raw result, discovery wrapper) are registered explicitly.\n * All other routes (meta, data, packages, analytics, automation, i18n, ui,\n * openapi, custom endpoints, and any future routes) are handled by a\n * catch-all that delegates to `HttpDispatcher.dispatch()`.\n *\n * This means new routes added to `HttpDispatcher` automatically work in\n * every adapter without any adapter-side code changes.\n *\n * @example\n * ```ts\n * import { createHonoApp } from '@objectstack/hono';\n * const app = createHonoApp({ kernel });\n * export default app;\n * ```\n */\nexport function createHonoApp(options: ObjectStackHonoOptions): Hono {\n const app = new Hono();\n const prefix = options.prefix || '/api';\n const dispatcher = new HttpDispatcher(options.kernel);\n\n const errorJson = (c: any, message: string, code: number = 500) => {\n return c.json({ success: false, error: { message, code } }, code);\n };\n\n const toResponse = (c: any, result: HttpDispatcherResult) => {\n if (result.handled) {\n if (result.response) {\n if (result.response.headers) {\n Object.entries(result.response.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return c.json(result.response.body, result.response.status);\n }\n if (result.result) {\n const res = result.result;\n if (res.type === 'redirect' && res.url) {\n return c.redirect(res.url);\n }\n if (res.type === 'stream' && res.stream) {\n if (res.headers) {\n Object.entries(res.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return new Response(res.stream, { status: 200 });\n }\n return c.json(res, 200);\n }\n }\n return errorJson(c, 'Not Found', 404);\n };\n\n // ─── Explicit routes (framework-specific handling required) ────────────────\n\n // --- Discovery ---\n app.get(prefix, async (c) => {\n return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });\n });\n\n // --- .well-known ---\n app.get('/.well-known/objectstack', (c) => {\n return c.redirect(prefix);\n });\n\n // --- Auth (needs auth service integration) ---\n app.all(`${prefix}/auth/*`, async (c) => {\n try {\n const path = c.req.path.substring(`${prefix}/auth/`.length);\n const method = c.req.method;\n\n // Try AuthPlugin service first (prefer async to support factory-based services)\n let authService: AuthService | null = null;\n try {\n if (typeof options.kernel.getServiceAsync === 'function') {\n authService = await options.kernel.getServiceAsync<AuthService>('auth');\n } else if (typeof options.kernel.getService === 'function') {\n authService = options.kernel.getService<AuthService>('auth');\n }\n } catch {\n // Service not registered — fall through to dispatcher\n authService = null;\n }\n\n if (authService && typeof authService.handleRequest === 'function') {\n const response = await authService.handleRequest(c.req.raw);\n return new Response(response.body, {\n status: response.status,\n headers: response.headers,\n });\n }\n\n // Fallback to legacy dispatcher\n const body = method === 'GET' || method === 'HEAD'\n ? {}\n : await c.req.json().catch(() => ({}));\n const result = await dispatcher.handleAuth(path, method, body, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- GraphQL (returns raw result, not HttpDispatcherResult) ---\n app.post(`${prefix}/graphql`, async (c) => {\n try {\n const body = await c.req.json();\n const result = await dispatcher.handleGraphQL(body, { request: c.req.raw });\n return c.json(result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- Storage (needs formData parsing) ---\n app.all(`${prefix}/storage/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(`${prefix}/storage`.length);\n const method = c.req.method;\n\n let file: any = undefined;\n if (method === 'POST' && subPath === '/upload') {\n const formData = await c.req.formData();\n file = formData.get('file');\n }\n\n const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // ─── Catch-all: delegate to dispatcher.dispatch() ─────────────────────────\n // Handles meta, data, packages, analytics, automation, i18n, ui, openapi,\n // custom API endpoints, and any future routes added to HttpDispatcher.\n app.all(`${prefix}/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(prefix.length);\n const method = c.req.method;\n\n let body: any = undefined;\n if (method === 'POST' || method === 'PUT' || method === 'PATCH') {\n body = await c.req.json().catch(() => ({}));\n }\n\n const queryParams: Record<string, any> = {};\n const url = new URL(c.req.url);\n url.searchParams.forEach((val, key) => { queryParams[key] = val; });\n\n const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n return app;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Hono } from 'hono';\nimport { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime';\n\nexport interface ObjectStackHonoOptions {\n kernel: ObjectKernel;\n prefix?: string;\n}\n\n/**\n * Auth service interface with handleRequest method\n */\ninterface AuthService {\n handleRequest(request: Request): Promise<Response>;\n}\n\n/**\n * Middleware mode for existing Hono apps\n */\nexport function objectStackMiddleware(kernel: ObjectKernel) {\n return async (c: any, next: any) => {\n c.set('objectStack', kernel);\n await next();\n };\n}\n\n/**\n * Creates a full-featured Hono app with all ObjectStack route dispatchers.\n *\n * Only routes that need framework-specific handling (auth service, storage\n * formData, GraphQL raw result, discovery wrapper) are registered explicitly.\n * All other routes (meta, data, packages, analytics, automation, i18n, ui,\n * openapi, custom endpoints, and any future routes) are handled by a\n * catch-all that delegates to `HttpDispatcher.dispatch()`.\n *\n * This means new routes added to `HttpDispatcher` automatically work in\n * every adapter without any adapter-side code changes.\n *\n * @example\n * ```ts\n * import { createHonoApp } from '@objectstack/hono';\n * const app = createHonoApp({ kernel });\n * export default app;\n * ```\n */\nexport function createHonoApp(options: ObjectStackHonoOptions): Hono {\n const app = new Hono();\n const prefix = options.prefix || '/api';\n const dispatcher = new HttpDispatcher(options.kernel);\n\n const errorJson = (c: any, message: string, code: number = 500) => {\n return c.json({ success: false, error: { message, code } }, code);\n };\n\n const toResponse = (c: any, result: HttpDispatcherResult) => {\n if (result.handled) {\n if (result.response) {\n if (result.response.headers) {\n Object.entries(result.response.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return c.json(result.response.body, result.response.status);\n }\n if (result.result) {\n const res = result.result;\n if (res.type === 'redirect' && res.url) {\n return c.redirect(res.url);\n }\n if (res.type === 'stream' && res.events) {\n // SSE / Vercel Data Stream streaming response\n const headers: Record<string, string> = {\n 'Content-Type': res.contentType || 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n ...(res.headers || {}),\n };\n const stream = new ReadableStream({\n async start(controller) {\n try {\n const encoder = new TextEncoder();\n for await (const event of res.events) {\n const chunk = res.vercelDataStream\n ? (typeof event === 'string' ? event : JSON.stringify(event) + '\\n')\n : `data: ${JSON.stringify(event)}\\n\\n`;\n controller.enqueue(encoder.encode(chunk));\n }\n } catch (err) {\n // Stream error — close gracefully\n } finally {\n controller.close();\n }\n },\n });\n return new Response(stream, { status: 200, headers });\n }\n if (res.type === 'stream' && res.stream) {\n if (res.headers) {\n Object.entries(res.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return new Response(res.stream, { status: 200 });\n }\n return c.json(res, 200);\n }\n }\n return errorJson(c, 'Not Found', 404);\n };\n\n // ─── Explicit routes (framework-specific handling required) ────────────────\n\n // --- Discovery ---\n app.get(prefix, async (c) => {\n return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });\n });\n\n app.get(`${prefix}/discovery`, async (c) => {\n return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });\n });\n\n // --- .well-known ---\n app.get('/.well-known/objectstack', (c) => {\n return c.redirect(prefix);\n });\n\n // --- Auth (needs auth service integration) ---\n app.all(`${prefix}/auth/*`, async (c) => {\n try {\n const path = c.req.path.substring(`${prefix}/auth/`.length);\n const method = c.req.method;\n\n // Try AuthPlugin service first (prefer async to support factory-based services)\n let authService: AuthService | null = null;\n try {\n if (typeof options.kernel.getServiceAsync === 'function') {\n authService = await options.kernel.getServiceAsync<AuthService>('auth');\n } else if (typeof options.kernel.getService === 'function') {\n authService = options.kernel.getService<AuthService>('auth');\n }\n } catch {\n // Service not registered — fall through to dispatcher\n authService = null;\n }\n\n if (authService && typeof authService.handleRequest === 'function') {\n const response = await authService.handleRequest(c.req.raw);\n return new Response(response.body, {\n status: response.status,\n headers: response.headers,\n });\n }\n\n // Fallback to legacy dispatcher\n const body = method === 'GET' || method === 'HEAD'\n ? {}\n : await c.req.json().catch(() => ({}));\n const result = await dispatcher.handleAuth(path, method, body, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- GraphQL (returns raw result, not HttpDispatcherResult) ---\n app.post(`${prefix}/graphql`, async (c) => {\n try {\n const body = await c.req.json();\n const result = await dispatcher.handleGraphQL(body, { request: c.req.raw });\n return c.json(result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- Storage (needs formData parsing) ---\n app.all(`${prefix}/storage/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(`${prefix}/storage`.length);\n const method = c.req.method;\n\n let file: any = undefined;\n if (method === 'POST' && subPath === '/upload') {\n const formData = await c.req.formData();\n file = formData.get('file');\n }\n\n const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // ─── Catch-all: delegate to dispatcher.dispatch() ─────────────────────────\n // Handles meta, data, packages, analytics, automation, i18n, ui, openapi,\n // custom API endpoints, and any future routes added to HttpDispatcher.\n app.all(`${prefix}/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(prefix.length);\n const method = c.req.method;\n\n let body: any = undefined;\n if (method === 'POST' || method === 'PUT' || method === 'PATCH') {\n body = await c.req.json().catch(() => ({}));\n }\n\n const queryParams: Record<string, any> = {};\n const url = new URL(c.req.url);\n url.searchParams.forEach((val, key) => { queryParams[key] = val; });\n\n const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw }, prefix);\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n return app;\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -27,6 +27,31 @@ function createHonoApp(options) {
27
27
  if (res.type === "redirect" && res.url) {
28
28
  return c.redirect(res.url);
29
29
  }
30
+ if (res.type === "stream" && res.events) {
31
+ const headers = {
32
+ "Content-Type": res.contentType || "text/event-stream",
33
+ "Cache-Control": "no-cache",
34
+ "Connection": "keep-alive",
35
+ ...res.headers || {}
36
+ };
37
+ const stream = new ReadableStream({
38
+ async start(controller) {
39
+ try {
40
+ const encoder = new TextEncoder();
41
+ for await (const event of res.events) {
42
+ const chunk = res.vercelDataStream ? typeof event === "string" ? event : JSON.stringify(event) + "\n" : `data: ${JSON.stringify(event)}
43
+
44
+ `;
45
+ controller.enqueue(encoder.encode(chunk));
46
+ }
47
+ } catch (err) {
48
+ } finally {
49
+ controller.close();
50
+ }
51
+ }
52
+ });
53
+ return new Response(stream, { status: 200, headers });
54
+ }
30
55
  if (res.type === "stream" && res.stream) {
31
56
  if (res.headers) {
32
57
  Object.entries(res.headers).forEach(([k, v]) => c.header(k, v));
@@ -41,6 +66,9 @@ function createHonoApp(options) {
41
66
  app.get(prefix, async (c) => {
42
67
  return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
43
68
  });
69
+ app.get(`${prefix}/discovery`, async (c) => {
70
+ return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
71
+ });
44
72
  app.get("/.well-known/objectstack", (c) => {
45
73
  return c.redirect(prefix);
46
74
  });
@@ -109,7 +137,7 @@ function createHonoApp(options) {
109
137
  url.searchParams.forEach((val, key) => {
110
138
  queryParams[key] = val;
111
139
  });
112
- const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });
140
+ const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw }, prefix);
113
141
  return toResponse(c, result);
114
142
  } catch (err) {
115
143
  return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Hono } from 'hono';\nimport { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime';\n\nexport interface ObjectStackHonoOptions {\n kernel: ObjectKernel;\n prefix?: string;\n}\n\n/**\n * Auth service interface with handleRequest method\n */\ninterface AuthService {\n handleRequest(request: Request): Promise<Response>;\n}\n\n/**\n * Middleware mode for existing Hono apps\n */\nexport function objectStackMiddleware(kernel: ObjectKernel) {\n return async (c: any, next: any) => {\n c.set('objectStack', kernel);\n await next();\n };\n}\n\n/**\n * Creates a full-featured Hono app with all ObjectStack route dispatchers.\n *\n * Only routes that need framework-specific handling (auth service, storage\n * formData, GraphQL raw result, discovery wrapper) are registered explicitly.\n * All other routes (meta, data, packages, analytics, automation, i18n, ui,\n * openapi, custom endpoints, and any future routes) are handled by a\n * catch-all that delegates to `HttpDispatcher.dispatch()`.\n *\n * This means new routes added to `HttpDispatcher` automatically work in\n * every adapter without any adapter-side code changes.\n *\n * @example\n * ```ts\n * import { createHonoApp } from '@objectstack/hono';\n * const app = createHonoApp({ kernel });\n * export default app;\n * ```\n */\nexport function createHonoApp(options: ObjectStackHonoOptions): Hono {\n const app = new Hono();\n const prefix = options.prefix || '/api';\n const dispatcher = new HttpDispatcher(options.kernel);\n\n const errorJson = (c: any, message: string, code: number = 500) => {\n return c.json({ success: false, error: { message, code } }, code);\n };\n\n const toResponse = (c: any, result: HttpDispatcherResult) => {\n if (result.handled) {\n if (result.response) {\n if (result.response.headers) {\n Object.entries(result.response.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return c.json(result.response.body, result.response.status);\n }\n if (result.result) {\n const res = result.result;\n if (res.type === 'redirect' && res.url) {\n return c.redirect(res.url);\n }\n if (res.type === 'stream' && res.stream) {\n if (res.headers) {\n Object.entries(res.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return new Response(res.stream, { status: 200 });\n }\n return c.json(res, 200);\n }\n }\n return errorJson(c, 'Not Found', 404);\n };\n\n // ─── Explicit routes (framework-specific handling required) ────────────────\n\n // --- Discovery ---\n app.get(prefix, async (c) => {\n return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });\n });\n\n // --- .well-known ---\n app.get('/.well-known/objectstack', (c) => {\n return c.redirect(prefix);\n });\n\n // --- Auth (needs auth service integration) ---\n app.all(`${prefix}/auth/*`, async (c) => {\n try {\n const path = c.req.path.substring(`${prefix}/auth/`.length);\n const method = c.req.method;\n\n // Try AuthPlugin service first (prefer async to support factory-based services)\n let authService: AuthService | null = null;\n try {\n if (typeof options.kernel.getServiceAsync === 'function') {\n authService = await options.kernel.getServiceAsync<AuthService>('auth');\n } else if (typeof options.kernel.getService === 'function') {\n authService = options.kernel.getService<AuthService>('auth');\n }\n } catch {\n // Service not registered — fall through to dispatcher\n authService = null;\n }\n\n if (authService && typeof authService.handleRequest === 'function') {\n const response = await authService.handleRequest(c.req.raw);\n return new Response(response.body, {\n status: response.status,\n headers: response.headers,\n });\n }\n\n // Fallback to legacy dispatcher\n const body = method === 'GET' || method === 'HEAD'\n ? {}\n : await c.req.json().catch(() => ({}));\n const result = await dispatcher.handleAuth(path, method, body, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- GraphQL (returns raw result, not HttpDispatcherResult) ---\n app.post(`${prefix}/graphql`, async (c) => {\n try {\n const body = await c.req.json();\n const result = await dispatcher.handleGraphQL(body, { request: c.req.raw });\n return c.json(result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- Storage (needs formData parsing) ---\n app.all(`${prefix}/storage/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(`${prefix}/storage`.length);\n const method = c.req.method;\n\n let file: any = undefined;\n if (method === 'POST' && subPath === '/upload') {\n const formData = await c.req.formData();\n file = formData.get('file');\n }\n\n const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // ─── Catch-all: delegate to dispatcher.dispatch() ─────────────────────────\n // Handles meta, data, packages, analytics, automation, i18n, ui, openapi,\n // custom API endpoints, and any future routes added to HttpDispatcher.\n app.all(`${prefix}/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(prefix.length);\n const method = c.req.method;\n\n let body: any = undefined;\n if (method === 'POST' || method === 'PUT' || method === 'PATCH') {\n body = await c.req.json().catch(() => ({}));\n }\n\n const queryParams: Record<string, any> = {};\n const url = new URL(c.req.url);\n url.searchParams.forEach((val, key) => { queryParams[key] = val; });\n\n const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n return app;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Hono } from 'hono';\nimport { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime';\n\nexport interface ObjectStackHonoOptions {\n kernel: ObjectKernel;\n prefix?: string;\n}\n\n/**\n * Auth service interface with handleRequest method\n */\ninterface AuthService {\n handleRequest(request: Request): Promise<Response>;\n}\n\n/**\n * Middleware mode for existing Hono apps\n */\nexport function objectStackMiddleware(kernel: ObjectKernel) {\n return async (c: any, next: any) => {\n c.set('objectStack', kernel);\n await next();\n };\n}\n\n/**\n * Creates a full-featured Hono app with all ObjectStack route dispatchers.\n *\n * Only routes that need framework-specific handling (auth service, storage\n * formData, GraphQL raw result, discovery wrapper) are registered explicitly.\n * All other routes (meta, data, packages, analytics, automation, i18n, ui,\n * openapi, custom endpoints, and any future routes) are handled by a\n * catch-all that delegates to `HttpDispatcher.dispatch()`.\n *\n * This means new routes added to `HttpDispatcher` automatically work in\n * every adapter without any adapter-side code changes.\n *\n * @example\n * ```ts\n * import { createHonoApp } from '@objectstack/hono';\n * const app = createHonoApp({ kernel });\n * export default app;\n * ```\n */\nexport function createHonoApp(options: ObjectStackHonoOptions): Hono {\n const app = new Hono();\n const prefix = options.prefix || '/api';\n const dispatcher = new HttpDispatcher(options.kernel);\n\n const errorJson = (c: any, message: string, code: number = 500) => {\n return c.json({ success: false, error: { message, code } }, code);\n };\n\n const toResponse = (c: any, result: HttpDispatcherResult) => {\n if (result.handled) {\n if (result.response) {\n if (result.response.headers) {\n Object.entries(result.response.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return c.json(result.response.body, result.response.status);\n }\n if (result.result) {\n const res = result.result;\n if (res.type === 'redirect' && res.url) {\n return c.redirect(res.url);\n }\n if (res.type === 'stream' && res.events) {\n // SSE / Vercel Data Stream streaming response\n const headers: Record<string, string> = {\n 'Content-Type': res.contentType || 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n ...(res.headers || {}),\n };\n const stream = new ReadableStream({\n async start(controller) {\n try {\n const encoder = new TextEncoder();\n for await (const event of res.events) {\n const chunk = res.vercelDataStream\n ? (typeof event === 'string' ? event : JSON.stringify(event) + '\\n')\n : `data: ${JSON.stringify(event)}\\n\\n`;\n controller.enqueue(encoder.encode(chunk));\n }\n } catch (err) {\n // Stream error — close gracefully\n } finally {\n controller.close();\n }\n },\n });\n return new Response(stream, { status: 200, headers });\n }\n if (res.type === 'stream' && res.stream) {\n if (res.headers) {\n Object.entries(res.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return new Response(res.stream, { status: 200 });\n }\n return c.json(res, 200);\n }\n }\n return errorJson(c, 'Not Found', 404);\n };\n\n // ─── Explicit routes (framework-specific handling required) ────────────────\n\n // --- Discovery ---\n app.get(prefix, async (c) => {\n return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });\n });\n\n app.get(`${prefix}/discovery`, async (c) => {\n return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });\n });\n\n // --- .well-known ---\n app.get('/.well-known/objectstack', (c) => {\n return c.redirect(prefix);\n });\n\n // --- Auth (needs auth service integration) ---\n app.all(`${prefix}/auth/*`, async (c) => {\n try {\n const path = c.req.path.substring(`${prefix}/auth/`.length);\n const method = c.req.method;\n\n // Try AuthPlugin service first (prefer async to support factory-based services)\n let authService: AuthService | null = null;\n try {\n if (typeof options.kernel.getServiceAsync === 'function') {\n authService = await options.kernel.getServiceAsync<AuthService>('auth');\n } else if (typeof options.kernel.getService === 'function') {\n authService = options.kernel.getService<AuthService>('auth');\n }\n } catch {\n // Service not registered — fall through to dispatcher\n authService = null;\n }\n\n if (authService && typeof authService.handleRequest === 'function') {\n const response = await authService.handleRequest(c.req.raw);\n return new Response(response.body, {\n status: response.status,\n headers: response.headers,\n });\n }\n\n // Fallback to legacy dispatcher\n const body = method === 'GET' || method === 'HEAD'\n ? {}\n : await c.req.json().catch(() => ({}));\n const result = await dispatcher.handleAuth(path, method, body, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- GraphQL (returns raw result, not HttpDispatcherResult) ---\n app.post(`${prefix}/graphql`, async (c) => {\n try {\n const body = await c.req.json();\n const result = await dispatcher.handleGraphQL(body, { request: c.req.raw });\n return c.json(result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- Storage (needs formData parsing) ---\n app.all(`${prefix}/storage/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(`${prefix}/storage`.length);\n const method = c.req.method;\n\n let file: any = undefined;\n if (method === 'POST' && subPath === '/upload') {\n const formData = await c.req.formData();\n file = formData.get('file');\n }\n\n const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // ─── Catch-all: delegate to dispatcher.dispatch() ─────────────────────────\n // Handles meta, data, packages, analytics, automation, i18n, ui, openapi,\n // custom API endpoints, and any future routes added to HttpDispatcher.\n app.all(`${prefix}/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(prefix.length);\n const method = c.req.method;\n\n let body: any = undefined;\n if (method === 'POST' || method === 'PUT' || method === 'PATCH') {\n body = await c.req.json().catch(() => ({}));\n }\n\n const queryParams: Record<string, any> = {};\n const url = new URL(c.req.url);\n url.searchParams.forEach((val, key) => { queryParams[key] = val; });\n\n const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw }, prefix);\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n return app;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/hono",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -13,13 +13,13 @@
13
13
  },
14
14
  "peerDependencies": {
15
15
  "hono": "^4.12.8",
16
- "@objectstack/runtime": "^4.0.0"
16
+ "@objectstack/runtime": "^4.0.2"
17
17
  },
18
18
  "devDependencies": {
19
- "hono": "^4.12.9",
19
+ "hono": "^4.12.10",
20
20
  "typescript": "^6.0.2",
21
21
  "vitest": "^4.1.2",
22
- "@objectstack/runtime": "4.0.0"
22
+ "@objectstack/runtime": "4.0.2"
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsup --config ../../../tsup.config.ts",
package/src/hono.test.ts CHANGED
@@ -97,6 +97,14 @@ describe('createHonoApp', () => {
97
97
  expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api');
98
98
  });
99
99
 
100
+ it('GET /api/discovery returns discovery info with correct prefix', async () => {
101
+ const res = await app.request('/api/discovery');
102
+ expect(res.status).toBe(200);
103
+ const json = await res.json();
104
+ expect(json.data).toBeDefined();
105
+ expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api');
106
+ });
107
+
100
108
  it('uses custom prefix for discovery', async () => {
101
109
  const customApp = createHonoApp({ kernel: mockKernel, prefix: '/v2' });
102
110
  const res = await customApp.request('/v2');
@@ -105,6 +113,15 @@ describe('createHonoApp', () => {
105
113
  expect(json.data).toBeDefined();
106
114
  expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2');
107
115
  });
116
+
117
+ it('uses custom prefix for /discovery route', async () => {
118
+ const customApp = createHonoApp({ kernel: mockKernel, prefix: '/v2' });
119
+ const res = await customApp.request('/v2/discovery');
120
+ expect(res.status).toBe(200);
121
+ const json = await res.json();
122
+ expect(json.data).toBeDefined();
123
+ expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2');
124
+ });
108
125
  });
109
126
 
110
127
  describe('.well-known Endpoint', () => {
@@ -277,6 +294,7 @@ describe('createHonoApp', () => {
277
294
  undefined,
278
295
  expect.any(Object),
279
296
  expect.objectContaining({ request: expect.anything() }),
297
+ '/api',
280
298
  );
281
299
  });
282
300
 
@@ -294,6 +312,7 @@ describe('createHonoApp', () => {
294
312
  body,
295
313
  expect.any(Object),
296
314
  expect.objectContaining({ request: expect.anything() }),
315
+ '/api',
297
316
  );
298
317
  });
299
318
 
@@ -306,6 +325,7 @@ describe('createHonoApp', () => {
306
325
  undefined,
307
326
  expect.any(Object),
308
327
  expect.objectContaining({ request: expect.anything() }),
328
+ '/api',
309
329
  );
310
330
  });
311
331
 
@@ -318,6 +338,7 @@ describe('createHonoApp', () => {
318
338
  undefined,
319
339
  expect.objectContaining({ package: 'com.acme.crm' }),
320
340
  expect.objectContaining({ request: expect.anything() }),
341
+ '/api',
321
342
  );
322
343
  });
323
344
 
@@ -330,6 +351,7 @@ describe('createHonoApp', () => {
330
351
  undefined,
331
352
  expect.any(Object),
332
353
  expect.objectContaining({ request: expect.anything() }),
354
+ '/api',
333
355
  );
334
356
  });
335
357
 
@@ -347,6 +369,7 @@ describe('createHonoApp', () => {
347
369
  body,
348
370
  expect.any(Object),
349
371
  expect.objectContaining({ request: expect.anything() }),
372
+ '/api',
350
373
  );
351
374
  });
352
375
 
@@ -364,6 +387,7 @@ describe('createHonoApp', () => {
364
387
  body,
365
388
  expect.any(Object),
366
389
  expect.objectContaining({ request: expect.anything() }),
390
+ '/api',
367
391
  );
368
392
  });
369
393
 
@@ -384,6 +408,7 @@ describe('createHonoApp', () => {
384
408
  undefined,
385
409
  expect.any(Object),
386
410
  expect.objectContaining({ request: expect.anything() }),
411
+ '/api',
387
412
  );
388
413
  });
389
414
 
@@ -396,6 +421,7 @@ describe('createHonoApp', () => {
396
421
  undefined,
397
422
  expect.any(Object),
398
423
  expect.objectContaining({ request: expect.anything() }),
424
+ '/api',
399
425
  );
400
426
  });
401
427
 
@@ -413,6 +439,7 @@ describe('createHonoApp', () => {
413
439
  body,
414
440
  expect.any(Object),
415
441
  expect.objectContaining({ request: expect.anything() }),
442
+ '/api',
416
443
  );
417
444
  });
418
445
 
@@ -425,6 +452,7 @@ describe('createHonoApp', () => {
425
452
  undefined,
426
453
  expect.objectContaining({ status: 'active' }),
427
454
  expect.objectContaining({ request: expect.anything() }),
455
+ '/api',
428
456
  );
429
457
  });
430
458
 
@@ -446,6 +474,7 @@ describe('createHonoApp', () => {
446
474
  undefined,
447
475
  expect.any(Object),
448
476
  expect.objectContaining({ request: expect.anything() }),
477
+ '/api',
449
478
  );
450
479
  });
451
480
 
@@ -458,6 +487,7 @@ describe('createHonoApp', () => {
458
487
  undefined,
459
488
  expect.any(Object),
460
489
  expect.objectContaining({ request: expect.anything() }),
490
+ '/api',
461
491
  );
462
492
  });
463
493
 
@@ -470,6 +500,7 @@ describe('createHonoApp', () => {
470
500
  undefined,
471
501
  expect.any(Object),
472
502
  expect.objectContaining({ request: expect.anything() }),
503
+ '/api',
473
504
  );
474
505
  });
475
506
 
@@ -482,6 +513,7 @@ describe('createHonoApp', () => {
482
513
  undefined,
483
514
  expect.any(Object),
484
515
  expect.objectContaining({ request: expect.anything() }),
516
+ '/api',
485
517
  );
486
518
  });
487
519
  });
@@ -571,6 +603,7 @@ describe('createHonoApp', () => {
571
603
  undefined,
572
604
  expect.any(Object),
573
605
  expect.objectContaining({ request: expect.anything() }),
606
+ '/api/v1',
574
607
  );
575
608
  });
576
609
 
@@ -585,6 +618,7 @@ describe('createHonoApp', () => {
585
618
  undefined,
586
619
  expect.any(Object),
587
620
  expect.objectContaining({ request: expect.anything() }),
621
+ '/api/v1',
588
622
  );
589
623
  });
590
624
 
@@ -598,6 +632,16 @@ describe('createHonoApp', () => {
598
632
  expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api/v1');
599
633
  });
600
634
 
635
+ it('routes /api/v1/discovery through outer→inner delegation with correct prefix', async () => {
636
+ const outerApp = createVercelApp();
637
+
638
+ const res = await outerApp.request('/api/v1/discovery');
639
+ expect(res.status).toBe(200);
640
+ const json = await res.json();
641
+ expect(json.data).toBeDefined();
642
+ expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api/v1');
643
+ });
644
+
601
645
  it('routes /api/v1/data/account through outer→inner delegation', async () => {
602
646
  const outerApp = createVercelApp();
603
647
 
@@ -609,6 +653,84 @@ describe('createHonoApp', () => {
609
653
  undefined,
610
654
  expect.any(Object),
611
655
  expect.objectContaining({ request: expect.anything() }),
656
+ '/api/v1',
657
+ );
658
+ });
659
+
660
+ it('POST /api/v1/data/account parses JSON body through outer→inner delegation', async () => {
661
+ const outerApp = createVercelApp();
662
+ const body = { name: 'Acme' };
663
+
664
+ const res = await outerApp.request('/api/v1/data/account', {
665
+ method: 'POST',
666
+ headers: { 'Content-Type': 'application/json' },
667
+ body: JSON.stringify(body),
668
+ });
669
+ expect(res.status).toBe(200);
670
+ expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
671
+ 'POST',
672
+ '/data/account',
673
+ body,
674
+ expect.any(Object),
675
+ expect.objectContaining({ request: expect.anything() }),
676
+ '/api/v1',
677
+ );
678
+ });
679
+
680
+ it('PUT /api/v1/data/account parses JSON body through outer→inner delegation', async () => {
681
+ const outerApp = createVercelApp();
682
+ const body = { name: 'Updated' };
683
+
684
+ const res = await outerApp.request('/api/v1/data/account', {
685
+ method: 'PUT',
686
+ headers: { 'Content-Type': 'application/json' },
687
+ body: JSON.stringify(body),
688
+ });
689
+ expect(res.status).toBe(200);
690
+ expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
691
+ 'PUT',
692
+ '/data/account',
693
+ body,
694
+ expect.any(Object),
695
+ expect.objectContaining({ request: expect.anything() }),
696
+ '/api/v1',
697
+ );
698
+ });
699
+
700
+ it('PATCH /api/v1/data/account parses JSON body through outer→inner delegation', async () => {
701
+ const outerApp = createVercelApp();
702
+ const body = { name: 'Patched' };
703
+
704
+ const res = await outerApp.request('/api/v1/data/account', {
705
+ method: 'PATCH',
706
+ headers: { 'Content-Type': 'application/json' },
707
+ body: JSON.stringify(body),
708
+ });
709
+ expect(res.status).toBe(200);
710
+ expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
711
+ 'PATCH',
712
+ '/data/account',
713
+ body,
714
+ expect.any(Object),
715
+ expect.objectContaining({ request: expect.anything() }),
716
+ '/api/v1',
717
+ );
718
+ });
719
+
720
+ it('DELETE /api/v1/data/account routes through outer→inner delegation', async () => {
721
+ const outerApp = createVercelApp();
722
+
723
+ const res = await outerApp.request('/api/v1/data/account', {
724
+ method: 'DELETE',
725
+ });
726
+ expect(res.status).toBe(200);
727
+ expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
728
+ 'DELETE',
729
+ '/data/account',
730
+ undefined,
731
+ expect.any(Object),
732
+ expect.objectContaining({ request: expect.anything() }),
733
+ '/api/v1',
612
734
  );
613
735
  });
614
736
 
@@ -635,6 +757,153 @@ describe('createHonoApp', () => {
635
757
  });
636
758
  });
637
759
 
760
+ describe('Body-safe Vercel delegation (buffered body forwarding)', () => {
761
+ /**
762
+ * Validates the body-safe delegation pattern used in
763
+ * `apps/studio/server/index.ts` where the outer handler buffers
764
+ * POST/PUT/PATCH bodies and creates a fresh `Request` for the inner app.
765
+ * This avoids @hono/node-server's lazy body materialisation which can
766
+ * hang on Vercel when the IncomingMessage stream state has changed.
767
+ */
768
+ function createBodySafeVercelApp() {
769
+ const innerApp = createHonoApp({ kernel: mockKernel, prefix: '/api/v1' });
770
+ const outerApp = new Hono();
771
+
772
+ outerApp.all('*', async (c) => {
773
+ const method = c.req.method;
774
+
775
+ // GET/HEAD have no body — pass through directly
776
+ if (method === 'GET' || method === 'HEAD') {
777
+ return innerApp.fetch(c.req.raw);
778
+ }
779
+
780
+ // Buffer body and create a fresh Request
781
+ const rawReq = c.req.raw;
782
+ const body = await rawReq.arrayBuffer();
783
+ const forwarded = new Request(rawReq.url, {
784
+ method,
785
+ headers: rawReq.headers,
786
+ body,
787
+ });
788
+ return innerApp.fetch(forwarded);
789
+ });
790
+
791
+ return outerApp;
792
+ }
793
+
794
+ it('GET requests work without body buffering', async () => {
795
+ const outerApp = createBodySafeVercelApp();
796
+
797
+ const res = await outerApp.request('/api/v1/data/account');
798
+ expect(res.status).toBe(200);
799
+ expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
800
+ 'GET',
801
+ '/data/account',
802
+ undefined,
803
+ expect.any(Object),
804
+ expect.objectContaining({ request: expect.anything() }),
805
+ '/api/v1',
806
+ );
807
+ });
808
+
809
+ it('POST body is forwarded correctly via buffered delegation', async () => {
810
+ const outerApp = createBodySafeVercelApp();
811
+ const body = { name: 'Acme Corp' };
812
+
813
+ const res = await outerApp.request('/api/v1/data/account', {
814
+ method: 'POST',
815
+ headers: { 'Content-Type': 'application/json' },
816
+ body: JSON.stringify(body),
817
+ });
818
+ expect(res.status).toBe(200);
819
+ expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
820
+ 'POST',
821
+ '/data/account',
822
+ body,
823
+ expect.any(Object),
824
+ expect.objectContaining({ request: expect.anything() }),
825
+ '/api/v1',
826
+ );
827
+ });
828
+
829
+ it('PUT body is forwarded correctly via buffered delegation', async () => {
830
+ const outerApp = createBodySafeVercelApp();
831
+ const body = { name: 'Updated Corp' };
832
+
833
+ const res = await outerApp.request('/api/v1/data/account', {
834
+ method: 'PUT',
835
+ headers: { 'Content-Type': 'application/json' },
836
+ body: JSON.stringify(body),
837
+ });
838
+ expect(res.status).toBe(200);
839
+ expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
840
+ 'PUT',
841
+ '/data/account',
842
+ body,
843
+ expect.any(Object),
844
+ expect.objectContaining({ request: expect.anything() }),
845
+ '/api/v1',
846
+ );
847
+ });
848
+
849
+ it('PATCH body is forwarded correctly via buffered delegation', async () => {
850
+ const outerApp = createBodySafeVercelApp();
851
+ const body = { status: 'active' };
852
+
853
+ const res = await outerApp.request('/api/v1/data/account', {
854
+ method: 'PATCH',
855
+ headers: { 'Content-Type': 'application/json' },
856
+ body: JSON.stringify(body),
857
+ });
858
+ expect(res.status).toBe(200);
859
+ expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
860
+ 'PATCH',
861
+ '/data/account',
862
+ body,
863
+ expect.any(Object),
864
+ expect.objectContaining({ request: expect.anything() }),
865
+ '/api/v1',
866
+ );
867
+ });
868
+
869
+ it('DELETE without body works via buffered delegation', async () => {
870
+ const outerApp = createBodySafeVercelApp();
871
+
872
+ const res = await outerApp.request('/api/v1/data/account', {
873
+ method: 'DELETE',
874
+ });
875
+ expect(res.status).toBe(200);
876
+ expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
877
+ 'DELETE',
878
+ '/data/account',
879
+ undefined,
880
+ expect.any(Object),
881
+ expect.objectContaining({ request: expect.anything() }),
882
+ '/api/v1',
883
+ );
884
+ });
885
+
886
+ it('POST with empty body defaults to {} via buffered delegation', async () => {
887
+ const outerApp = createBodySafeVercelApp();
888
+
889
+ const res = await outerApp.request('/api/v1/data/account', {
890
+ method: 'POST',
891
+ headers: { 'Content-Type': 'application/json' },
892
+ body: '',
893
+ });
894
+ expect(res.status).toBe(200);
895
+ // Empty body falls back to {} via .catch(() => ({})) in the adapter
896
+ expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
897
+ 'POST',
898
+ '/data/account',
899
+ {},
900
+ expect.any(Object),
901
+ expect.objectContaining({ request: expect.anything() }),
902
+ '/api/v1',
903
+ );
904
+ });
905
+ });
906
+
638
907
  describe('Vercel deployment endpoint smoke tests', () => {
639
908
  /**
640
909
  * These tests validate that the two key deployment-health endpoints
package/src/index.ts CHANGED
@@ -66,6 +66,33 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
66
66
  if (res.type === 'redirect' && res.url) {
67
67
  return c.redirect(res.url);
68
68
  }
69
+ if (res.type === 'stream' && res.events) {
70
+ // SSE / Vercel Data Stream streaming response
71
+ const headers: Record<string, string> = {
72
+ 'Content-Type': res.contentType || 'text/event-stream',
73
+ 'Cache-Control': 'no-cache',
74
+ 'Connection': 'keep-alive',
75
+ ...(res.headers || {}),
76
+ };
77
+ const stream = new ReadableStream({
78
+ async start(controller) {
79
+ try {
80
+ const encoder = new TextEncoder();
81
+ for await (const event of res.events) {
82
+ const chunk = res.vercelDataStream
83
+ ? (typeof event === 'string' ? event : JSON.stringify(event) + '\n')
84
+ : `data: ${JSON.stringify(event)}\n\n`;
85
+ controller.enqueue(encoder.encode(chunk));
86
+ }
87
+ } catch (err) {
88
+ // Stream error — close gracefully
89
+ } finally {
90
+ controller.close();
91
+ }
92
+ },
93
+ });
94
+ return new Response(stream, { status: 200, headers });
95
+ }
69
96
  if (res.type === 'stream' && res.stream) {
70
97
  if (res.headers) {
71
98
  Object.entries(res.headers).forEach(([k, v]) => c.header(k, v as string));
@@ -85,6 +112,10 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
85
112
  return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
86
113
  });
87
114
 
115
+ app.get(`${prefix}/discovery`, async (c) => {
116
+ return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
117
+ });
118
+
88
119
  // --- .well-known ---
89
120
  app.get('/.well-known/objectstack', (c) => {
90
121
  return c.redirect(prefix);
@@ -175,7 +206,7 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
175
206
  const url = new URL(c.req.url);
176
207
  url.searchParams.forEach((val, key) => { queryParams[key] = val; });
177
208
 
178
- const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });
209
+ const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw }, prefix);
179
210
  return toResponse(c, result);
180
211
  } catch (err: any) {
181
212
  return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);