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