@loutrejs/loutre 0.4.1 → 0.6.0

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.
Files changed (164) hide show
  1. package/README.md +63 -32
  2. package/dist/adapters/aws-lambda.d.ts +5 -3
  3. package/dist/adapters/aws-lambda.js +53 -23
  4. package/dist/adapters/bun.d.ts +7 -4
  5. package/dist/adapters/bun.js +52 -42
  6. package/dist/adapters/cloudflare-workers.d.ts +5 -3
  7. package/dist/adapters/cloudflare-workers.js +23 -28
  8. package/dist/adapters/deno.d.ts +7 -4
  9. package/dist/adapters/deno.js +94 -57
  10. package/dist/adapters/electron.d.ts +13 -5
  11. package/dist/adapters/electron.js +76 -28
  12. package/dist/application/definition.d.ts +31 -0
  13. package/dist/application/definition.js +16 -0
  14. package/dist/application/index.d.ts +4 -63
  15. package/dist/application/index.js +2 -11
  16. package/dist/application/kernel-internal.d.ts +2 -0
  17. package/dist/application/kernel-internal.js +10 -0
  18. package/dist/application/kernel.d.ts +21 -0
  19. package/dist/application/kernel.js +65 -0
  20. package/dist/core/diagnostic.d.ts +9 -0
  21. package/dist/core/diagnostic.js +9 -0
  22. package/dist/core/extension.d.ts +107 -0
  23. package/dist/core/extension.js +73 -0
  24. package/dist/core/generic-layer.d.ts +26 -0
  25. package/dist/core/generic-layer.js +116 -0
  26. package/dist/core/index.d.ts +31 -15
  27. package/dist/core/index.js +15 -15
  28. package/dist/core/injection.d.ts +4 -13
  29. package/dist/core/injection.js +63 -0
  30. package/dist/core/model/context.d.ts +18 -0
  31. package/dist/core/model/context.js +34 -0
  32. package/dist/core/model/lifecycle.d.ts +5 -0
  33. package/dist/core/model/lifecycle.js +44 -0
  34. package/dist/core/model/modules.d.ts +2 -0
  35. package/dist/core/model/modules.js +101 -0
  36. package/dist/core/model/providers.d.ts +4 -0
  37. package/dist/core/model/providers.js +74 -0
  38. package/dist/core/model/snapshot.d.ts +6 -0
  39. package/dist/core/model/snapshot.js +31 -0
  40. package/dist/core/model/types.d.ts +78 -0
  41. package/dist/core/model/validation.d.ts +4 -0
  42. package/dist/core/model/validation.js +40 -0
  43. package/dist/core/model/visibility.d.ts +4 -0
  44. package/dist/core/model/visibility.js +20 -0
  45. package/dist/core/model.d.ts +10 -0
  46. package/dist/core/model.js +346 -0
  47. package/dist/core/module.d.ts +8 -13
  48. package/dist/core/source-location.d.ts +8 -0
  49. package/dist/core/source-location.js +31 -0
  50. package/dist/core/token-internal.d.ts +3 -0
  51. package/dist/core/token-internal.js +4 -0
  52. package/dist/core/type.d.ts +7 -0
  53. package/dist/core/type.js +4 -0
  54. package/dist/generated/version.d.ts +1 -1
  55. package/dist/generated/version.js +1 -1
  56. package/dist/graph/index.d.ts +2 -3
  57. package/dist/graph/index.js +1 -3
  58. package/dist/graph/model.d.ts +34 -0
  59. package/dist/graph/model.js +127 -0
  60. package/dist/http/auth.d.ts +46 -0
  61. package/dist/http/auth.js +96 -0
  62. package/dist/http/client.d.ts +28 -31
  63. package/dist/http/client.js +60 -41
  64. package/dist/http/cors-internal.d.ts +29 -0
  65. package/dist/http/cors-internal.js +160 -0
  66. package/dist/http/cors.d.ts +5 -8
  67. package/dist/http/cors.js +7 -165
  68. package/dist/http/extension.d.ts +348 -0
  69. package/dist/http/extension.js +947 -0
  70. package/dist/http/index.d.ts +9 -7
  71. package/dist/http/index.js +5 -7
  72. package/dist/{application → http}/openapi.d.ts +12 -3
  73. package/dist/{application → http}/openapi.js +164 -101
  74. package/dist/http/path.d.ts +2 -39
  75. package/dist/http/path.js +7 -2
  76. package/dist/http/runtime/request.d.ts +17 -0
  77. package/dist/http/runtime/request.js +109 -0
  78. package/dist/http/runtime/response.d.ts +18 -0
  79. package/dist/http/runtime/response.js +108 -0
  80. package/dist/index.d.ts +4 -0
  81. package/dist/index.js +2 -0
  82. package/dist/message-port/extension.d.ts +66 -0
  83. package/dist/message-port/extension.js +395 -0
  84. package/dist/message-port/index.d.ts +4 -73
  85. package/dist/message-port/index.js +2 -189
  86. package/dist/message-port/transport.d.ts +11 -0
  87. package/dist/message-port/transport.js +45 -0
  88. package/dist/runtime/async-iterator-lifecycle.d.ts +12 -0
  89. package/dist/runtime/async-iterator-lifecycle.js +95 -0
  90. package/dist/runtime/capability.d.ts +11 -11
  91. package/dist/runtime/capability.js +3 -3
  92. package/dist/runtime/di.d.ts +2 -22
  93. package/dist/runtime/di.js +8 -285
  94. package/dist/runtime/execution-tracker.d.ts +8 -0
  95. package/dist/runtime/execution-tracker.js +54 -0
  96. package/dist/runtime/index.d.ts +15 -9
  97. package/dist/runtime/index.js +9 -9
  98. package/dist/runtime/ingress-gate.d.ts +7 -0
  99. package/dist/runtime/ingress-gate.js +33 -0
  100. package/dist/runtime/kernel.d.ts +23 -0
  101. package/dist/runtime/kernel.js +354 -0
  102. package/dist/runtime/logger.d.ts +2 -0
  103. package/dist/runtime/logger.js +2 -0
  104. package/dist/runtime/node-capabilities.d.ts +1 -1
  105. package/dist/runtime/node-capabilities.js +1 -1
  106. package/dist/runtime/server-port.d.ts +2 -0
  107. package/dist/runtime/server-port.js +11 -0
  108. package/dist/tasks/extension.d.ts +144 -0
  109. package/dist/tasks/extension.js +499 -0
  110. package/dist/tasks/index.d.ts +2 -0
  111. package/dist/tasks/index.js +1 -0
  112. package/dist/tasks/runtime/cron.d.ts +6 -0
  113. package/dist/tasks/runtime/cron.js +89 -0
  114. package/dist/websocket/errors.d.ts +9 -0
  115. package/dist/websocket/errors.js +18 -0
  116. package/dist/websocket/extension.d.ts +154 -0
  117. package/dist/websocket/extension.js +524 -0
  118. package/dist/websocket/index.d.ts +3 -0
  119. package/dist/websocket/index.js +2 -0
  120. package/package.json +12 -16
  121. package/dist/application/binding.d.ts +0 -33
  122. package/dist/application/binding.js +0 -295
  123. package/dist/application/host.d.ts +0 -8
  124. package/dist/application/host.js +0 -14
  125. package/dist/application/queue.d.ts +0 -3
  126. package/dist/application/queue.js +0 -4
  127. package/dist/application/scheduler.d.ts +0 -2
  128. package/dist/application/scheduler.js +0 -45
  129. package/dist/core/context-key.d.ts +0 -14
  130. package/dist/core/context-key.js +0 -14
  131. package/dist/core/contract.d.ts +0 -94
  132. package/dist/core/contract.js +0 -79
  133. package/dist/core/execution.d.ts +0 -78
  134. package/dist/core/execution.js +0 -61
  135. package/dist/core/implementation.d.ts +0 -34
  136. package/dist/core/implementation.js +0 -30
  137. package/dist/core/layer.d.ts +0 -109
  138. package/dist/core/layer.js +0 -52
  139. package/dist/graph/compiler.d.ts +0 -4
  140. package/dist/graph/compiler.js +0 -228
  141. package/dist/graph/graph.d.ts +0 -17
  142. package/dist/graph/graph.js +0 -1103
  143. package/dist/graph/ir.d.ts +0 -175
  144. package/dist/http/application.d.ts +0 -18
  145. package/dist/http/application.js +0 -506
  146. package/dist/http/basic-auth.d.ts +0 -37
  147. package/dist/http/basic-auth.js +0 -67
  148. package/dist/http/definitions.d.ts +0 -225
  149. package/dist/http/definitions.js +0 -69
  150. package/dist/http/hosted-protocol.d.ts +0 -4
  151. package/dist/http/hosted-protocol.js +0 -4
  152. package/dist/http/params.d.ts +0 -2
  153. package/dist/http/params.js +0 -19
  154. package/dist/http/request-body.d.ts +0 -4
  155. package/dist/http/request-body.js +0 -6
  156. package/dist/http/validate.d.ts +0 -20
  157. package/dist/http/validate.js +0 -6
  158. package/dist/message-port/environment.d.ts +0 -2
  159. package/dist/message-port/environment.js +0 -4
  160. package/dist/runtime/application.d.ts +0 -38
  161. package/dist/runtime/application.js +0 -339
  162. package/dist/runtime/pipeline.d.ts +0 -11
  163. package/dist/runtime/pipeline.js +0 -161
  164. /package/dist/{graph/ir.js → core/model/types.js} +0 -0
