@telorun/http-server 0.1.4 → 0.1.6

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @telorun/http-server
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Automated release.
8
+ - Updated dependencies
9
+ - @telorun/sdk@0.2.6
10
+
3
11
  ## 0.1.4
4
12
 
5
13
  ### Patch Changes
@@ -1,6 +1,33 @@
1
1
  import { Static } from "@sinclair/typebox";
2
- import { ControllerContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
3
- import { FastifyInstance } from "fastify";
2
+ import { ControllerContext, Invocable, KindRef, ResourceContext, ResourceInstance } from "@telorun/sdk";
3
+ import { FastifyInstance, FastifyReply } from "fastify";
4
+ declare const HttpApiRouteManifest: import("@sinclair/typebox").TObject<{
5
+ request: import("@sinclair/typebox").TObject<{
6
+ path: import("@sinclair/typebox").TString;
7
+ method: import("@sinclair/typebox").TString;
8
+ schema: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
9
+ params: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
10
+ query: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
11
+ body: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
12
+ headers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
13
+ }>>;
14
+ }>;
15
+ handler: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnsafe<KindRef<Invocable<Record<string, any>, any>>>>;
16
+ inputs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TAny>>;
17
+ response: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
18
+ status: import("@sinclair/typebox").TInteger;
19
+ when: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
20
+ mode: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"buffer">, import("@sinclair/typebox").TLiteral<"stream">]>>;
21
+ schema: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
22
+ query: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
23
+ body: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
24
+ headers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
25
+ }>>;
26
+ headers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
27
+ body: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
28
+ }>>;
29
+ }>;
30
+ type HttpApiRouteManifest = Static<typeof HttpApiRouteManifest>;
4
31
  declare const HttpApiManifest: import("@sinclair/typebox").TObject<{
5
32
  routes: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
6
33
  request: import("@sinclair/typebox").TObject<{
@@ -13,23 +40,28 @@ declare const HttpApiManifest: import("@sinclair/typebox").TObject<{
13
40
  headers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
14
41
  }>>;
15
42
  }>;
16
- handler: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
17
- response: import("@sinclair/typebox").TObject<{
18
- status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TString]>;
19
- statuses: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TObject<{
20
- schema: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
21
- query: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
22
- body: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
23
- headers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
24
- }>>;
25
- headers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
43
+ handler: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnsafe<KindRef<Invocable<Record<string, any>, any>>>>;
44
+ inputs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TAny>>;
45
+ response: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
46
+ status: import("@sinclair/typebox").TInteger;
47
+ when: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
48
+ mode: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"buffer">, import("@sinclair/typebox").TLiteral<"stream">]>>;
49
+ schema: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
50
+ query: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
26
51
  body: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
52
+ headers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
27
53
  }>>;
28
- }>;
54
+ headers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
55
+ body: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
56
+ }>>;
29
57
  }>>;
30
58
  }>;
31
59
  type HttpApiManifest = Static<typeof HttpApiManifest>;
