@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.
- package/README.md +63 -32
- package/dist/adapters/aws-lambda.d.ts +5 -3
- package/dist/adapters/aws-lambda.js +53 -23
- package/dist/adapters/bun.d.ts +7 -4
- package/dist/adapters/bun.js +52 -42
- package/dist/adapters/cloudflare-workers.d.ts +5 -3
- package/dist/adapters/cloudflare-workers.js +23 -28
- package/dist/adapters/deno.d.ts +7 -4
- package/dist/adapters/deno.js +94 -57
- package/dist/adapters/electron.d.ts +13 -5
- package/dist/adapters/electron.js +76 -28
- package/dist/application/definition.d.ts +31 -0
- package/dist/application/definition.js +16 -0
- package/dist/application/index.d.ts +4 -63
- package/dist/application/index.js +2 -11
- package/dist/application/kernel-internal.d.ts +2 -0
- package/dist/application/kernel-internal.js +10 -0
- package/dist/application/kernel.d.ts +22 -0
- package/dist/application/kernel.js +65 -0
- package/dist/core/diagnostic.d.ts +9 -0
- package/dist/core/diagnostic.js +9 -0
- package/dist/core/extension.d.ts +118 -0
- package/dist/core/extension.js +73 -0
- package/dist/core/generic-layer.d.ts +26 -0
- package/dist/core/generic-layer.js +116 -0
- package/dist/core/index.d.ts +32 -15
- package/dist/core/index.js +15 -15
- package/dist/core/injection.d.ts +4 -13
- package/dist/core/injection.js +63 -0
- package/dist/core/instrumentation.d.ts +36 -0
- package/dist/core/model/context.d.ts +18 -0
- package/dist/core/model/context.js +34 -0
- package/dist/core/model/lifecycle.d.ts +5 -0
- package/dist/core/model/lifecycle.js +44 -0
- package/dist/core/model/modules.d.ts +2 -0
- package/dist/core/model/modules.js +101 -0
- package/dist/core/model/providers.d.ts +4 -0
- package/dist/core/model/providers.js +74 -0
- package/dist/core/model/snapshot.d.ts +6 -0
- package/dist/core/model/snapshot.js +31 -0
- package/dist/core/model/types.d.ts +78 -0
- package/dist/core/model/types.js +1 -0
- package/dist/core/model/validation.d.ts +4 -0
- package/dist/core/model/validation.js +40 -0
- package/dist/core/model/visibility.d.ts +4 -0
- package/dist/core/model/visibility.js +20 -0
- package/dist/core/model.d.ts +10 -0
- package/dist/core/model.js +346 -0
- package/dist/core/module.d.ts +8 -13
- package/dist/core/source-location.d.ts +8 -0
- package/dist/core/source-location.js +31 -0
- package/dist/core/token-internal.d.ts +3 -0
- package/dist/core/token-internal.js +4 -0
- package/dist/core/type.d.ts +7 -0
- package/dist/core/type.js +4 -0
- package/dist/devtools/capsule.d.ts +51 -0
- package/dist/devtools/capsule.js +245 -0
- package/dist/devtools/index.d.ts +11 -0
- package/dist/devtools/index.js +6 -0
- package/dist/devtools/inspector.d.ts +57 -0
- package/dist/devtools/inspector.js +1 -0
- package/dist/devtools/module.d.ts +18 -0
- package/dist/devtools/module.js +26 -0
- package/dist/devtools/observer.d.ts +77 -0
- package/dist/devtools/observer.js +10 -0
- package/dist/devtools/protocol.d.ts +1 -0
- package/dist/devtools/protocol.js +1 -0
- package/dist/devtools/provider-instrumentation.d.ts +14 -0
- package/dist/devtools/provider-instrumentation.js +117 -0
- package/dist/devtools/runtime.d.ts +20 -0
- package/dist/devtools/runtime.js +519 -0
- package/dist/devtools/values.d.ts +35 -0
- package/dist/devtools/values.js +175 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/graph/index.d.ts +2 -3
- package/dist/graph/index.js +1 -3
- package/dist/graph/model.d.ts +34 -0
- package/dist/graph/model.js +127 -0
- package/dist/http/auth.d.ts +46 -0
- package/dist/http/auth.js +96 -0
- package/dist/http/client.d.ts +28 -31
- package/dist/http/client.js +58 -44
- package/dist/http/cors-internal.d.ts +29 -0
- package/dist/http/cors-internal.js +160 -0
- package/dist/http/cors.d.ts +5 -8
- package/dist/http/cors.js +7 -165
- package/dist/http/extension.d.ts +348 -0
- package/dist/http/extension.js +1061 -0
- package/dist/http/index.d.ts +9 -7
- package/dist/http/index.js +5 -7
- package/dist/{application → http}/openapi.d.ts +12 -3
- package/dist/{application → http}/openapi.js +164 -102
- package/dist/http/path.d.ts +1 -42
- package/dist/http/path.js +1 -2
- package/dist/http/runtime/request.d.ts +17 -0
- package/dist/http/runtime/request.js +109 -0
- package/dist/http/runtime/response.d.ts +18 -0
- package/dist/http/runtime/response.js +108 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/message-port/extension.d.ts +66 -0
- package/dist/message-port/extension.js +411 -0
- package/dist/message-port/index.d.ts +4 -73
- package/dist/message-port/index.js +2 -189
- package/dist/message-port/transport.d.ts +11 -0
- package/dist/message-port/transport.js +45 -0
- package/dist/runtime/async-iterator-lifecycle.d.ts +12 -0
- package/dist/runtime/async-iterator-lifecycle.js +95 -0
- package/dist/runtime/capability.d.ts +11 -11
- package/dist/runtime/capability.js +3 -3
- package/dist/runtime/di.d.ts +5 -21
- package/dist/runtime/di.js +22 -289
- package/dist/runtime/execution-tracker.d.ts +8 -0
- package/dist/runtime/execution-tracker.js +54 -0
- package/dist/runtime/index.d.ts +15 -9
- package/dist/runtime/index.js +9 -9
- package/dist/runtime/ingress-gate.d.ts +7 -0
- package/dist/runtime/ingress-gate.js +33 -0
- package/dist/runtime/kernel.d.ts +25 -0
- package/dist/runtime/kernel.js +447 -0
- package/dist/runtime/logger.d.ts +2 -0
- package/dist/runtime/logger.js +2 -0
- package/dist/runtime/node-capabilities.d.ts +1 -1
- package/dist/runtime/node-capabilities.js +1 -1
- package/dist/runtime/server-port.d.ts +2 -0
- package/dist/runtime/server-port.js +11 -0
- package/dist/tasks/extension.d.ts +144 -0
- package/dist/tasks/extension.js +525 -0
- package/dist/tasks/index.d.ts +2 -0
- package/dist/tasks/index.js +1 -0
- package/dist/tasks/runtime/cron.d.ts +6 -0
- package/dist/tasks/runtime/cron.js +89 -0
- package/dist/websocket/errors.d.ts +9 -0
- package/dist/websocket/errors.js +18 -0
- package/dist/websocket/extension.d.ts +154 -0
- package/dist/websocket/extension.js +533 -0
- package/dist/websocket/index.d.ts +3 -0
- package/dist/websocket/index.js +2 -0
- package/package.json +16 -16
- package/dist/application/binding.d.ts +0 -33
- package/dist/application/binding.js +0 -295
- package/dist/application/host.d.ts +0 -8
- package/dist/application/host.js +0 -14
- package/dist/application/queue.d.ts +0 -3
- package/dist/application/queue.js +0 -4
- package/dist/application/scheduler.d.ts +0 -2
- package/dist/application/scheduler.js +0 -45
- package/dist/core/context-key.d.ts +0 -16
- package/dist/core/context-key.js +0 -17
- package/dist/core/contract-internal.d.ts +0 -22
- package/dist/core/contract-internal.js +0 -61
- package/dist/core/contract.d.ts +0 -96
- package/dist/core/contract.js +0 -88
- package/dist/core/execution.d.ts +0 -78
- package/dist/core/execution.js +0 -61
- package/dist/core/implementation.d.ts +0 -44
- package/dist/core/implementation.js +0 -35
- package/dist/core/layer.d.ts +0 -137
- package/dist/core/layer.js +0 -81
- package/dist/graph/compiler.d.ts +0 -4
- package/dist/graph/compiler.js +0 -228
- package/dist/graph/graph.d.ts +0 -18
- package/dist/graph/graph.js +0 -1118
- package/dist/graph/ir.d.ts +0 -175
- package/dist/http/application.d.ts +0 -18
- package/dist/http/application.js +0 -515
- package/dist/http/basic-auth.d.ts +0 -37
- package/dist/http/basic-auth.js +0 -67
- package/dist/http/definitions.d.ts +0 -363
- package/dist/http/definitions.js +0 -310
- package/dist/http/hosted-protocol.d.ts +0 -4
- package/dist/http/hosted-protocol.js +0 -4
- package/dist/http/internal.d.ts +0 -8
- package/dist/http/internal.js +0 -1
- package/dist/http/params.d.ts +0 -2
- package/dist/http/params.js +0 -19
- package/dist/http/request-body.d.ts +0 -4
- package/dist/http/request-body.js +0 -6
- package/dist/http/validate.d.ts +0 -20
- package/dist/http/validate.js +0 -6
- package/dist/message-port/environment.d.ts +0 -2
- package/dist/message-port/environment.js +0 -4
- package/dist/runtime/application.d.ts +0 -38
- package/dist/runtime/application.js +0 -339
- package/dist/runtime/pipeline.d.ts +0 -11
- package/dist/runtime/pipeline.js +0 -161
- /package/dist/{graph/ir.js → core/instrumentation.js} +0 -0
package/dist/http/application.js
DELETED
|
@@ -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 {};
|
package/dist/http/basic-auth.js
DELETED
|
@@ -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
|
-
}
|