@loutrejs/loutre 0.5.0 → 0.7.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 (188) 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 +22 -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 +118 -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 +32 -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/instrumentation.d.ts +36 -0
  31. package/dist/core/model/context.d.ts +18 -0
  32. package/dist/core/model/context.js +34 -0
  33. package/dist/core/model/lifecycle.d.ts +5 -0
  34. package/dist/core/model/lifecycle.js +44 -0
  35. package/dist/core/model/modules.d.ts +2 -0
  36. package/dist/core/model/modules.js +101 -0
  37. package/dist/core/model/providers.d.ts +4 -0
  38. package/dist/core/model/providers.js +74 -0
  39. package/dist/core/model/snapshot.d.ts +6 -0
  40. package/dist/core/model/snapshot.js +31 -0
  41. package/dist/core/model/types.d.ts +78 -0
  42. package/dist/core/model/types.js +1 -0
  43. package/dist/core/model/validation.d.ts +4 -0
  44. package/dist/core/model/validation.js +40 -0
  45. package/dist/core/model/visibility.d.ts +4 -0
  46. package/dist/core/model/visibility.js +20 -0
  47. package/dist/core/model.d.ts +10 -0
  48. package/dist/core/model.js +346 -0
  49. package/dist/core/module.d.ts +8 -13
  50. package/dist/core/source-location.d.ts +8 -0
  51. package/dist/core/source-location.js +31 -0
  52. package/dist/core/token-internal.d.ts +3 -0
  53. package/dist/core/token-internal.js +4 -0
  54. package/dist/core/type.d.ts +7 -0
  55. package/dist/core/type.js +4 -0
  56. package/dist/devtools/capsule.d.ts +51 -0
  57. package/dist/devtools/capsule.js +245 -0
  58. package/dist/devtools/index.d.ts +11 -0
  59. package/dist/devtools/index.js +6 -0
  60. package/dist/devtools/inspector.d.ts +57 -0
  61. package/dist/devtools/inspector.js +1 -0
  62. package/dist/devtools/module.d.ts +18 -0
  63. package/dist/devtools/module.js +26 -0
  64. package/dist/devtools/observer.d.ts +77 -0
  65. package/dist/devtools/observer.js +10 -0
  66. package/dist/devtools/protocol.d.ts +1 -0
  67. package/dist/devtools/protocol.js +1 -0
  68. package/dist/devtools/provider-instrumentation.d.ts +14 -0
  69. package/dist/devtools/provider-instrumentation.js +117 -0
  70. package/dist/devtools/runtime.d.ts +20 -0
  71. package/dist/devtools/runtime.js +519 -0
  72. package/dist/devtools/values.d.ts +35 -0
  73. package/dist/devtools/values.js +175 -0
  74. package/dist/generated/version.d.ts +1 -1
  75. package/dist/generated/version.js +1 -1
  76. package/dist/graph/index.d.ts +2 -3
  77. package/dist/graph/index.js +1 -3
  78. package/dist/graph/model.d.ts +34 -0
  79. package/dist/graph/model.js +127 -0
  80. package/dist/http/auth.d.ts +46 -0
  81. package/dist/http/auth.js +96 -0
  82. package/dist/http/client.d.ts +28 -31
  83. package/dist/http/client.js +58 -44
  84. package/dist/http/cors-internal.d.ts +29 -0
  85. package/dist/http/cors-internal.js +160 -0
  86. package/dist/http/cors.d.ts +5 -8
  87. package/dist/http/cors.js +7 -165
  88. package/dist/http/extension.d.ts +348 -0
  89. package/dist/http/extension.js +1061 -0
  90. package/dist/http/index.d.ts +9 -7
  91. package/dist/http/index.js +5 -7
  92. package/dist/{application → http}/openapi.d.ts +12 -3
  93. package/dist/{application → http}/openapi.js +164 -102
  94. package/dist/http/path.d.ts +1 -42
  95. package/dist/http/path.js +1 -2
  96. package/dist/http/runtime/request.d.ts +17 -0
  97. package/dist/http/runtime/request.js +109 -0
  98. package/dist/http/runtime/response.d.ts +18 -0
  99. package/dist/http/runtime/response.js +108 -0
  100. package/dist/index.d.ts +4 -0
  101. package/dist/index.js +2 -0
  102. package/dist/message-port/extension.d.ts +66 -0
  103. package/dist/message-port/extension.js +411 -0
  104. package/dist/message-port/index.d.ts +4 -73
  105. package/dist/message-port/index.js +2 -189
  106. package/dist/message-port/transport.d.ts +11 -0
  107. package/dist/message-port/transport.js +45 -0
  108. package/dist/runtime/async-iterator-lifecycle.d.ts +12 -0
  109. package/dist/runtime/async-iterator-lifecycle.js +95 -0
  110. package/dist/runtime/capability.d.ts +11 -11
  111. package/dist/runtime/capability.js +3 -3
  112. package/dist/runtime/di.d.ts +5 -21
  113. package/dist/runtime/di.js +22 -289
  114. package/dist/runtime/execution-tracker.d.ts +8 -0
  115. package/dist/runtime/execution-tracker.js +54 -0
  116. package/dist/runtime/index.d.ts +15 -9
  117. package/dist/runtime/index.js +9 -9
  118. package/dist/runtime/ingress-gate.d.ts +7 -0
  119. package/dist/runtime/ingress-gate.js +33 -0
  120. package/dist/runtime/kernel.d.ts +25 -0
  121. package/dist/runtime/kernel.js +447 -0
  122. package/dist/runtime/logger.d.ts +2 -0
  123. package/dist/runtime/logger.js +2 -0
  124. package/dist/runtime/node-capabilities.d.ts +1 -1
  125. package/dist/runtime/node-capabilities.js +1 -1
  126. package/dist/runtime/server-port.d.ts +2 -0
  127. package/dist/runtime/server-port.js +11 -0
  128. package/dist/tasks/extension.d.ts +144 -0
  129. package/dist/tasks/extension.js +525 -0
  130. package/dist/tasks/index.d.ts +2 -0
  131. package/dist/tasks/index.js +1 -0
  132. package/dist/tasks/runtime/cron.d.ts +6 -0
  133. package/dist/tasks/runtime/cron.js +89 -0
  134. package/dist/websocket/errors.d.ts +9 -0
  135. package/dist/websocket/errors.js +18 -0
  136. package/dist/websocket/extension.d.ts +154 -0
  137. package/dist/websocket/extension.js +533 -0
  138. package/dist/websocket/index.d.ts +3 -0
  139. package/dist/websocket/index.js +2 -0
  140. package/package.json +16 -16
  141. package/dist/application/binding.d.ts +0 -33
  142. package/dist/application/binding.js +0 -295
  143. package/dist/application/host.d.ts +0 -8
  144. package/dist/application/host.js +0 -14
  145. package/dist/application/queue.d.ts +0 -3
  146. package/dist/application/queue.js +0 -4
  147. package/dist/application/scheduler.d.ts +0 -2
  148. package/dist/application/scheduler.js +0 -45
  149. package/dist/core/context-key.d.ts +0 -16
  150. package/dist/core/context-key.js +0 -17
  151. package/dist/core/contract-internal.d.ts +0 -22
  152. package/dist/core/contract-internal.js +0 -61
  153. package/dist/core/contract.d.ts +0 -96
  154. package/dist/core/contract.js +0 -88
  155. package/dist/core/execution.d.ts +0 -78
  156. package/dist/core/execution.js +0 -61
  157. package/dist/core/implementation.d.ts +0 -44
  158. package/dist/core/implementation.js +0 -35
  159. package/dist/core/layer.d.ts +0 -137
  160. package/dist/core/layer.js +0 -81
  161. package/dist/graph/compiler.d.ts +0 -4
  162. package/dist/graph/compiler.js +0 -228
  163. package/dist/graph/graph.d.ts +0 -18
  164. package/dist/graph/graph.js +0 -1118
  165. package/dist/graph/ir.d.ts +0 -175
  166. package/dist/http/application.d.ts +0 -18
  167. package/dist/http/application.js +0 -515
  168. package/dist/http/basic-auth.d.ts +0 -37
  169. package/dist/http/basic-auth.js +0 -67
  170. package/dist/http/definitions.d.ts +0 -363
  171. package/dist/http/definitions.js +0 -310
  172. package/dist/http/hosted-protocol.d.ts +0 -4
  173. package/dist/http/hosted-protocol.js +0 -4
  174. package/dist/http/internal.d.ts +0 -8
  175. package/dist/http/internal.js +0 -1
  176. package/dist/http/params.d.ts +0 -2
  177. package/dist/http/params.js +0 -19
  178. package/dist/http/request-body.d.ts +0 -4
  179. package/dist/http/request-body.js +0 -6
  180. package/dist/http/validate.d.ts +0 -20
  181. package/dist/http/validate.js +0 -6
  182. package/dist/message-port/environment.d.ts +0 -2
  183. package/dist/message-port/environment.js +0 -4
  184. package/dist/runtime/application.d.ts +0 -38
  185. package/dist/runtime/application.js +0 -339
  186. package/dist/runtime/pipeline.d.ts +0 -11
  187. package/dist/runtime/pipeline.js +0 -161
  188. /package/dist/{graph/ir.js → core/instrumentation.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,7 +1,5 @@
1
- import { supportsJsonSchema, } from '../core/index.js';
2
- import { resolveContractProcedureIdentity } from '../core/contract-internal.js';
3
- import { assertValidCompilation, compileApplication } from '../graph/index.js';
4
- import { httpRequestBodyContentType, httpRequestBodySchema, } from '../http/index.js';
1
+ import { supportsJsonSchema, assertValidApplicationModel, } from '../core/index.js';
2
+ import { httpExecutionExtension, } from './extension.js';
5
3
  const FIXED_METHODS = new Set([
6
4
  'get',
7
5
  'put',
@@ -13,40 +11,29 @@ const FIXED_METHODS = new Set([
13
11
  'trace',
14
12
  'query',
15
13
  ]);
16
- export function generateOpenApi(application, options) {
17
- assertValidCompilation(compileApplication({
18
- modules: application.modules,
19
- ...(application.arguments === undefined
20
- ? {}
21
- : { arguments: application.arguments }),
22
- tasks: application.tasks,
23
- triggers: application.triggers,
24
- }));
14
+ export function generateOpenApi(source, options) {
15
+ const model = 'model' in source ? source.model : source;
16
+ assertValidApplicationModel(model);
25
17
  const registry = new SchemaRegistry();
26
18
  const paths = {};
27
19
  const operationIds = new Set();
28
- for (const module of collectModules(application.modules)) {
29
- for (const implementation of module.definition.implementations ?? []) {
30
- if (implementation.protocol !== 'http')
31
- continue;
32
- for (const procedure of implementation.procedures) {
33
- const protocol = implementation.contract.procedures[procedure]?.protocols.http;
34
- if (!protocol || protocol.protocol !== 'http')
35
- continue;
36
- const typed = protocol;
37
- const target = {
38
- definition: typed.definition,
39
- procedure: resolveContractProcedureIdentity(implementation.contract, procedure).procedure,
40
- };
41
- const operation = createOperation(target, registry, operationIds, options.operationId);
42
- attachOperation(paths, typed.definition, operation);
43
- }
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));
44
32
  }
45
33
  }
46
34
  const schemas = registry.components();
47
35
  return {
48
36
  openapi: '3.2.0',
49
- jsonSchemaDialect: 'https://json-schema.org/draft/2020-12/schema',
50
37
  info: options.info,
51
38
  ...(options.servers === undefined ? {} : { servers: options.servers }),
52
39
  paths,
@@ -83,6 +70,7 @@ function createOperation(target, registry, operationIds, resolveOperationId) {
83
70
  }
84
71
  if (request?.query) {
85
72
  parameters.push({
73
+ name: 'query',
86
74
  in: 'querystring',
87
75
  content: {
88
76
  'application/x-www-form-urlencoded': {
@@ -95,7 +83,7 @@ function createOperation(target, registry, operationIds, resolveOperationId) {
95
83
  parameters.push(...headerParameters(request.headers, registry, target, 'RequestHeaders_Input'));
96
84
  }
97
85
  const requestBody = request?.body
98
- ? createRequestBody(request.body, registry, target)
86
+ ? createRequestBody(request.body, request.headers, registry, target)
99
87
  : undefined;
100
88
  return {
101
89
  ...(definition.tags === undefined ? {} : { tags: definition.tags }),
@@ -114,21 +102,62 @@ function createOperation(target, registry, operationIds, resolveOperationId) {
114
102
  : { deprecated: definition.deprecated }),
115
103
  };
116
104
  }
117
- function createRequestBody(body, registry, target) {
118
- 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'));
119
108
  return {
120
- content: {
121
- [contentType]: {
122
- schema: registry.reference(httpRequestBodySchema(body), 'input', componentName(target, 'RequestBody_Input')),
123
- },
124
- },
109
+ content: Object.fromEntries(mediaTypes.map((mediaType) => [mediaType, { schema }])),
125
110
  };
126
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
+ }
127
156
  function createResponses(target, registry) {
128
157
  const grouped = new Map();
129
- for (const [variant, response] of Object.entries(target.definition.responses)) {
158
+ for (const [name, response] of Object.entries(target.definition.responses)) {
130
159
  const current = grouped.get(response.status) ?? [];
131
- current.push({ variant, response });
160
+ current.push({ name, response });
132
161
  grouped.set(response.status, current);
133
162
  }
134
163
  return Object.fromEntries([...grouped.entries()]
@@ -143,50 +172,64 @@ function createResponse(target, entries, registry) {
143
172
  if (streamKinds.size > 1) {
144
173
  throw openApiError('LUTRE_OPENAPI_RESPONSE_STREAM_001', `${describeTarget(target)} declares streaming and non-streaming responses with the same status.`);
145
174
  }
146
- 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`)));
147
178
  const payloadSchema = schemas.length === 1 ? schemas[0] : { oneOf: schemas };
148
179
  const streaming = entries[0]?.response.stream === 'server';
149
180
  const headers = mergeResponseHeaders(entries, registry, target);
150
181
  const descriptions = [
151
- ...new Set(entries.map(({ variant, response }) => response.description ?? variant)),
182
+ ...new Set(entries.map(({ name, response }) => response.description ?? name)),
152
183
  ];
153
184
  return {
154
185
  description: descriptions.join(' / '),
155
186
  ...(headers === undefined ? {} : { headers }),
156
- content: streaming
157
- ? {
158
- 'text/event-stream': {
159
- itemSchema: {
160
- type: 'object',
161
- required: ['data'],
162
- properties: {
163
- data: {
164
- type: 'string',
165
- contentMediaType: 'application/json',
166
- 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
+ },
167
203
  },
168
204
  },
205
+ }
206
+ : {
207
+ 'application/json': {
208
+ schema: payloadSchema,
209
+ },
169
210
  },
170
- },
171
- }
172
- : {
173
- 'application/json': {
174
- schema: payloadSchema,
175
- },
176
- },
211
+ }),
177
212
  };
178
213
  }
179
214
  function mergeResponseHeaders(entries, registry, target) {
180
215
  const result = new Map();
181
- for (const { variant, response } of entries) {
182
- 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)
183
226
  continue;
184
- 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`));
185
228
  const properties = objectProperties(materialized.schema, 'LUTRE_OPENAPI_HEADER_SCHEMA_001', `${describeTarget(target)} response headers`);
186
- for (const [name, schema] of Object.entries(properties)) {
187
- const current = result.get(name) ?? [];
229
+ for (const [headerName, schema] of Object.entries(properties)) {
230
+ const current = result.get(headerName) ?? [];
188
231
  current.push(schema);
189
- result.set(name, current);
232
+ result.set(headerName, current);
190
233
  }
191
234
  }
192
235
  if (result.size === 0)
@@ -200,13 +243,47 @@ function mergeResponseHeaders(entries, registry, target) {
200
243
  },
201
244
  ]));
202
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
+ }
203
278
  function headerParameters(schema, registry, target, suffix) {
204
279
  const materialized = registry.materialize(schema, 'input', componentName(target, suffix));
205
280
  const properties = objectProperties(materialized.schema, 'LUTRE_OPENAPI_HEADER_SCHEMA_001', `${describeTarget(target)} request headers`);
206
281
  const required = new Set(Array.isArray(materialized.schema.required)
207
282
  ? materialized.schema.required.filter((value) => typeof value === 'string')
208
283
  : []);
209
- return Object.entries(properties).map(([name, propertySchema]) => ({
284
+ return Object.entries(properties)
285
+ .filter(([name]) => name.toLowerCase() !== 'content-type')
286
+ .map(([name, propertySchema]) => ({
210
287
  name,
211
288
  in: 'header',
212
289
  required: required.has(name),
@@ -241,20 +318,27 @@ function attachOperation(paths, definition, operation) {
241
318
  }
242
319
  additional[methodName] = operation;
243
320
  }
244
- function collectModules(roots) {
245
- const result = [];
246
- const seen = new Set();
247
- const visit = (module) => {
248
- if (seen.has(module))
249
- return;
250
- seen.add(module);
251
- for (const imported of module.definition.imports ?? [])
252
- visit(imported);
253
- 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;
254
340
  };
255
- for (const root of roots)
256
- visit(root);
257
- return result;
341
+ return visit(schema);
258
342
  }
259
343
  class SchemaRegistry {
260
344
  bySchema = new Map();
@@ -295,28 +379,6 @@ class SchemaRegistry {
295
379
  return this.schemas;
296
380
  }
297
381
  }
298
- function rebaseLocalDefinitions(schema, schemaComponentName) {
299
- const visit = (value) => {
300
- if (Array.isArray(value))
301
- return value.map(visit);
302
- if (typeof value !== 'object' || value === null)
303
- return value;
304
- const result = {};
305
- for (const [key, child] of Object.entries(value)) {
306
- if (key === '$ref' &&
307
- typeof child === 'string' &&
308
- child.startsWith('#/$defs/')) {
309
- result[key] =
310
- `#/components/schemas/${escapeJsonPointer(schemaComponentName)}/$defs/${child.slice('#/$defs/'.length)}`;
311
- }
312
- else {
313
- result[key] = visit(child);
314
- }
315
- }
316
- return result;
317
- };
318
- return visit(schema);
319
- }
320
382
  function componentName(target, suffix) {
321
383
  return sanitizeComponentName(`${target.definition.method} ${target.definition.path} ${target.procedure} ${suffix}`);
322
384
  }
@@ -1,44 +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 HttpMethodTokenCharacter = AsciiLetter | AsciiDigit | '!' | '#' | '$' | '%' | '&' | "'" | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|' | '~';
7
- type IsHttpMethodTokenRest<TMethod extends string> = TMethod extends '' ? true : TMethod extends `${HttpMethodTokenCharacter}${infer TRest}` ? IsHttpMethodTokenRest<TRest> : false;
8
- export type IsValidHttpMethod<TMethod extends string> = string extends TMethod ? false : TMethod extends `${HttpMethodTokenCharacter}${infer TRest}` ? IsHttpMethodTokenRest<TRest> : false;
9
- type IsParamNameRest<TName extends string> = TName extends '' ? true : TName extends `${ParamNameContinuation}${infer TRest}` ? IsParamNameRest<TRest> : false;
10
- type IsParamName<TName extends string> = TName extends `${ParamNameStart}${infer TRest}` ? IsParamNameRest<TRest> : false;
11
- type IsStaticSegment<TSegment extends string> = TSegment extends '' ? false : TSegment extends `${string}{${string}` | `${string}}${string}` ? false : TSegment extends `${string}?${string}` | `${string}#${string}` ? false : true;
12
- type ValidatePathSegments<TSegments extends string, TSeen extends string = never> = TSegments extends `${infer THead}/${infer TTail}` ? ValidatePathSegment<THead, TSeen> extends infer TResult ? TResult extends {
13
- readonly valid: true;
14
- readonly seen: infer TNext extends string;
15
- } ? ValidatePathSegments<TTail, TNext> : false : false : ValidatePathSegment<TSegments, TSeen> extends {
16
- readonly valid: true;
17
- } ? true : false;
18
- type ValidatePathSegment<TSegment extends string, TSeen extends string> = TSegment extends `{${infer TName}}` ? IsParamName<TName> extends true ? TName extends TSeen ? {
19
- readonly valid: false;
20
- } : {
21
- readonly valid: true;
22
- readonly seen: TSeen | TName;
23
- } : {
24
- readonly valid: false;
25
- } : IsStaticSegment<TSegment> extends true ? {
26
- readonly valid: true;
27
- readonly seen: TSeen;
28
- } : {
29
- readonly valid: false;
30
- };
31
- export type IsValidHttpPath<TPath extends string> = string extends TPath ? false : TPath extends '/' ? true : TPath extends `/${infer TSegments}` ? ValidatePathSegments<TSegments> : false;
32
- type PathParamNamesFromSegments<TSegments extends string> = TSegments extends `${infer THead}/${infer TTail}` ? PathParamName<THead> | PathParamNamesFromSegments<TTail> : PathParamName<TSegments>;
33
- type PathParamName<TSegment extends string> = TSegment extends `{${infer TName}}` ? TName : never;
34
- export type PathParamNames<TPath extends string> = TPath extends `/${infer TSegments}` ? PathParamNamesFromSegments<TSegments> : never;
35
- export type RawPathParams<TPath extends string> = Readonly<{
36
- [TName in PathParamNames<TPath>]: string;
37
- }>;
38
- type NormalizeHttpPathSegments<TSegments extends string> = TSegments extends `${infer THead}/${infer TTail}` ? `${NormalizeHttpPathSegment<THead>}/${NormalizeHttpPathSegments<TTail>}` : NormalizeHttpPathSegment<TSegments>;
39
- type NormalizeHttpPathSegment<TSegment extends string> = TSegment extends `{${string}}` ? '{}' : TSegment;
40
- export type NormalizeHttpPath<TPath extends string> = TPath extends '/' ? '/' : string extends TPath ? string : TPath extends `/${infer TSegments}` ? `/${NormalizeHttpPathSegments<TSegments>}` : never;
41
- export type HttpDispatchKey<TMethod extends string, TPath extends string> = `http:${Uppercase<TMethod>}:${NormalizeHttpPath<TPath>}`;
42
1
  export type HttpPathSegment = {
43
2
  readonly kind: 'static';
44
3
  readonly value: string;
@@ -46,6 +5,7 @@ export type HttpPathSegment = {
46
5
  readonly kind: 'param';
47
6
  readonly name: string;
48
7
  };
8
+ export type PathParamNames<TPath extends string> = TPath extends `${string}{${infer TName}}${infer TRest}` ? TName | PathParamNames<TRest> : never;
49
9
  export declare function assertValidHttpMethod(method: string): void;
50
10
  export declare function parseHttpPath(path: string): readonly HttpPathSegment[];
51
11
  export declare function normalizeHttpPath(segments: readonly HttpPathSegment[]): string;
@@ -56,4 +16,3 @@ export declare class HttpPathDecodeError extends Error {
56
16
  readonly cause: unknown;
57
17
  constructor(cause: unknown);
58
18
  }
59
- export {};
package/dist/http/path.js CHANGED
@@ -56,8 +56,7 @@ export function matchHttpPath(segments, pathname) {
56
56
  if (requestSegments.length !== segments.length)
57
57
  return undefined;
58
58
  const params = {};
59
- for (let index = 0; index < segments.length; index += 1) {
60
- const routeSegment = segments[index];
59
+ for (const [index, routeSegment] of segments.entries()) {
61
60
  const requestSegment = requestSegments[index];
62
61
  if (routeSegment.kind === 'static') {
63
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 {};