32
- export declare function register(ctx: ControllerContext): Promise<void>;
60
+ export declare function register(_ctx: ControllerContext): Promise<void>;
61
+ export type ResponseEntry = Static<(typeof HttpApiRouteManifest)["properties"]["response"]["items"]>;
62
+ export declare function dispatchResponse(response: ResponseEntry[], result: unknown, requestContext: Record<string, unknown>, moduleContext: {
63
+ expandWith: (v: unknown, ctx: Record<string, unknown>) => unknown;
64
+ }, validateSchema: (value: unknown, schema: unknown) => void, reply: FastifyReply): Promise<void>;
33
65
  export declare class HttpServerApi implements ResourceInstance {
34
66
  private readonly ctx;
35
67
  readonly manifest: HttpApiManifest;
@@ -1,4 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox";
2
+ import { Ref, } from "@telorun/sdk";
3
+ import { pipeline } from "stream/promises";
2
4
  const HttpApiRouteManifest = Type.Object({
3
5
  request: Type.Object({
4
6
  path: Type.String(),
@@ -10,24 +12,73 @@ const HttpApiRouteManifest = Type.Object({
10
12
  headers: Type.Optional(Type.Any()),
11
13
  })),
12
14
  }),
13
- handler: Type.Optional(Type.Any()), // Any handler shape is allowed - will be processed in create()
14
- response: Type.Object({
15
- status: Type.Union([Type.Number({ minimum: 100, maximum: 599 }), Type.String()]),
16
- statuses: Type.Record(Type.String(), Type.Object({
17
- schema: Type.Optional(Type.Object({
18
- query: Type.Optional(Type.Any()),
19
- body: Type.Optional(Type.Any()),
20
- headers: Type.Optional(Type.Any()),
21
- })),
22
- headers: Type.Optional(Type.Record(Type.String(), Type.String())),
15
+ handler: Type.Optional(Type.Unsafe(Ref("kernel#Invocable"))),
16
+ inputs: Type.Optional(Type.Record(Type.String(), Type.Any())),
17
+ response: Type.Array(Type.Object({
18
+ status: Type.Integer({ minimum: 100, maximum: 599 }),
19
+ when: Type.Optional(Type.String()),
20
+ mode: Type.Optional(Type.Union([Type.Literal("buffer"), Type.Literal("stream")])),
21
+ schema: Type.Optional(Type.Object({
22
+ query: Type.Optional(Type.Any()),
23
23
  body: Type.Optional(Type.Any()),
24
+ headers: Type.Optional(Type.Any()),
24
25
  })),
25
- }),
26
+ headers: Type.Optional(Type.Record(Type.String(), Type.String())),
27
+ body: Type.Optional(Type.Any()),
28
+ })),
26
29
  });
27
30
  const HttpApiManifest = Type.Object({
28
31
  routes: Type.Array(HttpApiRouteManifest),
29
32
  });
30
- export async function register(ctx) { }
33
+ export async function register(_ctx) { }
34
+ export async function dispatchResponse(response, result, requestContext, moduleContext, validateSchema, reply) {
35
+ let matched;
36
+ let fallback;
37
+ for (const entry of response) {
38
+ if (!entry.when) {
39
+ fallback ??= entry;
40
+ continue;
41
+ }
42
+ const condition = moduleContext.expandWith(entry.when, { result, ...requestContext });
43
+ if (condition === true) {
44
+ matched = entry;
45
+ break;
46
+ }
47
+ }
48
+ const statusEntry = matched ?? fallback;
49
+ if (!statusEntry) {
50
+ reply.code(500);
51
+ reply.send({
52
+ error: "InternalServerError",
53
+ message: "No matching response status entry",
54
+ status: 500,
55
+ });
56
+ return;
57
+ }
58
+ reply.code(statusEntry.status);
59
+ if (statusEntry.headers) {
60
+ const mappedHeaders = moduleContext.expandWith(statusEntry.headers, {
61
+ result,
62
+ ...requestContext,
63
+ });
64
+ Object.entries(mappedHeaders).forEach(([key, value]) => reply.header(key, value));
65
+ }
66
+ if (statusEntry.mode === "stream") {
67
+ reply.hijack();
68
+ reply.raw.writeHead(statusEntry.status, reply.getHeaders());
69
+ await pipeline(result, reply.raw);
70
+ return;
71
+ }
72
+ if (statusEntry.body !== undefined) {
73
+ const mappedBody = moduleContext.expandWith(statusEntry.body, { result, ...requestContext });
74
+ if (statusEntry.schema?.body) {
75
+ validateSchema(mappedBody, statusEntry.schema.body);
76
+ }
77
+ reply.send(mappedBody);
78
+ return;
79
+ }
80
+ reply.send(result);
81
+ }
31
82
  export class HttpServerApi {
32
83
  ctx;
33
84
  manifest;
@@ -53,7 +104,8 @@ export class HttpServerApi {
53
104
  }
54
105
  }
55
106
  registerRoute(app, route) {
56
- const handler = route.handler ? resolveHandlerName(route.handler) : null;
107
+ // After Phase 5 injection, KindRef<Invocable> is replaced with the live Invocable instance.
108
+ const handler = route.handler;
57
109
  const translatedPath = translateOpenApiPath(route.request.path);
58
110
  const schema = {
59
111
  response: {},
@@ -70,15 +122,12 @@ export class HttpServerApi {
70
122
  if (route.request.schema?.headers) {
71
123
  schema.headers = route.request.schema?.headers;
72
124
  }
73
- schema.response = Object.keys(route.response.statuses).reduce((acc, status) => {
74
- const statusConfig = route.response.statuses[status];
75
- if (statusConfig.schema) {
76
- if (statusConfig.schema.body) {
77
- acc[status] = statusConfig.schema.body;
78
- }
79
- else {
80
- acc[status] = {};
81
- }
125
+ schema.response = route.response.reduce((acc, entry) => {
126
+ if (entry.schema?.body) {
127
+ acc[entry.status] = entry.schema.body;
128
+ }
129
+ else if (entry.schema) {
130
+ acc[entry.status] = {};
82
131
  }
83
132
  return acc;
84
133
  }, {});
@@ -88,59 +137,25 @@ export class HttpServerApi {
88
137
  schema,
89
138
  handler: async (request, reply) => {
90
139
  try {
91
- // Normalize headers to lowercase
92
- const normalizedHeaders = normalizeHeaders(request.headers);
93
- // Construct standardized Telo request object
94
- const requestPayload = {
95
- method: request.method,
96
- path: request.url,
97
- params: request.params || {},
98
- query: request.query || {},
99
- headers: normalizedHeaders,
100
- body: request.body,
140
+ const requestContext = {
141
+ request: {
142
+ method: request.method,
143
+ path: request.url,
144
+ params: request.params || {},
145
+ query: request.query || {},
146
+ headers: normalizeHeaders(request.headers),
147
+ body: request.body,
148
+ },
149
+ };
150
+ const resolvedInputs = route.inputs
151
+ ? (this.ctx.moduleContext.expandWith(route.inputs, requestContext) ?? {})
152
+ : requestContext;
153
+ const invokeInput = {
154
+ ...resolvedInputs,
155
+ inputs: resolvedInputs,
101
156
  };
102
- // Wrap in "request" object as per spec
103
- const teloRequestContext = { request: requestPayload };
104
- const result = handler
105
- ? await this.ctx.invoke(handler.kind, handler.name, resolveHandlerInputs(route.handler, teloRequestContext))
106
- : undefined;
107
- const response = route.response;
108
- // Determine final status code
109
- let statusCode = response.status;
110
- if (typeof statusCode === "string") {
111
- statusCode = this.ctx.expandValue(statusCode, { result });
112
- }
113
- // Convert status to string for lookup
114
- const statusKey = String(statusCode);
115
- const statusConfig = response.statuses[statusKey];
116
- if (!statusConfig) {
117
- reply.code(500);
118
- return reply.send({
119
- error: "InternalServerError",
120
- message: "Response status configuration not found",
121
- status: 500,
122
- });
123
- }
124
- // Set HTTP status code
125
- reply.code(statusCode);
126
- // Map and set response headers if specified
127
- if (statusConfig.headers) {
128
- const mappedHeaders = this.ctx.expandValue(statusConfig.headers, { result });
129
- Object.entries(mappedHeaders).forEach(([key, value]) => {
130
- reply.header(key, value);
131
- });
132
- }
133
- // Map and send response body if specified
134
- if (statusConfig.body !== undefined) {
135
- const mappedBody = this.ctx.expandValue(statusConfig.body, { result });
136
- // Validate response body if schema is specified
137
- if (statusConfig.schema && statusConfig.schema.body) {
138
- this.ctx.validateSchema(mappedBody, statusConfig.schema.body);
139
- }
140
- return reply.send(mappedBody);
141
- }
142
- // No body mapping, send result as-is
143
- return reply.send(result);
157
+ const result = handler ? await handler.invoke(invokeInput) : undefined;
158
+ return dispatchResponse(route.response, result, requestContext, this.ctx.moduleContext, this.ctx.validateSchema.bind(this.ctx), reply);
144
159
  }
145
160
  catch (error) {
146
161
  // Let the error handler deal with all errors
@@ -151,92 +166,8 @@ export class HttpServerApi {
151
166
  }
152
167
  }
153
168
  export async function create(resource, ctx) {
154
- // First validate with a permissive schema (handler can be any shape)
155
169
  ctx.validateSchema(resource, HttpApiManifest);
156
- // Process routes and register unnamed handlers as child resources
157
- let handlerCounter = 0;
158
- const processedRoutes = (resource.routes || []).map((route) => {
159
- if (!route.handler) {
160
- return route;
161
- }
162
- // Check if handler is unnamed (inline handler)
163
- if (typeof route.handler === "object" && !route.handler.name) {
164
- // Use resolveChildren to register the unnamed handler and get its normalized reference
165
- const resolvedHandler = ctx.resolveChildren(route.handler, `__handler_${handlerCounter++}`);
166
- // Return route with the resolved handler reference
167
- return {
168
- ...route,
169
- handler: {
170
- kind: resolvedHandler.kind,
171
- name: resolvedHandler.name,
172
- inputs: route.handler.inputs,
173
- },
174
- };
175
- }
176
- return route;
177
- });
178
- // Create the API instance with processed routes
179
- const processedResource = {
180
- ...resource,
181
- routes: processedRoutes,
182
- };
183
- return new HttpServerApi(ctx, processedResource);
184
- }
185
- function resolveHandlerName(handler) {
186
- if (typeof handler === "string") {
187
- const [kind, name] = handler.split("/");
188
- return { kind, name };
189
- }
190
- if (handler && typeof handler === "object" && typeof handler.kind === "string") {
191
- // name should always be present after create() processes the routes
192
- // but fallback gracefully if it's not
193
- const name = handler.name || `__unnamed_${Math.random().toString(36).slice(2, 9)}`;
194
- return { name, kind: handler.kind };
195
- }
196
- throw new Error("Unable to resolve handler - handler must have a 'kind' property");
197
- }
198
- function resolveHandlerInputs(handler, requestContext) {
199
- if (typeof handler === "string") {
200
- return requestContext;
201
- }
202
- if (!handler || typeof handler !== "object") {
203
- return requestContext;
204
- }
205
- if (!handler.inputs) {
206
- return requestContext;
207
- }
208
- return resolveTemplateInputs(handler.inputs, requestContext);
209
- }
210
- function resolveTemplateInputs(value, context) {
211
- if (typeof value === "string") {
212
- const match = value.match(/^\s*\$\{\{\s*([^}]+)\s*\}\}\s*$/);
213
- if (match) {
214
- return resolveTemplatePath(match[1], context);
215
- }
216
- return value;
217
- }
218
- if (Array.isArray(value)) {
219
- return value.map((item) => resolveTemplateInputs(item, context));
220
- }
221
- if (value && typeof value === "object") {
222
- const resolved = {};
223
- for (const [key, entry] of Object.entries(value)) {
224
- resolved[key] = resolveTemplateInputs(entry, context);
225
- }
226
- return resolved;
227
- }
228
- return value;
229
- }
230
- function resolveTemplatePath(pathExpression, context) {
231
- const parts = pathExpression.trim().split(".").filter(Boolean);
232
- let current = context;
233
- for (const part of parts) {
234
- if (!current || (typeof current !== "object" && typeof current !== "function")) {
235
- return undefined;
236
- }
237
- current = current[part];
238
- }
239
- return current;
170
+ return new HttpServerApi(ctx, resource);
240
171
  }
241
172
  /**
242
173
  * Translates OpenAPI path format {paramName} to Fastify format :paramName
@@ -255,53 +186,3 @@ function normalizeHeaders(headers) {
255
186
  }
256
187
  return normalized;
257
188
  }
258
- /**
259
- * Legacy function - kept for compatibility but not used
260
- * Converts framework-specific validation errors to standardized Telo format
261
- * Returns null if the error is not a validation error
262
- */
263
- function convertValidationError(error) {
264
- // Check if this is a Fastify/AJV validation error
265
- if (!error || typeof error !== "object") {
266
- return null;
267
- }
268
- // Fastify validation errors have a statusCode of 400 and validation array
269
- if (error.statusCode === 400 && Array.isArray(error.validation)) {
270
- const details = error.validation.map((err) => {
271
- const path = err.instancePath ? err.instancePath.replace(/^\//, "").replace(/\//g, ".") : "";
272
- // Determine location from keyword/message context
273
- let location = "body"; // default
274
- if (err.keyword === "required" && err.params?.missingProperty) {
275
- location = determinLocationFromContext(err);
276
- }
277
- else {
278
- location = determinLocationFromContext(err);
279
- }
280
- return {
281
- location,
282
- path: path || err.params?.missingProperty || "",
283
- message: err.message || "Validation failed",
284
- };
285
- });
286
- return {
287
- error: "ValidationError",
288
- message: "Request validation failed",
289
- status: 400,
290
- details,
291
- };
292
- }
293
- return null;
294
- }
295
- /**
296
- * Helper to determine the location (body, query, params, headers) from validation error context
297
- */
298
- function determinLocationFromContext(err) {
299
- // AJV validation errors in Fastify include parent keyword context
300
- if (err.parentSchema && err.instancePath) {
301
- const path = err.instancePath;
302
- // This is a simplified check; in practice, Fastify provides better context
303
- // For now, default to "body" for general validation errors
304
- return "body";
305
- }
306
- return "body";
307
- }
@@ -1,8 +1,29 @@
1
- import type { ResourceContext, ResourceInstance, RuntimeResource } from "@telorun/sdk";
1
+ import type { Invocable, KindRef, ResourceContext, ResourceInstance, RuntimeResource } from "@telorun/sdk";
2
+ import { ResponseEntry } from "./http-api-controller.js";
3
+ type CorsOptions = {
4
+ origin?: string | boolean | string[];
5
+ methods?: string | string[];
6
+ allowedHeaders?: string | string[];
7
+ exposedHeaders?: string | string[];
8
+ credentials?: boolean;
9
+ maxAge?: number;
10
+ cacheControl?: number | string;
11
+ preflightContinue?: boolean;
12
+ optionsSuccessStatus?: number;
13
+ preflight?: boolean;
14
+ strictPreflight?: boolean;
15
+ hideOptionsRoute?: boolean;
16
+ };
2
17
  type HttpServerResource = RuntimeResource & {
3
18
  host?: string;
4
19
  port?: number;
5
20
  baseUrl?: string;
21
+ logger?: boolean;
22
+ cors?: CorsOptions;
23
+ contentTypeParsers?: Array<{
24
+ contentType: string;
25
+ parser?: Invocable;
26
+ }>;
6
27
  openapi?: {
7
28
  info: {
8
29
  title: string;
@@ -13,6 +34,10 @@ type HttpServerResource = RuntimeResource & {
13
34
  path?: string;
14
35
  type?: string;
15
36
  }>;
37
+ notFoundHandler?: {
38
+ invoke: KindRef<Invocable>;
39
+ response?: ResponseEntry[];
40
+ };
16
41
  };
17
- export declare function create(resource: HttpServerResource, ctx: ResourceContext): ResourceInstance | null;
42
+ export declare function create(resource: HttpServerResource, ctx: ResourceContext): Promise<ResourceInstance | null>;
18
43
  export {};
@@ -1,31 +1,75 @@
1
+ import cors from "@fastify/cors";
1
2
  import swagger from "@fastify/swagger";
2
3
  import apiReference from "@scalar/fastify-api-reference";
3
4
  import addFormats from "ajv-formats";
4
5
  import Fastify from "fastify";
6
+ import { dispatchResponse } from "./http-api-controller.js";
5
7
  class HttpServer {
6
8
  releaseHold = null;
9
+ pluginsInitialized = false;
7
10
  app;
8
11
  host;
9
12
  port;
10
13
  baseUrl;
11
14
  resource;
12
15
  ctx;
13
- constructor(resource, ctx) {
16
+ resolvedNotFoundHandler;
17
+ constructor(resource, ctx, resolvedNotFoundHandler = null) {
14
18
  this.resource = resource;
15
19
  this.ctx = ctx;
16
20
  this.host = resource.host || "0.0.0.0";
17
21
  this.port = Number(resource.port || 0);
18
22
  this.baseUrl = resource.baseUrl ?? `http://${this.host}:${this.port}`;
23
+ this.resolvedNotFoundHandler = resolvedNotFoundHandler;
19
24
  if (!this.port) {
20
25
  throw new Error("Http.Server port is required");
21
26
  }
22
- this.app = Fastify({ logger: true, ajv: { plugins: [addFormats.default] } });
27
+ this.app = Fastify({
28
+ logger: resource.logger,
29
+ ajv: { customOptions: { useDefaults: true }, plugins: [addFormats.default] },
30
+ });
23
31
  }
24
32
  async init() {
25
- await this.setupPlugins();
33
+ if (!this.pluginsInitialized) {
34
+ await this.setupPlugins();
35
+ this.pluginsInitialized = true;
36
+ }
26
37
  this.setupRoutes();
27
38
  }
28
39
  async setupPlugins() {
40
+ for (const { contentType, parser } of this.resource.contentTypeParsers ?? []) {
41
+ if (parser) {
42
+ this.app.addContentTypeParser(contentType, { parseAs: "string" }, async (_req, body, done) => {
43
+ try {
44
+ done(null, await parser.invoke({ body }));
45
+ }
46
+ catch (err) {
47
+ done(err, undefined);
48
+ }
49
+ });
50
+ }
51
+ else {
52
+ this.app.addContentTypeParser(contentType, { parseAs: "string" }, (_req, body, done) => {
53
+ done(null, body);
54
+ });
55
+ }
56
+ }
57
+ if (this.resource.cors) {
58
+ await this.app.register(cors, {
59
+ origin: this.resource.cors.origin,
60
+ methods: this.resource.cors.methods,
61
+ allowedHeaders: this.resource.cors.allowedHeaders,
62
+ exposedHeaders: this.resource.cors.exposedHeaders,
63
+ credentials: this.resource.cors.credentials,
64
+ maxAge: this.resource.cors.maxAge,
65
+ cacheControl: this.resource.cors.cacheControl,
66
+ preflightContinue: this.resource.cors.preflightContinue,
67
+ optionsSuccessStatus: this.resource.cors.optionsSuccessStatus,
68
+ preflight: this.resource.cors.preflight,
69
+ strictPreflight: this.resource.cors.strictPreflight,
70
+ hideOptionsRoute: this.resource.cors.hideOptionsRoute,
71
+ });
72
+ }
29
73
  // Register custom error handler for validation errors
30
74
  this.app.setErrorHandler((error, request, reply) => {
31
75
  const mappedError = convertFastifyValidationError(error);
@@ -67,12 +111,41 @@ class HttpServer {
67
111
  const type = mount.type || "";
68
112
  const { kind, name } = parseType(type);
69
113
  const prefix = mount.path || "";
70
- const api = this.ctx.moduleContext.getInvokable(name);
114
+ const api = this.ctx.moduleContext.getInvocable(name);
71
115
  if (!api) {
72
116
  throw new Error(`Failed to mount Http.Api at "${prefix}": ${type} not found`);
73
117
  }
74
118
  api.register(this.app, prefix);
75
119
  }
120
+ if (this.resolvedNotFoundHandler) {
121
+ const handler = this.resolvedNotFoundHandler;
122
+ this.app.setNotFoundHandler(async (request, reply) => {
123
+ const normalizedHeaders = {};
124
+ for (const [key, value] of Object.entries(request.headers)) {
125
+ normalizedHeaders[key.toLowerCase()] = value;
126
+ }
127
+ const requestContext = {
128
+ request: {
129
+ method: request.method,
130
+ path: request.url,
131
+ params: request.params || {},
132
+ query: request.query || {},
133
+ headers: normalizedHeaders,
134
+ body: request.body,
135
+ },
136
+ };
137
+ const result = await this.ctx.invoke(handler.kind, handler.name, requestContext);
138
+ if (handler.response) {
139
+ return dispatchResponse(handler.response, result, requestContext, this.ctx.moduleContext, this.ctx.validateSchema.bind(this.ctx), reply);
140
+ }
141
+ const status = result?.status ?? 200;
142
+ reply.code(status);
143
+ if (result?.headers) {
144
+ Object.entries(result.headers).forEach(([key, value]) => reply.header(key, value));
145
+ }
146
+ return reply.send(result?.body ?? result);
147
+ });
148
+ }
76
149
  }
77
150
  async run() {
78
151
  this.releaseHold = this.ctx.acquireHold();
@@ -103,8 +176,18 @@ class HttpServer {
103
176
  await this.app.close();
104
177
  }
105
178
  }
106
- export function create(resource, ctx) {
107
- return new HttpServer(resource, ctx);
179
+ export async function create(resource, ctx) {
180
+ let resolvedNotFoundHandler = null;
181
+ if (resource.notFoundHandler) {
182
+ const resolved = ctx.resolveChildren(resource.notFoundHandler.invoke);
183
+ resolvedNotFoundHandler = {
184
+ kind: resolved.kind,
185
+ name: resolved.name,
186
+ inputs: resource.notFoundHandler.invoke.inputs ?? {},
187
+ response: resource.notFoundHandler.response,
188
+ };
189
+ }
190
+ return new HttpServer(resource, ctx, resolvedNotFoundHandler);
108
191
  }
109
192
  function parseType(type) {
110
193
  const separator = type.lastIndexOf(".");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/http-server",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -16,20 +16,21 @@
16
16
  "import": "./dist/http-api-controller.js"
17
17
  }
18
18
  },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.lib.json"
21
+ },
19
22
  "dependencies": {
23
+ "@fastify/cors": "^11.2.0",
20
24
  "@fastify/swagger": "^9.6.1",
21
25
  "@scalar/fastify-api-reference": "^1.44.6",
22
26
  "@sinclair/typebox": "^0.34.48",
27
+ "@telorun/sdk": "workspace:*",
23
28
  "ajv": "^8.17.1",
24
29
  "ajv-formats": "^3.0.1",
25
- "fastify": "^5.7.2",
26
- "@telorun/sdk": "0.2.5"
30
+ "fastify": "^5.7.2"
27
31
  },
28
32
  "devDependencies": {
29
33
  "@types/node": "^20.0.0",
30
34
  "typescript": "^5.0.0"
31
- },
32
- "scripts": {
33
- "build": "tsc -p tsconfig.lib.json"
34
35
  }
35
- }
36
+ }