@@ -1,8 +1,10 @@
1
- export * from './application.js';
2
- export * from './basic-auth.js';
3
- export * from './client.js';
1
+ export { createHttpClient, fetchHttpTransport, HttpClientResponseError, } from './client.js';
2
+ export type { FetchHttpTransportOptions, HttpClient, HttpClientRequest, HttpClientResponse, HttpClientTransport, HttpClientTransportRequest, HttpClientTransportResponse, } from './client.js';
3
+ export { cors } from './cors.js';
4
4
  export type { CorsLayerDescriptor, CorsOptions, CorsOrigin } from './cors.js';
5
- export * from './definitions.js';
6
- export { http } from './hosted-protocol.js';
7
- export * from './request-body.js';
8
- export { validate } from './validate.js';
5
+ export { basicAuth, bearerAuth } from './auth.js';
6
+ export type { BasicAuthContext, BasicAuthCredentials, BasicAuthDefinition, BasicAuthUnauthorized, BasicAuthRuntime, BearerAuthContext, BearerAuthDefinition, BearerAuthRuntime, BearerAuthUnauthorized, HttpAuthenticationFailure, } from './auth.js';
7
+ export { bindHttpServer, defineHttpContract, defineHttpImplementation, defineHttpMiddleware, httpError, executionHttp as http, httpExecutionExtension, HTTP_SERVER, validate, } from './extension.js';
8
+ export type { HttpContract, HttpContractBranchDefinition, HttpContractNodeDefinition, HttpContractRouteTree, HttpExecutionContext, HttpExecutionDefinition, HttpExecutionRequestDefinition, HttpExecutionResponseDefinition, HttpExecutionResult, HttpExecutionRouteDefinition, HttpErrorMapping, HttpErrorMatcher, HttpExtensionRuntime, HttpHandlers, HttpHeaderValue, HttpHeaders, HttpHostApi, HttpImplementationDefinition, HttpMiddleware, HttpMiddlewareContext, HttpResponseHeadersDefinition, HttpResponseResult, HttpResponseHeadersWithDefaults, HttpServerDriver, HttpValidationMiddleware, HttpValidationPart, ResolvedHttpContractRoutes, } from './extension.js';
9
+ export { compareHttpPathSpecificity, HttpPathDecodeError, matchHttpPath, normalizeHttpPath, parseHttpPath, } from './path.js';
10
+ export type { HttpPathSegment, PathParamNames } from './path.js';
@@ -1,7 +1,5 @@
1
- export * from './application.js';
2
- export * from './basic-auth.js';
3
- export * from './client.js';
4
- export * from './definitions.js';
5
- export { http } from './hosted-protocol.js';
6
- export * from './request-body.js';
7
- export { validate } from './validate.js';
1
+ export { createHttpClient, fetchHttpTransport, HttpClientResponseError, } from './client.js';
2
+ export { cors } from './cors.js';
3
+ export { basicAuth, bearerAuth } from './auth.js';
4
+ export { bindHttpServer, defineHttpContract, defineHttpImplementation, defineHttpMiddleware, httpError, executionHttp as http, httpExecutionExtension, HTTP_SERVER, validate, } from './extension.js';
5
+ export { compareHttpPathSpecificity, HttpPathDecodeError, matchHttpPath, normalizeHttpPath, parseHttpPath, } from './path.js';
@@ -1,4 +1,5 @@
1
- import type { ApplicationDefinition } from './index.js';
1
+ import { type ApplicationModel } from '../core/index.js';
2
+ import { type HttpExecutionResponseDefinition, type HttpExecutionRouteDefinition } from './extension.js';
2
3
  export interface OpenApiInfo {
3
4
  readonly title: string;
4
5
  readonly version: string;
@@ -20,7 +21,6 @@ export interface GenerateOpenApiOptions {
20
21
  }
21
22
  export interface OpenApiDocument {
22
23
  readonly openapi: '3.2.0';
23
- readonly jsonSchemaDialect: 'https://json-schema.org/draft/2020-12/schema';
24
24
  readonly info: OpenApiInfo;
25
25
  readonly servers?: readonly OpenApiServer[];
26
26
  readonly paths: Readonly<Record<string, OpenApiPathItem>>;
@@ -30,5 +30,14 @@ export interface OpenApiDocument {
30
30
  }
31
31
  type JsonSchema = Record<string, unknown>;
32
32
  type OpenApiPathItem = Record<string, unknown>;
33
- export declare function generateOpenApi(application: ApplicationDefinition, options: GenerateOpenApiOptions): OpenApiDocument;
33
+ type OpenApiResponseDefinition = HttpExecutionResponseDefinition;
34
+ export interface HttpOperationTarget {
35
+ readonly definition: Omit<HttpExecutionRouteDefinition, 'responses'> & {
36
+ readonly responses: Readonly<Record<string, OpenApiResponseDefinition>>;
37
+ };
38
+ readonly procedure: string;
39
+ }
40
+ export declare function generateOpenApi(source: ApplicationModel | {
41
+ readonly model: ApplicationModel;
42
+ }, options: GenerateOpenApiOptions): OpenApiDocument;
34
43
  export {};
@@ -1,6 +1,5 @@
1
- import { supportsJsonSchema, } from '../core/index.js';
2
- import { assertValidCompilation, compileApplication } from '../graph/index.js';
3
- import { httpRequestBodyContentType, httpRequestBodySchema, } from '../http/index.js';
1
+ import { supportsJsonSchema, assertValidApplicationModel, } from '../core/index.js';
2
+ import { httpExecutionExtension, } from './extension.js';
4
3
  const FIXED_METHODS = new Set([
5
4
  'get',
6
5
  'put',
@@ -12,40 +11,29 @@ const FIXED_METHODS = new Set([
12
11
  'trace',
13
12
  'query',
14
13
  ]);
15
- export function generateOpenApi(application, options) {
16
- assertValidCompilation(compileApplication({
17
- modules: application.modules,
18
- ...(application.arguments === undefined
19
- ? {}
20
- : { arguments: application.arguments }),
21
- tasks: application.tasks,
22
- triggers: application.triggers,
23
- }));
14
+ export function generateOpenApi(source, options) {
15
+ const model = 'model' in source ? source.model : source;
16
+ assertValidApplicationModel(model);
24
17
  const registry = new SchemaRegistry();
25
18
  const paths = {};
26
19
  const operationIds = new Set();
27
- for (const module of collectModules(application.modules)) {
28
- for (const implementation of module.definition.implementations ?? []) {
29
- if (implementation.protocol !== 'http')
30
- continue;
31
- for (const procedure of implementation.procedures) {
32
- const protocol = implementation.contract.procedures[procedure]?.protocols.http;
33
- if (!protocol || protocol.protocol !== 'http')
34
- continue;
35
- const typed = protocol;
36
- const target = {
37
- definition: typed.definition,
38
- procedure,
39
- };
40
- const operation = createOperation(target, registry, operationIds, options.operationId);
41
- attachOperation(paths, typed.definition, operation);
42
- }
20
+ const http = model.extensions.get(httpExecutionExtension);
21
+ for (const execution of http?.executions ?? []) {
22
+ for (const route of execution.compiled.routes) {
23
+ const target = {
24
+ procedure: route.name,
25
+ definition: {
26
+ ...route.definition,
27
+ method: route.method,
28
+ path: route.path,
29
+ },
30
+ };
31
+ attachOperation(paths, target.definition, createOperation(target, registry, operationIds, options.operationId));
43
32
  }
44
33
  }
45
34
  const schemas = registry.components();
46
35
  return {
47
36
  openapi: '3.2.0',
48
- jsonSchemaDialect: 'https://json-schema.org/draft/2020-12/schema',
49
37
  info: options.info,
50
38
  ...(options.servers === undefined ? {} : { servers: options.servers }),
51
39
  paths,
@@ -82,6 +70,7 @@ function createOperation(target, registry, operationIds, resolveOperationId) {
82
70
  }
83
71
  if (request?.query) {
84
72
  parameters.push({
73
+ name: 'query',
85
74
  in: 'querystring',
86
75
  content: {
87
76
  'application/x-www-form-urlencoded': {
@@ -94,7 +83,7 @@ function createOperation(target, registry, operationIds, resolveOperationId) {
94
83
  parameters.push(...headerParameters(request.headers, registry, target, 'RequestHeaders_Input'));
95
84
  }
96
85
  const requestBody = request?.body
97
- ? createRequestBody(request.body, registry, target)
86
+ ? createRequestBody(request.body, request.headers, registry, target)
98
87
  : undefined;
99
88
  return {
100
89
  ...(definition.tags === undefined ? {} : { tags: definition.tags }),
@@ -113,21 +102,62 @@ function createOperation(target, registry, operationIds, resolveOperationId) {
113
102
  : { deprecated: definition.deprecated }),
114
103
  };
115
104
  }
116
- function createRequestBody(body, registry, target) {
117
- const contentType = httpRequestBodyContentType(body);
105
+ function createRequestBody(body, headers, registry, target) {
106
+ const mediaTypes = requestBodyMediaTypes(headers, registry, target);
107
+ const schema = registry.reference(body, 'input', componentName(target, 'RequestBody_Input'));
118
108
  return {
119
- content: {
120
- [contentType]: {
121
- schema: registry.reference(httpRequestBodySchema(body), 'input', componentName(target, 'RequestBody_Input')),
122
- },
123
- },
109
+ content: Object.fromEntries(mediaTypes.map((mediaType) => [mediaType, { schema }])),
124
110
  };
125
111
  }
112
+ function requestBodyMediaTypes(headers, registry, target) {
113
+ if (!headers) {
114
+ throw openApiError('LUTRE_OPENAPI_CONTENT_TYPE_001', `${describeTarget(target)} request body requires a request headers schema with content-type.`);
115
+ }
116
+ const materialized = registry.materialize(headers, 'input', componentName(target, 'RequestHeaders_Input'));
117
+ const properties = objectProperties(materialized.schema, 'LUTRE_OPENAPI_HEADER_SCHEMA_001', `${describeTarget(target)} request headers`);
118
+ const contentType = properties['content-type'];
119
+ if (!contentType) {
120
+ throw openApiError('LUTRE_OPENAPI_CONTENT_TYPE_001', `${describeTarget(target)} request headers must declare content-type.`);
121
+ }
122
+ const values = finiteStringValues(contentType);
123
+ if (!values || values.length === 0) {
124
+ throw openApiError('LUTRE_OPENAPI_CONTENT_TYPE_002', `${describeTarget(target)} request content-type must resolve to a finite set of string literals.`);
125
+ }
126
+ return [...new Set(values)];
127
+ }
128
+ function finiteStringValues(schema) {
129
+ if (typeof schema.const === 'string')
130
+ return [schema.const];
131
+ if (Array.isArray(schema.enum)) {
132
+ return schema.enum.every((value) => typeof value === 'string')
133
+ ? schema.enum
134
+ : undefined;
135
+ }
136
+ for (const keyword of ['anyOf', 'oneOf']) {
137
+ const alternatives = schema[keyword];
138
+ if (!Array.isArray(alternatives))
139
+ continue;
140
+ const values = [];
141
+ for (const alternative of alternatives) {
142
+ if (typeof alternative !== 'object' ||
143
+ alternative === null ||
144
+ Array.isArray(alternative)) {
145
+ return undefined;
146
+ }
147
+ const nested = finiteStringValues(alternative);
148
+ if (!nested)
149
+ return undefined;
150
+ values.push(...nested);
151
+ }
152
+ return values;
153
+ }
154
+ return undefined;
155
+ }
126
156
  function createResponses(target, registry) {
127
157
  const grouped = new Map();
128
- for (const [variant, response] of Object.entries(target.definition.responses)) {
158
+ for (const [name, response] of Object.entries(target.definition.responses)) {
129
159
  const current = grouped.get(response.status) ?? [];
130
- current.push({ variant, response });
160
+ current.push({ name, response });
131
161
  grouped.set(response.status, current);
132
162
  }
133
163
  return Object.fromEntries([...grouped.entries()]
@@ -142,50 +172,64 @@ function createResponse(target, entries, registry) {
142
172
  if (streamKinds.size > 1) {
143
173
  throw openApiError('LUTRE_OPENAPI_RESPONSE_STREAM_001', `${describeTarget(target)} declares streaming and non-streaming responses with the same status.`);
144
174
  }
145
- const schemas = entries.map(({ variant, response }) => registry.reference(response.body, 'output', componentName(target, `Response_${variant}_Output`)));
175
+ const schemas = entries
176
+ .filter(({ response }) => response.body !== undefined)
177
+ .map(({ name, response }) => registry.reference(response.body, 'output', componentName(target, `Response_${name}_Output`)));
146
178
  const payloadSchema = schemas.length === 1 ? schemas[0] : { oneOf: schemas };
147
179
  const streaming = entries[0]?.response.stream === 'server';
148
180
  const headers = mergeResponseHeaders(entries, registry, target);
149
181
  const descriptions = [
150
- ...new Set(entries.map(({ variant, response }) => response.description ?? variant)),
182
+ ...new Set(entries.map(({ name, response }) => response.description ?? name)),
151
183
  ];
152
184
  return {
153
185
  description: descriptions.join(' / '),
154
186
  ...(headers === undefined ? {} : { headers }),
155
- content: streaming
156
- ? {
157
- 'text/event-stream': {
158
- itemSchema: {
159
- type: 'object',
160
- required: ['data'],
161
- properties: {
162
- data: {
163
- type: 'string',
164
- contentMediaType: 'application/json',
165
- contentSchema: payloadSchema,
187
+ ...(schemas.length === 0
188
+ ? {}
189
+ : {
190
+ content: streaming
191
+ ? {
192
+ 'text/event-stream': {
193
+ itemSchema: {
194
+ type: 'object',
195
+ required: ['data'],
196
+ properties: {
197
+ data: {
198
+ type: 'string',
199
+ contentMediaType: 'application/json',
200
+ contentSchema: payloadSchema,
201
+ },
202
+ },
166
203
  },
167
204
  },
205
+ }
206
+ : {
207
+ 'application/json': {
208
+ schema: payloadSchema,
209
+ },
168
210
  },
169
- },
170
- }
171
- : {
172
- 'application/json': {
173
- schema: payloadSchema,
174
- },
175
- },
211
+ }),
176
212
  };
177
213
  }
178
214
  function mergeResponseHeaders(entries, registry, target) {
179
215
  const result = new Map();
180
- for (const { variant, response } of entries) {
181
- if (!response.headers)
216
+ for (const { name, response } of entries) {
217
+ for (const [headerName, value] of Object.entries(responseHeadersDefaults(response.headers) ?? {})) {
218
+ if (value === undefined)
219
+ continue;
220
+ const current = result.get(headerName) ?? [];
221
+ current.push(responseHeaderDefaultSchema(value));
222
+ result.set(headerName, current);
223
+ }
224
+ const headerSchema = responseHeadersSchema(response.headers);
225
+ if (!headerSchema)
182
226
  continue;
183
- const materialized = registry.materialize(response.headers, 'output', componentName(target, `Response_${variant}_Headers_Output`));
227
+ const materialized = registry.materialize(headerSchema, 'output', componentName(target, `Response_${name}_Headers_Output`));
184
228
  const properties = objectProperties(materialized.schema, 'LUTRE_OPENAPI_HEADER_SCHEMA_001', `${describeTarget(target)} response headers`);
185
- for (const [name, schema] of Object.entries(properties)) {
186
- const current = result.get(name) ?? [];
229
+ for (const [headerName, schema] of Object.entries(properties)) {
230
+ const current = result.get(headerName) ?? [];
187
231
  current.push(schema);
188
- result.set(name, current);
232
+ result.set(headerName, current);
189
233
  }
190
234
  }
191
235
  if (result.size === 0)
@@ -199,13 +243,47 @@ function mergeResponseHeaders(entries, registry, target) {
199
243
  },
200
244
  ]));
201
245
  }
246
+ function responseHeadersSchema(headers) {
247
+ if (isStandardSchema(headers))
248
+ return headers;
249
+ if (isResponseHeadersWithDefaults(headers))
250
+ return headers.schema;
251
+ return undefined;
252
+ }
253
+ function responseHeadersDefaults(headers) {
254
+ if (headers === undefined || isStandardSchema(headers))
255
+ return undefined;
256
+ if (isResponseHeadersWithDefaults(headers))
257
+ return headers.defaults;
258
+ return headers;
259
+ }
260
+ function isStandardSchema(value) {
261
+ return typeof value === 'object' && value !== null && '~standard' in value;
262
+ }
263
+ function isResponseHeadersWithDefaults(value) {
264
+ return (typeof value === 'object' &&
265
+ value !== null &&
266
+ 'schema' in value &&
267
+ isStandardSchema(value.schema));
268
+ }
269
+ function responseHeaderDefaultSchema(value) {
270
+ if (typeof value === 'string')
271
+ return { type: 'string', const: value };
272
+ return {
273
+ type: 'array',
274
+ items: { type: 'string' },
275
+ const: [...value],
276
+ };
277
+ }
202
278
  function headerParameters(schema, registry, target, suffix) {
203
279
  const materialized = registry.materialize(schema, 'input', componentName(target, suffix));
204
280
  const properties = objectProperties(materialized.schema, 'LUTRE_OPENAPI_HEADER_SCHEMA_001', `${describeTarget(target)} request headers`);
205
281
  const required = new Set(Array.isArray(materialized.schema.required)
206
282
  ? materialized.schema.required.filter((value) => typeof value === 'string')
207
283
  : []);
208
- return Object.entries(properties).map(([name, propertySchema]) => ({
284
+ return Object.entries(properties)
285
+ .filter(([name]) => name.toLowerCase() !== 'content-type')
286
+ .map(([name, propertySchema]) => ({
209
287
  name,
210
288
  in: 'header',
211
289
  required: required.has(name),
@@ -240,20 +318,27 @@ function attachOperation(paths, definition, operation) {
240
318
  }
241
319
  additional[methodName] = operation;
242
320
  }
243
- function collectModules(roots) {
244
- const result = [];
245
- const seen = new Set();
246
- const visit = (module) => {
247
- if (seen.has(module))
248
- return;
249
- seen.add(module);
250
- for (const imported of module.definition.imports ?? [])
251
- visit(imported);
252
- result.push(module);
321
+ function rebaseLocalDefinitions(schema, schemaComponentName) {
322
+ const visit = (value) => {
323
+ if (Array.isArray(value))
324
+ return value.map(visit);
325
+ if (typeof value !== 'object' || value === null)
326
+ return value;
327
+ const result = {};
328
+ for (const [key, child] of Object.entries(value)) {
329
+ if (key === '$ref' &&
330
+ typeof child === 'string' &&
331
+ child.startsWith('#/$defs/')) {
332
+ result[key] =
333
+ `#/components/schemas/${escapeJsonPointer(schemaComponentName)}/$defs/${child.slice('#/$defs/'.length)}`;
334
+ }
335
+ else {
336
+ result[key] = visit(child);
337
+ }
338
+ }
339
+ return result;
253
340
  };
254
- for (const root of roots)
255
- visit(root);
256
- return result;
341
+ return visit(schema);
257
342
  }
258
343
  class SchemaRegistry {
259
344
  bySchema = new Map();
@@ -294,28 +379,6 @@ class SchemaRegistry {
294
379
  return this.schemas;
295
380
  }
296
381
  }
297
- function rebaseLocalDefinitions(schema, schemaComponentName) {
298
- const visit = (value) => {
299
- if (Array.isArray(value))
300
- return value.map(visit);
301
- if (typeof value !== 'object' || value === null)
302
- return value;
303
- const result = {};
304
- for (const [key, child] of Object.entries(value)) {
305
- if (key === '$ref' &&
306
- typeof child === 'string' &&
307
- child.startsWith('#/$defs/')) {
308
- result[key] =
309
- `#/components/schemas/${escapeJsonPointer(schemaComponentName)}/$defs/${child.slice('#/$defs/'.length)}`;
310
- }
311
- else {
312
- result[key] = visit(child);
313
- }
314
- }
315
- return result;
316
- };
317
- return visit(schema);
318
- }
319
382
  function componentName(target, suffix) {
320
383
  return sanitizeComponentName(`${target.definition.method} ${target.definition.path} ${target.procedure} ${suffix}`);
321
384
  }
@@ -1,41 +1,3 @@
1
- type AsciiLowercase = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
2
- type AsciiLetter = AsciiLowercase | Uppercase<AsciiLowercase>;
3
- type AsciiDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
4
- type ParamNameStart = AsciiLetter | '_';
5
- type ParamNameContinuation = ParamNameStart | AsciiDigit;
6
- type IsParamNameRest<TName extends string> = TName extends '' ? true : TName extends `${ParamNameContinuation}${infer TRest}` ? IsParamNameRest<TRest> : false;
7
- type IsParamName<TName extends string> = TName extends `${ParamNameStart}${infer TRest}` ? IsParamNameRest<TRest> : false;
8
- type IsStaticSegment<TSegment extends string> = TSegment extends '' ? false : TSegment extends `${string}{${string}` | `${string}}${string}` ? false : TSegment extends `${string}?${string}` | `${string}#${string}` ? false : true;
9
- type ValidatePathSegments<TSegments extends string, TSeen extends string = never> = TSegments extends `${infer THead}/${infer TTail}` ? ValidatePathSegment<THead, TSeen> extends infer TResult ? TResult extends {
10
- readonly valid: true;
11
- readonly seen: infer TNext extends string;
12
- } ? ValidatePathSegments<TTail, TNext> : false : false : ValidatePathSegment<TSegments, TSeen> extends {
13
- readonly valid: true;
14
- } ? true : false;
15
- type ValidatePathSegment<TSegment extends string, TSeen extends string> = TSegment extends `{${infer TName}}` ? IsParamName<TName> extends true ? TName extends TSeen ? {
16
- readonly valid: false;
17
- } : {
18
- readonly valid: true;
19
- readonly seen: TSeen | TName;
20
- } : {
21
- readonly valid: false;
22
- } : IsStaticSegment<TSegment> extends true ? {
23
- readonly valid: true;
24
- readonly seen: TSeen;
25
- } : {
26
- readonly valid: false;
27
- };
28
- export type IsValidHttpPath<TPath extends string> = string extends TPath ? false : TPath extends '/' ? true : TPath extends `/${infer TSegments}` ? ValidatePathSegments<TSegments> : false;
29
- type PathParamNamesFromSegments<TSegments extends string> = TSegments extends `${infer THead}/${infer TTail}` ? PathParamName<THead> | PathParamNamesFromSegments<TTail> : PathParamName<TSegments>;
30
- type PathParamName<TSegment extends string> = TSegment extends `{${infer TName}}` ? TName : never;
31
- export type PathParamNames<TPath extends string> = TPath extends `/${infer TSegments}` ? PathParamNamesFromSegments<TSegments> : never;
32
- export type RawPathParams<TPath extends string> = Readonly<{
33
- [TName in PathParamNames<TPath>]: string;
34
- }>;
35
- type NormalizeHttpPathSegments<TSegments extends string> = TSegments extends `${infer THead}/${infer TTail}` ? `${NormalizeHttpPathSegment<THead>}/${NormalizeHttpPathSegments<TTail>}` : NormalizeHttpPathSegment<TSegments>;
36
- type NormalizeHttpPathSegment<TSegment extends string> = TSegment extends `{${string}}` ? '{}' : TSegment;
37
- export type NormalizeHttpPath<TPath extends string> = TPath extends '/' ? '/' : string extends TPath ? string : TPath extends `/${infer TSegments}` ? `/${NormalizeHttpPathSegments<TSegments>}` : never;
38
- export type HttpDispatchKey<TMethod extends string, TPath extends string> = `http:${Uppercase<TMethod>}:${NormalizeHttpPath<TPath>}`;
39
1
  export type HttpPathSegment = {
40
2
  readonly kind: 'static';
41
3
  readonly value: string;
@@ -43,6 +5,8 @@ export type HttpPathSegment = {
43
5
  readonly kind: 'param';
44
6
  readonly name: string;
45
7
  };
8
+ export type PathParamNames<TPath extends string> = TPath extends `${string}{${infer TName}}${infer TRest}` ? TName | PathParamNames<TRest> : never;
9
+ export declare function assertValidHttpMethod(method: string): void;
46
10
  export declare function parseHttpPath(path: string): readonly HttpPathSegment[];
47
11
  export declare function normalizeHttpPath(segments: readonly HttpPathSegment[]): string;
48
12
  export declare function createHttpDispatchKey(method: string, segments: readonly HttpPathSegment[]): string;
@@ -52,4 +16,3 @@ export declare class HttpPathDecodeError extends Error {
52
16
  readonly cause: unknown;
53
17
  constructor(cause: unknown);
54
18
  }
55
- export {};
package/dist/http/path.js CHANGED
@@ -1,4 +1,10 @@
1
1
  const PARAM_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
2
+ const HTTP_METHOD_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
3
+ export function assertValidHttpMethod(method) {
4
+ if (!HTTP_METHOD_PATTERN.test(method)) {
5
+ throw new Error(`Invalid HTTP method: ${JSON.stringify(method)}`);
6
+ }
7
+ }
2
8
  export function parseHttpPath(path) {
3
9
  if (path === '/')
4
10
  return Object.freeze([]);
@@ -50,8 +56,7 @@ export function matchHttpPath(segments, pathname) {
50
56
  if (requestSegments.length !== segments.length)
51
57
  return undefined;
52
58
  const params = {};
53
- for (let index = 0; index < segments.length; index += 1) {
54
- const routeSegment = segments[index];
59
+ for (const [index, routeSegment] of segments.entries()) {
55
60
  const requestSegment = requestSegments[index];
56
61
  if (routeSegment.kind === 'static') {
57
62
  if (routeSegment.value !== requestSegment)
@@ -0,0 +1,17 @@
1
+ import { type StandardSchemaV1 } from '../../core/index.js';
2
+ export declare function decodeQuery(searchParams: URLSearchParams): Readonly<Record<string, string | string[]>>;
3
+ export declare function validateRequestHeaders(schema: StandardSchemaV1, bodyDeclared: boolean, headers: Headers): Promise<unknown>;
4
+ export declare function validatedContentType(headers: unknown): string;
5
+ export declare function decodeBody(request: Request, contentType: string): Promise<unknown>;
6
+ export declare class HttpInputDecodeError extends Error {
7
+ readonly cause: unknown;
8
+ constructor(cause: unknown);
9
+ }
10
+ export declare class HttpInputValidationError extends Error {
11
+ readonly cause: unknown;
12
+ constructor(cause: unknown);
13
+ }
14
+ export declare class HttpUnsupportedMediaTypeError extends Error {
15
+ readonly mediaType: string | undefined;
16
+ constructor(mediaType: string | undefined);
17
+ }
@@ -0,0 +1,109 @@
1
+ import { SchemaValidationError, validateSchema, } from '../../core/index.js';
2
+ export function decodeQuery(searchParams) {
3
+ const query = {};
4
+ for (const [key, value] of searchParams) {
5
+ const current = query[key];
6
+ query[key] =
7
+ current === undefined
8
+ ? value
9
+ : Array.isArray(current)
10
+ ? [...current, value]
11
+ : [current, value];
12
+ }
13
+ return query;
14
+ }
15
+ export async function validateRequestHeaders(schema, bodyDeclared, headers) {
16
+ try {
17
+ return await validateSchema(schema, requestHeadersForValidation(headers));
18
+ }
19
+ catch (error) {
20
+ if (bodyDeclared &&
21
+ error instanceof SchemaValidationError &&
22
+ hasContentTypeIssue(error)) {
23
+ throw new HttpUnsupportedMediaTypeError(normalizeMediaType(headers.get('content-type')));
24
+ }
25
+ throw error;
26
+ }
27
+ }
28
+ export function validatedContentType(headers) {
29
+ if (typeof headers !== 'object' || headers === null) {
30
+ throw new TypeError('HTTP body requires validated request headers');
31
+ }
32
+ const contentType = headers['content-type'];
33
+ if (typeof contentType !== 'string' || contentType.length === 0) {
34
+ throw new TypeError('HTTP body requires request.headers content-type to resolve to a string');
35
+ }
36
+ return contentType;
37
+ }
38
+ export async function decodeBody(request, contentType) {
39
+ const mediaType = normalizeMediaType(contentType);
40
+ if (mediaType === 'application/json' || mediaType.endsWith('+json')) {
41
+ try {
42
+ return await request.json();
43
+ }
44
+ catch (error) {
45
+ throw new HttpInputDecodeError(error);
46
+ }
47
+ }
48
+ if (mediaType === 'multipart/form-data') {
49
+ try {
50
+ return await request.formData();
51
+ }
52
+ catch (error) {
53
+ throw new HttpInputDecodeError(error);
54
+ }
55
+ }
56
+ if (mediaType.startsWith('text/')) {
57
+ try {
58
+ return await request.text();
59
+ }
60
+ catch (error) {
61
+ throw new HttpInputDecodeError(error);
62
+ }
63
+ }
64
+ return request.body;
65
+ }
66
+ function requestHeadersForValidation(headers) {
67
+ const decoded = Object.fromEntries(headers.entries());
68
+ const contentType = normalizeMediaType(headers.get('content-type'));
69
+ if (contentType)
70
+ decoded['content-type'] = contentType;
71
+ return decoded;
72
+ }
73
+ function hasContentTypeIssue(error) {
74
+ return error.issues.some((issue) => {
75
+ const first = issue.path?.[0];
76
+ const key = typeof first === 'object' && first !== null && 'key' in first
77
+ ? first.key
78
+ : first;
79
+ return key === 'content-type';
80
+ });
81
+ }
82
+ function normalizeMediaType(value) {
83
+ const normalized = value?.split(';', 1)[0]?.trim().toLowerCase();
84
+ return normalized ? normalized : undefined;
85
+ }
86
+ export class HttpInputDecodeError extends Error {
87
+ cause;
88
+ constructor(cause) {
89
+ super('HTTP request body decode failed', { cause });
90
+ this.cause = cause;
91
+ this.name = 'HttpInputDecodeError';
92
+ }
93
+ }
94
+ export class HttpInputValidationError extends Error {
95
+ cause;
96
+ constructor(cause) {
97
+ super('HTTP request body validation failed', { cause });
98
+ this.cause = cause;
99
+ this.name = 'HttpInputValidationError';
100
+ }
101
+ }
102
+ export class HttpUnsupportedMediaTypeError extends Error {
103
+ mediaType;
104
+ constructor(mediaType) {
105
+ super('HTTP request content type is unsupported by the Contract');
106
+ this.mediaType = mediaType;
107
+ this.name = 'HttpUnsupportedMediaTypeError';
108
+ }
109
+ }
@@ -0,0 +1,18 @@
1
+ import { type StandardSchemaV1 } from '../../core/index.js';
2
+ type HttpHeaderValue = string | readonly string[];
3
+ type HttpHeaders = Readonly<Record<string, HttpHeaderValue | undefined>>;
4
+ interface HttpResponseHeadersWithDefaults {
5
+ readonly schema: StandardSchemaV1;
6
+ readonly defaults: HttpHeaders;
7
+ }
8
+ type HttpResponseHeadersDefinition = StandardSchemaV1 | HttpHeaders | HttpResponseHeadersWithDefaults;
9
+ export declare function validateResponseHeaders(schema: StandardSchemaV1 | undefined, headers: HttpHeaders | undefined): Promise<HttpHeaders | undefined>;
10
+ export declare function responseHeadersSchema(headers: HttpResponseHeadersDefinition | undefined): StandardSchemaV1 | undefined;
11
+ export declare function responseHeadersDefaults(headers: HttpResponseHeadersDefinition | undefined): HttpHeaders | undefined;
12
+ export declare function isStandardSchema(value: unknown): value is StandardSchemaV1;
13
+ export declare function isResponseHeadersWithDefaults(value: unknown): value is HttpResponseHeadersWithDefaults;
14
+ export declare function mergeResponseHeaders(defaults: HttpHeaders | undefined, dynamic: HttpHeaders | undefined): Headers;
15
+ export declare function applyResponseHeaders(headers: Headers, source: HttpHeaders | undefined): void;
16
+ export declare function applyFrameworkHeadersToResponse(response: Response, source: Headers | undefined): Response;
17
+ export declare function applyFrameworkResponseHeaders(headers: Headers, source: HttpHeaders | undefined): void;
18
+ export {};