@loutrejs/loutre 0.1.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/dist/adapters/aws-lambda.d.ts +52 -0
- package/dist/adapters/aws-lambda.js +114 -0
- package/dist/adapters/bun.d.ts +20 -0
- package/dist/adapters/bun.js +82 -0
- package/dist/adapters/cloudflare-workers.d.ts +18 -0
- package/dist/adapters/cloudflare-workers.js +55 -0
- package/dist/adapters/deno.d.ts +28 -0
- package/dist/adapters/deno.js +106 -0
- package/dist/adapters/electron.d.ts +27 -0
- package/dist/adapters/electron.js +52 -0
- package/dist/application/binding.d.ts +33 -0
- package/dist/application/binding.js +292 -0
- package/dist/application/host.d.ts +8 -0
- package/dist/application/host.js +14 -0
- package/dist/application/index.d.ts +62 -0
- package/dist/application/index.js +11 -0
- package/dist/application/openapi.d.ts +28 -0
- package/dist/application/openapi.js +362 -0
- package/dist/application/queue.d.ts +3 -0
- package/dist/application/queue.js +4 -0
- package/dist/application/scheduler.d.ts +2 -0
- package/dist/application/scheduler.js +45 -0
- package/dist/core/args.d.ts +16 -0
- package/dist/core/args.js +29 -0
- package/dist/core/context-key.d.ts +14 -0
- package/dist/core/context-key.js +14 -0
- package/dist/core/contract.d.ts +54 -0
- package/dist/core/contract.js +30 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +29 -0
- package/dist/core/error.d.ts +24 -0
- package/dist/core/error.js +25 -0
- package/dist/core/execution.d.ts +78 -0
- package/dist/core/execution.js +61 -0
- package/dist/core/implementation.d.ts +34 -0
- package/dist/core/implementation.js +30 -0
- package/dist/core/index.d.ts +15 -0
- package/dist/core/index.js +15 -0
- package/dist/core/injection.d.ts +28 -0
- package/dist/core/injection.js +35 -0
- package/dist/core/layer.d.ts +109 -0
- package/dist/core/layer.js +52 -0
- package/dist/core/lifecycle.d.ts +34 -0
- package/dist/core/lifecycle.js +3 -0
- package/dist/core/module.d.ts +53 -0
- package/dist/core/module.js +31 -0
- package/dist/core/provider.d.ts +57 -0
- package/dist/core/provider.js +63 -0
- package/dist/core/runtime-input.d.ts +11 -0
- package/dist/core/runtime-input.js +1 -0
- package/dist/core/schema.d.ts +13 -0
- package/dist/core/schema.js +20 -0
- package/dist/core/token.d.ts +16 -0
- package/dist/core/token.js +13 -0
- package/dist/graph/graph.d.ts +17 -0
- package/dist/graph/graph.js +1078 -0
- package/dist/graph/index.d.ts +2 -0
- package/dist/graph/index.js +2 -0
- package/dist/graph/ir.d.ts +159 -0
- package/dist/graph/ir.js +1 -0
- package/dist/http/application.d.ts +18 -0
- package/dist/http/application.js +506 -0
- package/dist/http/basic-auth.d.ts +37 -0
- package/dist/http/basic-auth.js +67 -0
- package/dist/http/cors.d.ts +17 -0
- package/dist/http/cors.js +171 -0
- package/dist/http/definitions.d.ts +217 -0
- package/dist/http/definitions.js +65 -0
- package/dist/http/hosted-protocol.d.ts +4 -0
- package/dist/http/hosted-protocol.js +4 -0
- package/dist/http/index.d.ts +7 -0
- package/dist/http/index.js +6 -0
- package/dist/http/params.d.ts +2 -0
- package/dist/http/params.js +19 -0
- package/dist/http/path.d.ts +55 -0
- package/dist/http/path.js +88 -0
- package/dist/http/request-body.d.ts +4 -0
- package/dist/http/request-body.js +6 -0
- package/dist/http/validate.d.ts +20 -0
- package/dist/http/validate.js +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/message-port/environment.d.ts +2 -0
- package/dist/message-port/environment.js +4 -0
- package/dist/message-port/index.d.ts +65 -0
- package/dist/message-port/index.js +182 -0
- package/dist/runtime/application.d.ts +37 -0
- package/dist/runtime/application.js +324 -0
- package/dist/runtime/capability.d.ts +16 -0
- package/dist/runtime/capability.js +18 -0
- package/dist/runtime/di.d.ts +37 -0
- package/dist/runtime/di.js +441 -0
- package/dist/runtime/error.d.ts +10 -0
- package/dist/runtime/error.js +15 -0
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.js +7 -0
- package/dist/runtime/logger.d.ts +36 -0
- package/dist/runtime/logger.js +207 -0
- package/dist/runtime/node-capabilities.d.ts +5 -0
- package/dist/runtime/node-capabilities.js +18 -0
- package/dist/runtime/pipeline.d.ts +11 -0
- package/dist/runtime/pipeline.js +161 -0
- package/package.json +81 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export interface ModuleIR {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly name?: string;
|
|
4
|
+
readonly description?: string;
|
|
5
|
+
readonly imports: readonly string[];
|
|
6
|
+
readonly environment: readonly string[];
|
|
7
|
+
readonly providers: readonly string[];
|
|
8
|
+
readonly exports: readonly string[];
|
|
9
|
+
readonly lifecycle: readonly string[];
|
|
10
|
+
readonly requires: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export interface ProviderIR {
|
|
13
|
+
readonly token: string;
|
|
14
|
+
readonly kind: 'class' | 'value' | 'factory' | 'conditional' | 'environment' | 'arguments';
|
|
15
|
+
readonly scope: 'application' | 'transient';
|
|
16
|
+
readonly dependencies: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export interface DependencyNodeIR {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly label: string;
|
|
21
|
+
readonly kind: 'class' | 'token' | 'factory' | 'conditional' | 'environment' | 'arguments' | 'implementation' | 'layer' | 'task' | 'framework';
|
|
22
|
+
readonly scope?: 'application' | 'transient';
|
|
23
|
+
readonly module?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface DependencyEdgeIR {
|
|
26
|
+
readonly from: string;
|
|
27
|
+
readonly to: string;
|
|
28
|
+
readonly kind: 'inject' | 'factory' | 'lifecycle' | 'conditional' | 'framework';
|
|
29
|
+
readonly source: 'declared' | 'probed';
|
|
30
|
+
readonly condition?: {
|
|
31
|
+
readonly source: 'environment' | 'arguments';
|
|
32
|
+
readonly contract: string;
|
|
33
|
+
readonly key: string;
|
|
34
|
+
readonly equals: PropertyKey;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export interface TokenIR {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ContextKeyIR {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
}
|
|
43
|
+
export interface LayerIR {
|
|
44
|
+
readonly index: number;
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly role: 'generic' | 'authentication' | 'guard' | 'validation' | 'framework' | 'terminal';
|
|
47
|
+
readonly requires: readonly string[];
|
|
48
|
+
readonly provides: readonly string[];
|
|
49
|
+
readonly returns?: readonly string[];
|
|
50
|
+
readonly requiresValidated: readonly string[];
|
|
51
|
+
readonly dependencies?: readonly string[];
|
|
52
|
+
readonly attributes?: Readonly<Record<string, string | number | boolean>>;
|
|
53
|
+
readonly pipeline?: readonly LayerIR[];
|
|
54
|
+
readonly shortCircuits?: readonly {
|
|
55
|
+
readonly protocol: string;
|
|
56
|
+
readonly variant: string;
|
|
57
|
+
readonly response?: Readonly<Record<string, unknown>>;
|
|
58
|
+
}[];
|
|
59
|
+
}
|
|
60
|
+
export interface PipelineIR {
|
|
61
|
+
readonly contract: string;
|
|
62
|
+
readonly procedure: string;
|
|
63
|
+
readonly protocol: string;
|
|
64
|
+
readonly layers: readonly LayerIR[];
|
|
65
|
+
}
|
|
66
|
+
export interface ImplementationIR {
|
|
67
|
+
readonly contract: string;
|
|
68
|
+
readonly procedure: string;
|
|
69
|
+
readonly protocol: string;
|
|
70
|
+
readonly implementation: string;
|
|
71
|
+
readonly method: string;
|
|
72
|
+
}
|
|
73
|
+
export interface CapabilityIR {
|
|
74
|
+
readonly name: string;
|
|
75
|
+
readonly scope: 'application' | 'execution';
|
|
76
|
+
readonly requiredBy: string;
|
|
77
|
+
}
|
|
78
|
+
export interface ProtocolExecutionRootIR {
|
|
79
|
+
readonly id: `protocol:${string}`;
|
|
80
|
+
readonly kind: 'protocol';
|
|
81
|
+
readonly protocol: string;
|
|
82
|
+
readonly contract: string;
|
|
83
|
+
readonly procedure: string;
|
|
84
|
+
readonly implementation: string;
|
|
85
|
+
}
|
|
86
|
+
export interface ApplicationArgumentsIR {
|
|
87
|
+
readonly name: string;
|
|
88
|
+
}
|
|
89
|
+
export interface TaskIR {
|
|
90
|
+
readonly id: `task:${string}`;
|
|
91
|
+
readonly name: string;
|
|
92
|
+
readonly public: boolean;
|
|
93
|
+
}
|
|
94
|
+
export interface TaskExecutionRootIR {
|
|
95
|
+
readonly id: `task:${string}`;
|
|
96
|
+
readonly kind: 'task';
|
|
97
|
+
readonly name: string;
|
|
98
|
+
}
|
|
99
|
+
export interface CronTriggerExecutionRootIR {
|
|
100
|
+
readonly id: `trigger:${string}`;
|
|
101
|
+
readonly kind: 'trigger';
|
|
102
|
+
readonly trigger: 'cron';
|
|
103
|
+
readonly name: string;
|
|
104
|
+
readonly expression: string;
|
|
105
|
+
readonly timezone: string;
|
|
106
|
+
readonly overlap: 'allow' | 'skip';
|
|
107
|
+
readonly task: string;
|
|
108
|
+
}
|
|
109
|
+
export interface FixedDelayTriggerExecutionRootIR {
|
|
110
|
+
readonly id: `trigger:${string}`;
|
|
111
|
+
readonly kind: 'trigger';
|
|
112
|
+
readonly trigger: 'fixed-delay';
|
|
113
|
+
readonly name: string;
|
|
114
|
+
readonly delay: number;
|
|
115
|
+
readonly immediate: boolean;
|
|
116
|
+
readonly task: string;
|
|
117
|
+
}
|
|
118
|
+
export interface QueueConsumerTriggerExecutionRootIR {
|
|
119
|
+
readonly id: `trigger:${string}`;
|
|
120
|
+
readonly kind: 'trigger';
|
|
121
|
+
readonly trigger: 'queue-consumer';
|
|
122
|
+
readonly name: string;
|
|
123
|
+
readonly queue: string;
|
|
124
|
+
readonly task: string;
|
|
125
|
+
}
|
|
126
|
+
export type TriggerExecutionRootIR = CronTriggerExecutionRootIR | FixedDelayTriggerExecutionRootIR | QueueConsumerTriggerExecutionRootIR;
|
|
127
|
+
export type ExecutionRootIR = ProtocolExecutionRootIR | TaskExecutionRootIR | TriggerExecutionRootIR;
|
|
128
|
+
export interface QueueIR {
|
|
129
|
+
readonly id: `queue:${string}`;
|
|
130
|
+
readonly name: string;
|
|
131
|
+
readonly payloadSchema: string;
|
|
132
|
+
}
|
|
133
|
+
export interface ApplicationGraphIR {
|
|
134
|
+
readonly modules: readonly ModuleIR[];
|
|
135
|
+
readonly arguments?: ApplicationArgumentsIR;
|
|
136
|
+
readonly providers: readonly ProviderIR[];
|
|
137
|
+
readonly tokens: readonly TokenIR[];
|
|
138
|
+
readonly contextKeys: readonly ContextKeyIR[];
|
|
139
|
+
readonly contracts: readonly string[];
|
|
140
|
+
readonly pipelines: readonly PipelineIR[];
|
|
141
|
+
readonly implementations: readonly ImplementationIR[];
|
|
142
|
+
readonly tasks: readonly TaskIR[];
|
|
143
|
+
readonly queues: readonly QueueIR[];
|
|
144
|
+
readonly executions: readonly ExecutionRootIR[];
|
|
145
|
+
readonly capabilities: readonly CapabilityIR[];
|
|
146
|
+
readonly hostCapabilities: readonly string[];
|
|
147
|
+
readonly nodes: readonly DependencyNodeIR[];
|
|
148
|
+
readonly edges: readonly DependencyEdgeIR[];
|
|
149
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
150
|
+
}
|
|
151
|
+
export interface Diagnostic {
|
|
152
|
+
readonly code: string;
|
|
153
|
+
readonly message: string;
|
|
154
|
+
readonly path: string;
|
|
155
|
+
}
|
|
156
|
+
export interface CompilationResult {
|
|
157
|
+
readonly graph: ApplicationGraphIR;
|
|
158
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
159
|
+
}
|
package/dist/graph/ir.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ApplicationGraphIR } from '../graph/index.js';
|
|
2
|
+
import { ApplicationRuntime, Logger } from '../runtime/index.js';
|
|
3
|
+
export interface HttpProtocolExecution {
|
|
4
|
+
readonly graph: ApplicationGraphIR;
|
|
5
|
+
initialize(): Promise<void>;
|
|
6
|
+
shutdown(signal?: string): Promise<void>;
|
|
7
|
+
onServerListening(url: string): void;
|
|
8
|
+
handle(request: Request): Promise<Response>;
|
|
9
|
+
}
|
|
10
|
+
export interface HttpProtocolExecutionLifecycle {
|
|
11
|
+
readonly onServerListening?: (url: string) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function createHttpExecution(options: {
|
|
14
|
+
readonly runtime: ApplicationRuntime;
|
|
15
|
+
readonly graph: ApplicationGraphIR;
|
|
16
|
+
readonly lifecycle?: HttpProtocolExecutionLifecycle;
|
|
17
|
+
readonly logger?: Logger;
|
|
18
|
+
}): HttpProtocolExecution;
|
|
@@ -0,0 +1,506 @@
|
|
|
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には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が中断されました'));
|
|
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
|
+
return jsonResponse(response.status, body, mergeResponseHeaders(response.staticHeaders, responseHeaders));
|
|
367
|
+
}
|
|
368
|
+
async function validateResponseHeaders(schema, headers) {
|
|
369
|
+
if (!schema) {
|
|
370
|
+
if (headers !== undefined) {
|
|
371
|
+
throw new Error('未宣言のHTTP response headerが返されました');
|
|
372
|
+
}
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
const validated = await validateSchema(schema, headers);
|
|
376
|
+
if (validated === undefined)
|
|
377
|
+
return undefined;
|
|
378
|
+
if (!isHttpHeaders(validated)) {
|
|
379
|
+
throw new Error('HTTP response header schemaの出力が不正です');
|
|
380
|
+
}
|
|
381
|
+
return validated;
|
|
382
|
+
}
|
|
383
|
+
function isHttpHeaders(value) {
|
|
384
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
return Object.values(value).every((header) => header === undefined ||
|
|
388
|
+
typeof header === 'string' ||
|
|
389
|
+
(Array.isArray(header) &&
|
|
390
|
+
header.every((item) => typeof item === 'string')));
|
|
391
|
+
}
|
|
392
|
+
function collectRoutes(modules) {
|
|
393
|
+
const routes = [];
|
|
394
|
+
for (const module of modules) {
|
|
395
|
+
for (const implementation of module.definition.implementations ?? []) {
|
|
396
|
+
if (implementation.protocol !== 'http')
|
|
397
|
+
continue;
|
|
398
|
+
for (const procedure of implementation.procedures) {
|
|
399
|
+
const protocol = implementation.contract.procedures[procedure]?.protocols.http;
|
|
400
|
+
if (!protocol || protocol.protocol !== 'http')
|
|
401
|
+
continue;
|
|
402
|
+
const typed = protocol;
|
|
403
|
+
routes.push({
|
|
404
|
+
method: typed.definition.method.toUpperCase(),
|
|
405
|
+
path: typed.definition.path,
|
|
406
|
+
segments: parseHttpPath(typed.definition.path),
|
|
407
|
+
dispatchKey: typed.dispatchKey,
|
|
408
|
+
protocol: typed,
|
|
409
|
+
implementation,
|
|
410
|
+
procedure,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return routes.toSorted((left, right) => compareHttpPathSpecificity(left.segments, right.segments));
|
|
416
|
+
}
|
|
417
|
+
function jsonResponse(status, body, headers = new Headers()) {
|
|
418
|
+
headers.set('content-type', 'application/json; charset=utf-8');
|
|
419
|
+
return new Response(JSON.stringify(body), {
|
|
420
|
+
status,
|
|
421
|
+
headers,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function mergeResponseHeaders(declared, dynamic, framework = undefined) {
|
|
425
|
+
const headers = new Headers();
|
|
426
|
+
applyResponseHeaders(headers, declared);
|
|
427
|
+
applyResponseHeaders(headers, dynamic);
|
|
428
|
+
applyResponseHeaders(headers, framework);
|
|
429
|
+
return headers;
|
|
430
|
+
}
|
|
431
|
+
function applyResponseHeaders(headers, source) {
|
|
432
|
+
if (!source)
|
|
433
|
+
return;
|
|
434
|
+
for (const [name, value] of Object.entries(source)) {
|
|
435
|
+
if (value === undefined)
|
|
436
|
+
continue;
|
|
437
|
+
headers.delete(name);
|
|
438
|
+
if (typeof value === 'string') {
|
|
439
|
+
headers.set(name, value);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
for (const item of value)
|
|
443
|
+
headers.append(name, item);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function isValidationError(error) {
|
|
447
|
+
return error instanceof HttpInputValidationError;
|
|
448
|
+
}
|
|
449
|
+
function isDecodeError(error) {
|
|
450
|
+
return (error instanceof HttpInputDecodeError ||
|
|
451
|
+
error instanceof HttpPathDecodeError);
|
|
452
|
+
}
|
|
453
|
+
async function mapDeclaredError(definition, error) {
|
|
454
|
+
for (const [variant, response] of Object.entries(definition.responses)) {
|
|
455
|
+
const errorMapping = response.error;
|
|
456
|
+
if (errorMapping?.definition.is(error)) {
|
|
457
|
+
const result = await errorMapping.map(error);
|
|
458
|
+
if (typeof result !== 'object' ||
|
|
459
|
+
result === null ||
|
|
460
|
+
!('body' in result)) {
|
|
461
|
+
throw new Error('HTTP error mappingはbodyを返す必要があります');
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
kind: 'http-result',
|
|
465
|
+
variant,
|
|
466
|
+
body: result.body,
|
|
467
|
+
...('headers' in result
|
|
468
|
+
? { headers: result.headers }
|
|
469
|
+
: {}),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
function isAsyncIterable(value) {
|
|
476
|
+
return (typeof value === 'object' &&
|
|
477
|
+
value !== null &&
|
|
478
|
+
Symbol.asyncIterator in value &&
|
|
479
|
+
typeof value[Symbol.asyncIterator] === 'function');
|
|
480
|
+
}
|
|
481
|
+
class HttpInputValidationError extends Error {
|
|
482
|
+
cause;
|
|
483
|
+
constructor(cause) {
|
|
484
|
+
super('HTTP input validation failed', { cause });
|
|
485
|
+
this.cause = cause;
|
|
486
|
+
this.name = 'HttpInputValidationError';
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
class HttpInputDecodeError extends Error {
|
|
490
|
+
cause;
|
|
491
|
+
constructor(cause) {
|
|
492
|
+
super('HTTP input decode failed', { cause });
|
|
493
|
+
this.cause = cause;
|
|
494
|
+
this.name = 'HttpInputDecodeError';
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
class HttpUnsupportedMediaTypeError extends Error {
|
|
498
|
+
expected;
|
|
499
|
+
actual;
|
|
500
|
+
constructor(expected, actual) {
|
|
501
|
+
super(`Unsupported HTTP request media type: expected ${expected}, received ${actual ?? '(missing)'}`);
|
|
502
|
+
this.expected = expected;
|
|
503
|
+
this.actual = actual;
|
|
504
|
+
this.name = 'HttpUnsupportedMediaTypeError';
|
|
505
|
+
}
|
|
506
|
+
}
|