@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,515 +0,0 @@
1
- import { validateSchema, } from '../core/index.js';
2
- import { executePipeline, Logger, normalizeUnknownError, } from '../runtime/index.js';
3
- import { createCorsActualResponseHeaders, createCorsPreflightResponseHeaders, } from './cors.js';
4
- import { compareHttpPathSpecificity, HttpPathDecodeError, matchHttpPath, parseHttpPath, } from './path.js';
5
- import { validateHttpParamsSchemas } from './params.js';
6
- export function createHttpExecution(options) {
7
- const { runtime } = options;
8
- const graph = options.graph;
9
- const applicationLogger = options.logger ?? new Logger();
10
- const runtimeGraph = runtime.graph;
11
- const container = runtime.container;
12
- const routes = collectRoutes(runtimeGraph.modules);
13
- const logger = applicationLogger.child({ protocol: 'http' });
14
- let initialization;
15
- const initialize = () => (initialization ??= runtime.initialize());
16
- return {
17
- graph,
18
- initialize,
19
- shutdown: (signal) => runtime.shutdown(signal),
20
- onServerListening: (url) => options.lifecycle?.onServerListening?.(url),
21
- async handle(request) {
22
- const startedAt = Date.now();
23
- const url = new URL(request.url);
24
- const method = request.method.toUpperCase();
25
- let requestLogger = logger.child({
26
- executionId: crypto.randomUUID(),
27
- method,
28
- path: url.pathname,
29
- });
30
- try {
31
- await initialize();
32
- }
33
- catch (error) {
34
- return logHttpResponse(requestLogger, startedAt, createInternalErrorResponse(error, requestLogger));
35
- }
36
- return runtime.execute(async () => {
37
- if (isCorsPreflightRequest(request)) {
38
- try {
39
- const requestedMethod = request.headers
40
- .get('access-control-request-method')
41
- .toUpperCase();
42
- const preflightMatch = findRoute(routes, requestedMethod, url.pathname);
43
- if (preflightMatch) {
44
- const headers = await createCorsPreflightResponseHeaders(preflightMatch.route.protocol.definition.pipeline, request, preflightMatch.route.method);
45
- if (headers) {
46
- requestLogger = requestLogger.child({
47
- procedure: preflightMatch.route.procedure,
48
- source: `${preflightMatch.route.implementation.name}.${preflightMatch.route.procedure}`,
49
- });
50
- return logHttpResponse(requestLogger, startedAt, new Response(null, { status: 204, headers }));
51
- }
52
- }
53
- }
54
- catch (error) {
55
- return logHttpResponse(requestLogger, startedAt, isDecodeError(error)
56
- ? jsonResponse(400, { error: 'Invalid request' })
57
- : createInternalErrorResponse(error, requestLogger));
58
- }
59
- }
60
- let routeMatch;
61
- try {
62
- routeMatch = findRoute(routes, method, url.pathname);
63
- }
64
- catch (error) {
65
- return logHttpResponse(requestLogger, startedAt, isDecodeError(error)
66
- ? jsonResponse(400, { error: 'Invalid request' })
67
- : createInternalErrorResponse(error, requestLogger));
68
- }
69
- if (!routeMatch?.params) {
70
- return logHttpResponse(requestLogger, startedAt, jsonResponse(404, { error: 'Not Found' }));
71
- }
72
- requestLogger = requestLogger.child({
73
- procedure: routeMatch.route.procedure,
74
- source: `${routeMatch.route.implementation.name}.${routeMatch.route.procedure}`,
75
- });
76
- let corsHeaders;
77
- try {
78
- corsHeaders = await createCorsActualResponseHeaders(routeMatch.route.protocol.definition.pipeline, request);
79
- }
80
- catch (error) {
81
- return logHttpResponse(requestLogger, startedAt, createInternalErrorResponse(error, requestLogger));
82
- }
83
- const complete = (response) => logHttpResponse(requestLogger, startedAt, applyFrameworkHeaders(response, corsHeaders));
84
- try {
85
- const decoded = await decodeRequest(request, url, routeMatch.params, routeMatch.route.protocol.definition);
86
- const raw = {
87
- ...decoded,
88
- logger: requestLogger,
89
- signal: request.signal,
90
- };
91
- const logical = await executePipeline(routeMatch.route.protocol.definition.pipeline, {
92
- context: raw,
93
- layer: (descriptor) => container.layerRuntime(descriptor),
94
- validate: async (layer, context) => {
95
- const declared = routeMatch.route.protocol.definition.request?.[layer.part];
96
- const schema = layer.part === 'body' && declared
97
- ? declared.schema
98
- : declared;
99
- if (schema) {
100
- try {
101
- context[layer.part] =
102
- layer.part === 'params'
103
- ? await validateHttpParamsSchemas(schema, context.params)
104
- : await validateSchema(schema, context[layer.part]);
105
- }
106
- catch (error) {
107
- throw new HttpInputValidationError(error);
108
- }
109
- }
110
- },
111
- terminal: async (_layer, context) => invokeController(routeMatch.route, context, container),
112
- });
113
- return complete(await finalizeResponse(routeMatch.route.protocol.definition, logical, request.signal));
114
- }
115
- catch (error) {
116
- if (error instanceof HttpUnsupportedMediaTypeError) {
117
- return complete(jsonResponse(415, { error: 'Unsupported Media Type' }));
118
- }
119
- if (isDecodeError(error)) {
120
- return complete(jsonResponse(400, { error: 'Invalid request' }));
121
- }
122
- if (isValidationError(error)) {
123
- return complete(jsonResponse(400, { error: 'Validation failed' }));
124
- }
125
- let mapped;
126
- try {
127
- mapped = await mapDeclaredError(routeMatch.route.protocol.definition, error);
128
- }
129
- catch (mappingError) {
130
- return complete(createInternalErrorResponse(mappingError, requestLogger));
131
- }
132
- if (mapped) {
133
- try {
134
- return complete(await finalizeResponse(routeMatch.route.protocol.definition, mapped, request.signal));
135
- }
136
- catch (finalizationError) {
137
- return complete(createInternalErrorResponse(finalizationError, requestLogger));
138
- }
139
- }
140
- return complete(createInternalErrorResponse(error, requestLogger));
141
- }
142
- });
143
- },
144
- };
145
- }
146
- function findRoute(routes, method, pathname) {
147
- const candidate = routes
148
- .filter((route) => route.method === method)
149
- .map((route) => ({
150
- route,
151
- params: matchHttpPath(route.segments, pathname),
152
- }))
153
- .find((match) => match.params !== undefined);
154
- return candidate?.params === undefined
155
- ? undefined
156
- : { route: candidate.route, params: candidate.params };
157
- }
158
- function isCorsPreflightRequest(request) {
159
- return (request.method.toUpperCase() === 'OPTIONS' &&
160
- request.headers.has('origin') &&
161
- request.headers.has('access-control-request-method'));
162
- }
163
- function applyFrameworkHeaders(response, frameworkHeaders) {
164
- if (!frameworkHeaders)
165
- return response;
166
- frameworkHeaders.forEach((value, name) => {
167
- if (name.toLowerCase() === 'vary') {
168
- for (const item of value.split(',')) {
169
- appendVary(response.headers, item.trim());
170
- }
171
- return;
172
- }
173
- response.headers.set(name, value);
174
- });
175
- return response;
176
- }
177
- function appendVary(headers, value) {
178
- if (value.length === 0)
179
- return;
180
- const current = headers.get('vary');
181
- if (current === null) {
182
- headers.set('vary', value);
183
- return;
184
- }
185
- const values = current
186
- .split(',')
187
- .map((item) => item.trim())
188
- .filter(Boolean);
189
- if (!values.some((item) => item.toLowerCase() === value.toLowerCase())) {
190
- values.push(value);
191
- }
192
- headers.set('vary', values.join(', '));
193
- }
194
- function logHttpResponse(logger, startedAt, response) {
195
- logger.info('HTTP request completed', {
196
- event: 'http.request.completed',
197
- status: response.status,
198
- durationMs: Math.max(0, Date.now() - startedAt),
199
- });
200
- return response;
201
- }
202
- function createInternalErrorResponse(error, logger) {
203
- const normalized = normalizeUnknownError(error, logger.context);
204
- logger.error('Unhandled application error', {
205
- event: 'application.error',
206
- error: {
207
- code: normalized.code,
208
- id: normalized.errorId,
209
- name: error instanceof Error ? error.name : typeof error,
210
- message: normalized.message,
211
- ...(normalized.stack === undefined ? {} : { stack: normalized.stack }),
212
- ...(normalized.cause === undefined ? {} : { cause: normalized.cause }),
213
- },
214
- });
215
- return jsonResponse(500, {
216
- error: 'Internal Server Error',
217
- errorId: normalized.errorId,
218
- });
219
- }
220
- async function decodeRequest(request, url, params, definition) {
221
- const query = {};
222
- for (const [key, value] of url.searchParams) {
223
- const current = query[key];
224
- query[key] =
225
- current === undefined
226
- ? value
227
- : Array.isArray(current)
228
- ? [...current, value]
229
- : [current, value];
230
- }
231
- const headers = Object.fromEntries(request.headers.entries());
232
- let body = undefined;
233
- const bodyDefinition = definition.request?.body;
234
- if (bodyDefinition) {
235
- const actualMediaType = normalizeMediaType(request.headers.get('content-type'));
236
- const declaredMediaType = normalizeMediaType(bodyDefinition.contentType);
237
- if (request.body !== null && actualMediaType !== declaredMediaType) {
238
- throw new HttpUnsupportedMediaTypeError(declaredMediaType, actualMediaType);
239
- }
240
- if (declaredMediaType === 'application/json' ||
241
- declaredMediaType.endsWith('+json')) {
242
- try {
243
- body = await request.json();
244
- }
245
- catch (error) {
246
- throw new HttpInputDecodeError(error);
247
- }
248
- }
249
- else if (declaredMediaType === 'multipart/form-data') {
250
- try {
251
- body = await request.formData();
252
- }
253
- catch (error) {
254
- throw new HttpInputDecodeError(error);
255
- }
256
- }
257
- else if (declaredMediaType.startsWith('text/')) {
258
- body = await request.text();
259
- }
260
- else {
261
- body = request.body;
262
- }
263
- }
264
- return { params, query, headers, body };
265
- }
266
- function normalizeMediaType(value) {
267
- const normalized = value?.split(';', 1)[0]?.trim().toLowerCase();
268
- return normalized ? normalized : undefined;
269
- }
270
- async function invokeController(route, raw, container) {
271
- const controller = container.implementationRuntime(route.implementation);
272
- const method = controller[route.procedure];
273
- if (typeof method !== 'function') {
274
- throw new Error(`${route.implementation.name}.${route.procedure} is not callable`);
275
- }
276
- const response = Object.fromEntries(Object.keys(route.protocol.definition.responses).map((variant) => [
277
- variant,
278
- (result) => ({
279
- kind: 'http-result',
280
- variant,
281
- body: result.body,
282
- ...(result.headers === undefined ? {} : { headers: result.headers }),
283
- }),
284
- ]));
285
- const context = {
286
- ...raw,
287
- response,
288
- };
289
- return Reflect.apply(method, controller, [
290
- context,
291
- ]);
292
- }
293
- async function finalizeResponse(definition, result, signal) {
294
- if (result?.kind !== 'http-result') {
295
- throw new Error('Controller returned a non-HTTP logical result');
296
- }
297
- const response = definition.responses[result.variant];
298
- if (!response) {
299
- throw new Error(`Undeclared HTTP response variant: ${result.variant}`);
300
- }
301
- const responseHeaders = await validateResponseHeaders(response.headers, result.headers);
302
- if (response.stream === 'server') {
303
- if (!isAsyncIterable(result.body)) {
304
- throw new Error('Server-stream response requires an AsyncIterable');
305
- }
306
- const encoder = new TextEncoder();
307
- const iterator = result.body[Symbol.asyncIterator]();
308
- let finished = false;
309
- let abort;
310
- const stream = new ReadableStream({
311
- start(controller) {
312
- abort = () => {
313
- if (finished)
314
- return;
315
- finished = true;
316
- signal.removeEventListener('abort', abort);
317
- void iterator.return?.(signal.reason).finally(() => {
318
- controller.error(signal.reason ?? new Error('HTTP request was aborted'));
319
- });
320
- };
321
- if (signal.aborted)
322
- abort();
323
- else
324
- signal.addEventListener('abort', abort, { once: true });
325
- },
326
- async pull(controller) {
327
- if (finished)
328
- return;
329
- try {
330
- const next = await iterator.next();
331
- if (next.done) {
332
- finished = true;
333
- if (abort)
334
- signal.removeEventListener('abort', abort);
335
- controller.close();
336
- return;
337
- }
338
- const value = await validateSchema(response.body, next.value);
339
- controller.enqueue(encoder.encode(`data:${JSON.stringify(value)}\n\n`));
340
- }
341
- catch (error) {
342
- finished = true;
343
- if (abort)
344
- signal.removeEventListener('abort', abort);
345
- controller.error(error);
346
- }
347
- },
348
- async cancel(reason) {
349
- if (finished)
350
- return;
351
- finished = true;
352
- if (abort)
353
- signal.removeEventListener('abort', abort);
354
- await iterator.return?.(reason);
355
- },
356
- });
357
- return new Response(stream, {
358
- status: response.status,
359
- headers: mergeResponseHeaders(response.staticHeaders, responseHeaders, {
360
- 'content-type': 'text/event-stream; charset=utf-8',
361
- 'cache-control': 'no-cache',
362
- }),
363
- });
364
- }
365
- const body = await validateSchema(response.body, result.body);
366
- const headers = mergeResponseHeaders(response.staticHeaders, responseHeaders);
367
- if (response.status === 204 ||
368
- response.status === 205 ||
369
- response.status === 304) {
370
- if (body !== undefined) {
371
- throw new Error(`HTTP ${response.status} response body must be undefined`);
372
- }
373
- return new Response(undefined, { status: response.status, headers });
374
- }
375
- return jsonResponse(response.status, body, headers);
376
- }
377
- async function validateResponseHeaders(schema, headers) {
378
- if (!schema) {
379
- if (headers !== undefined) {
380
- throw new Error('Undeclared HTTP response header was returned');
381
- }
382
- return undefined;
383
- }
384
- const validated = await validateSchema(schema, headers);
385
- if (validated === undefined)
386
- return undefined;
387
- if (!isHttpHeaders(validated)) {
388
- throw new Error('HTTP response header schema produced an invalid value');
389
- }
390
- return validated;
391
- }
392
- function isHttpHeaders(value) {
393
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
394
- return false;
395
- }
396
- return Object.values(value).every((header) => header === undefined ||
397
- typeof header === 'string' ||
398
- (Array.isArray(header) &&
399
- header.every((item) => typeof item === 'string')));
400
- }
401
- function collectRoutes(modules) {
402
- const routes = [];
403
- for (const module of modules) {
404
- for (const implementation of module.definition.implementations ?? []) {
405
- if (implementation.protocol !== 'http')
406
- continue;
407
- for (const procedure of implementation.procedures) {
408
- const protocol = implementation.contract.procedures[procedure]?.protocols.http;
409
- if (!protocol || protocol.protocol !== 'http')
410
- continue;
411
- const typed = protocol;
412
- routes.push({
413
- method: typed.definition.method.toUpperCase(),
414
- path: typed.definition.path,
415
- segments: parseHttpPath(typed.definition.path),
416
- dispatchKey: typed.dispatchKey,
417
- protocol: typed,
418
- implementation,
419
- procedure,
420
- });
421
- }
422
- }
423
- }
424
- return routes.toSorted((left, right) => compareHttpPathSpecificity(left.segments, right.segments));
425
- }
426
- function jsonResponse(status, body, headers = new Headers()) {
427
- headers.set('content-type', 'application/json; charset=utf-8');
428
- return new Response(JSON.stringify(body), {
429
- status,
430
- headers,
431
- });
432
- }
433
- function mergeResponseHeaders(declared, dynamic, framework = undefined) {
434
- const headers = new Headers();
435
- applyResponseHeaders(headers, declared);
436
- applyResponseHeaders(headers, dynamic);
437
- applyResponseHeaders(headers, framework);
438
- return headers;
439
- }
440
- function applyResponseHeaders(headers, source) {
441
- if (!source)
442
- return;
443
- for (const [name, value] of Object.entries(source)) {
444
- if (value === undefined)
445
- continue;
446
- headers.delete(name);
447
- if (typeof value === 'string') {
448
- headers.set(name, value);
449
- continue;
450
- }
451
- for (const item of value)
452
- headers.append(name, item);
453
- }
454
- }
455
- function isValidationError(error) {
456
- return error instanceof HttpInputValidationError;
457
- }
458
- function isDecodeError(error) {
459
- return (error instanceof HttpInputDecodeError ||
460
- error instanceof HttpPathDecodeError);
461
- }
462
- async function mapDeclaredError(definition, error) {
463
- for (const [variant, response] of Object.entries(definition.responses)) {
464
- const errorMapping = response.error;
465
- if (errorMapping?.definition.is(error)) {
466
- const result = await errorMapping.map(error);
467
- if (typeof result !== 'object' ||
468
- result === null ||
469
- !('body' in result)) {
470
- throw new Error('HTTP error mapping must return a body');
471
- }
472
- return {
473
- kind: 'http-result',
474
- variant,
475
- body: result.body,
476
- ...('headers' in result
477
- ? { headers: result.headers }
478
- : {}),
479
- };
480
- }
481
- }
482
- return undefined;
483
- }
484
- function isAsyncIterable(value) {
485
- return (typeof value === 'object' &&
486
- value !== null &&
487
- Symbol.asyncIterator in value &&
488
- typeof value[Symbol.asyncIterator] === 'function');
489
- }
490
- class HttpInputValidationError extends Error {
491
- cause;
492
- constructor(cause) {
493
- super('HTTP input validation failed', { cause });
494
- this.cause = cause;
495
- this.name = 'HttpInputValidationError';
496
- }
497
- }
498
- class HttpInputDecodeError extends Error {
499
- cause;
500
- constructor(cause) {
501
- super('HTTP input decode failed', { cause });
502
- this.cause = cause;
503
- this.name = 'HttpInputDecodeError';
504
- }
505
- }
506
- class HttpUnsupportedMediaTypeError extends Error {
507
- expected;
508
- actual;
509
- constructor(expected, actual) {
510
- super(`Unsupported HTTP request media type: expected ${expected}, received ${actual ?? '(missing)'}`);
511
- this.expected = expected;
512
- this.actual = actual;
513
- this.name = 'HttpUnsupportedMediaTypeError';
514
- }
515
- }
@@ -1,37 +0,0 @@
1
- import { type ContextKey, type ContextKeyValue, type LayerDescriptor } from '../core/index.js';
2
- import type { LogicalHttpResult } from './definitions.js';
3
- export interface BasicAuthCredentials {
4
- readonly username: string;
5
- readonly password: string;
6
- }
7
- export interface BasicAuthUnauthorized<TVariant extends string, TBody> {
8
- readonly variant: TVariant;
9
- readonly body: TBody;
10
- }
11
- export interface BasicAuthOptions<TPrincipal extends ContextKey, TVariant extends string, TUnauthorizedBody> {
12
- readonly realm: string;
13
- readonly principal: TPrincipal;
14
- readonly authenticate: (credentials: BasicAuthCredentials) => ContextKeyValue<TPrincipal> | null | undefined | Promise<ContextKeyValue<TPrincipal> | null | undefined>;
15
- readonly unauthorized: BasicAuthUnauthorized<TVariant, TUnauthorizedBody>;
16
- readonly name?: string;
17
- }
18
- type BasicAuthShortCircuits<TVariant extends string> = readonly [
19
- {
20
- readonly protocol: 'http';
21
- readonly variant: TVariant;
22
- readonly response: {
23
- readonly status: 401;
24
- };
25
- }
26
- ];
27
- type BasicAuthResponseHeaders = {
28
- readonly 'www-authenticate': string;
29
- };
30
- export interface BasicAuthLayerDescriptor<TPrincipal extends ContextKey, TVariant extends string, TUnauthorizedBody> extends LayerDescriptor<readonly [], readonly [TPrincipal], string extends TVariant ? unknown : LogicalHttpResult<TVariant, TUnauthorizedBody, BasicAuthResponseHeaders>, BasicAuthShortCircuits<TVariant>, string, 'authentication', readonly [], BasicAuthContext> {
31
- readonly role: 'authentication';
32
- }
33
- export interface BasicAuthContext {
34
- readonly headers: Readonly<Record<string, string | undefined>>;
35
- }
36
- export declare function basicAuth<TPrincipal extends ContextKey, const TVariant extends string, TUnauthorizedBody>(options: BasicAuthOptions<TPrincipal, TVariant, TUnauthorizedBody>): BasicAuthLayerDescriptor<TPrincipal, TVariant, TUnauthorizedBody>;
37
- export {};
@@ -1,67 +0,0 @@
1
- import { layer, shortCircuit, } from '../core/index.js';
2
- export function basicAuth(options) {
3
- const challenge = formatBasicChallenge(options.realm);
4
- const descriptor = layer({
5
- name: options.name ?? 'basicAuth',
6
- role: 'authentication',
7
- provides: [options.principal],
8
- shortCircuits: [
9
- {
10
- protocol: 'http',
11
- variant: options.unauthorized.variant,
12
- response: { status: 401 },
13
- },
14
- ],
15
- factory: () => async (ctx, next) => {
16
- const credentials = decodeBasicCredentials(ctx.headers.authorization);
17
- if (!credentials) {
18
- return unauthorizedResult(options.unauthorized, challenge);
19
- }
20
- const principal = await options.authenticate(credentials);
21
- if (principal == null) {
22
- return unauthorizedResult(options.unauthorized, challenge);
23
- }
24
- await next({
25
- [options.principal.name]: principal,
26
- });
27
- },
28
- });
29
- return Object.freeze(descriptor);
30
- }
31
- function unauthorizedResult(unauthorized, challenge) {
32
- return shortCircuit({
33
- kind: 'http-result',
34
- variant: unauthorized.variant,
35
- body: unauthorized.body,
36
- headers: {
37
- 'www-authenticate': challenge,
38
- },
39
- });
40
- }
41
- function formatBasicChallenge(realm) {
42
- if (realm.length === 0 || /[\u0000-\u001f\u007f]/.test(realm)) {
43
- throw new TypeError('Basic authentication realm cannot be empty or contain control characters');
44
- }
45
- const escaped = realm.replace(/[\\"]/g, '\\$&');
46
- return `Basic realm="${escaped}", charset="UTF-8"`;
47
- }
48
- function decodeBasicCredentials(authorization) {
49
- const match = /^Basic +([A-Za-z0-9+/]+={0,2})$/i.exec(authorization ?? '');
50
- if (!match?.[1])
51
- return undefined;
52
- try {
53
- const binary = atob(match[1]);
54
- const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
55
- const decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
56
- const separator = decoded.indexOf(':');
57
- if (separator < 0)
58
- return undefined;
59
- return {
60
- username: decoded.slice(0, separator),
61
- password: decoded.slice(separator + 1),
62
- };
63
- }
64
- catch {
65
- return undefined;
66
- }
67
- }