@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,37 @@
|
|
|
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 {};
|
|
@@ -0,0 +1,67 @@
|
|
|
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認証のrealmには空文字列または制御文字を使用できません');
|
|
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type LayerDescriptor, type PipelineItem } from '../core/index.js';
|
|
2
|
+
export type CorsOrigin = string | readonly string[] | ((origin: string) => boolean | Promise<boolean>);
|
|
3
|
+
export interface CorsOptions {
|
|
4
|
+
readonly origin?: CorsOrigin;
|
|
5
|
+
readonly allowMethods?: readonly string[];
|
|
6
|
+
readonly allowHeaders?: readonly string[];
|
|
7
|
+
readonly exposeHeaders?: readonly string[];
|
|
8
|
+
readonly credentials?: boolean;
|
|
9
|
+
readonly maxAge?: number;
|
|
10
|
+
readonly name?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface CorsLayerDescriptor extends LayerDescriptor<readonly [], readonly [], never, readonly [], string, 'framework', readonly [], object> {
|
|
13
|
+
readonly role: 'framework';
|
|
14
|
+
}
|
|
15
|
+
export declare function cors(options?: CorsOptions): CorsLayerDescriptor;
|
|
16
|
+
export declare function createCorsActualResponseHeaders(pipeline: readonly PipelineItem[], request: Request): Promise<Headers | undefined>;
|
|
17
|
+
export declare function createCorsPreflightResponseHeaders(pipeline: readonly PipelineItem[], request: Request, targetMethod: string): Promise<Headers | undefined>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { childPipelineOf, layer, layerDefinitionOf, } from '../core/index.js';
|
|
2
|
+
const policies = new WeakMap();
|
|
3
|
+
const httpToken = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
4
|
+
export function cors(options = {}) {
|
|
5
|
+
const policy = normalizeCorsOptions(options);
|
|
6
|
+
const descriptor = layer({
|
|
7
|
+
name: options.name ?? 'cors',
|
|
8
|
+
role: 'framework',
|
|
9
|
+
factory: () => async (_ctx, next) => {
|
|
10
|
+
await next();
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
policies.set(descriptor, policy);
|
|
14
|
+
return descriptor;
|
|
15
|
+
}
|
|
16
|
+
export async function createCorsActualResponseHeaders(pipeline, request) {
|
|
17
|
+
const policy = corsPolicyOfPipeline(pipeline);
|
|
18
|
+
if (!policy)
|
|
19
|
+
return undefined;
|
|
20
|
+
const origin = request.headers.get('origin');
|
|
21
|
+
const headers = new Headers();
|
|
22
|
+
if (policy.origin.kind !== 'wildcard')
|
|
23
|
+
appendVary(headers, 'Origin');
|
|
24
|
+
if (origin === null)
|
|
25
|
+
return headers;
|
|
26
|
+
await applyOrigin(headers, policy, origin);
|
|
27
|
+
if (!headers.has('access-control-allow-origin'))
|
|
28
|
+
return headers;
|
|
29
|
+
if (policy.credentials) {
|
|
30
|
+
headers.set('access-control-allow-credentials', 'true');
|
|
31
|
+
}
|
|
32
|
+
if (policy.exposeHeaders.length > 0) {
|
|
33
|
+
headers.set('access-control-expose-headers', policy.exposeHeaders.join(', '));
|
|
34
|
+
}
|
|
35
|
+
return headers;
|
|
36
|
+
}
|
|
37
|
+
export async function createCorsPreflightResponseHeaders(pipeline, request, targetMethod) {
|
|
38
|
+
const policy = corsPolicyOfPipeline(pipeline);
|
|
39
|
+
if (!policy)
|
|
40
|
+
return undefined;
|
|
41
|
+
const headers = new Headers();
|
|
42
|
+
appendVary(headers, 'Access-Control-Request-Method');
|
|
43
|
+
const origin = request.headers.get('origin');
|
|
44
|
+
if (origin === null)
|
|
45
|
+
return headers;
|
|
46
|
+
await applyOrigin(headers, policy, origin);
|
|
47
|
+
if (!headers.has('access-control-allow-origin'))
|
|
48
|
+
return headers;
|
|
49
|
+
if (policy.credentials) {
|
|
50
|
+
headers.set('access-control-allow-credentials', 'true');
|
|
51
|
+
}
|
|
52
|
+
const methods = policy.allowMethods ?? [targetMethod.toUpperCase()];
|
|
53
|
+
if (methods.length > 0) {
|
|
54
|
+
headers.set('access-control-allow-methods', methods.join(', '));
|
|
55
|
+
}
|
|
56
|
+
if (policy.allowHeaders === undefined) {
|
|
57
|
+
const requested = request.headers.get('access-control-request-headers');
|
|
58
|
+
if (requested !== null && requested.trim().length > 0) {
|
|
59
|
+
headers.set('access-control-allow-headers', requested);
|
|
60
|
+
appendVary(headers, 'Access-Control-Request-Headers');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (policy.allowHeaders.length > 0) {
|
|
64
|
+
headers.set('access-control-allow-headers', policy.allowHeaders.join(', '));
|
|
65
|
+
}
|
|
66
|
+
if (policy.maxAge !== undefined) {
|
|
67
|
+
headers.set('access-control-max-age', String(policy.maxAge));
|
|
68
|
+
}
|
|
69
|
+
return headers;
|
|
70
|
+
}
|
|
71
|
+
function corsPolicyOfPipeline(pipeline) {
|
|
72
|
+
let found;
|
|
73
|
+
const visit = (items) => {
|
|
74
|
+
for (const item of items) {
|
|
75
|
+
if (item.kind !== 'layer')
|
|
76
|
+
continue;
|
|
77
|
+
const definition = layerDefinitionOf(item);
|
|
78
|
+
const policy = policies.get(definition);
|
|
79
|
+
if (policy)
|
|
80
|
+
found = policy;
|
|
81
|
+
const child = childPipelineOf(item);
|
|
82
|
+
if (child)
|
|
83
|
+
visit(child);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
visit(pipeline);
|
|
87
|
+
return found;
|
|
88
|
+
}
|
|
89
|
+
async function applyOrigin(headers, policy, origin) {
|
|
90
|
+
if (policy.origin.kind === 'wildcard') {
|
|
91
|
+
headers.set('access-control-allow-origin', '*');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
appendVary(headers, 'Origin');
|
|
95
|
+
const allowed = policy.origin.kind === 'exact'
|
|
96
|
+
? policy.origin.origins.has(origin)
|
|
97
|
+
: await policy.origin.predicate(origin);
|
|
98
|
+
if (allowed)
|
|
99
|
+
headers.set('access-control-allow-origin', origin);
|
|
100
|
+
}
|
|
101
|
+
function normalizeCorsOptions(options) {
|
|
102
|
+
const origin = normalizeOrigin(options.origin ?? '*', options.credentials ?? false);
|
|
103
|
+
return Object.freeze({
|
|
104
|
+
origin,
|
|
105
|
+
allowMethods: options.allowMethods === undefined
|
|
106
|
+
? undefined
|
|
107
|
+
: normalizeTokens(options.allowMethods, 'allowMethods', true),
|
|
108
|
+
allowHeaders: options.allowHeaders === undefined
|
|
109
|
+
? undefined
|
|
110
|
+
: normalizeTokens(options.allowHeaders, 'allowHeaders', false),
|
|
111
|
+
exposeHeaders: normalizeTokens(options.exposeHeaders ?? [], 'exposeHeaders', false),
|
|
112
|
+
credentials: options.credentials ?? false,
|
|
113
|
+
maxAge: normalizeMaxAge(options.maxAge),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function normalizeOrigin(origin, credentials) {
|
|
117
|
+
if (typeof origin === 'function') {
|
|
118
|
+
return Object.freeze({ kind: 'predicate', predicate: origin });
|
|
119
|
+
}
|
|
120
|
+
const origins = typeof origin === 'string' ? [origin] : [...origin];
|
|
121
|
+
if (origins.length === 0) {
|
|
122
|
+
return Object.freeze({ kind: 'exact', origins: new Set() });
|
|
123
|
+
}
|
|
124
|
+
for (const value of origins) {
|
|
125
|
+
if (value.length === 0 || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
126
|
+
throw new TypeError('CORS originには空文字列または制御文字を使用できません');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (origins.includes('*')) {
|
|
130
|
+
if (credentials) {
|
|
131
|
+
throw new TypeError('credentialsを有効にする場合、CORS originにワイルドカードは使用できません');
|
|
132
|
+
}
|
|
133
|
+
return Object.freeze({ kind: 'wildcard' });
|
|
134
|
+
}
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
kind: 'exact',
|
|
137
|
+
origins: new Set(origins),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function normalizeTokens(values, option, uppercase) {
|
|
141
|
+
return Object.freeze(values.map((value) => {
|
|
142
|
+
const token = value.trim();
|
|
143
|
+
if (!httpToken.test(token)) {
|
|
144
|
+
throw new TypeError(`CORS ${option}に不正なHTTP tokenが含まれています`);
|
|
145
|
+
}
|
|
146
|
+
return uppercase ? token.toUpperCase() : token;
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
function normalizeMaxAge(maxAge) {
|
|
150
|
+
if (maxAge === undefined)
|
|
151
|
+
return undefined;
|
|
152
|
+
if (!Number.isSafeInteger(maxAge) || maxAge < 0) {
|
|
153
|
+
throw new TypeError('CORS maxAgeには0以上の安全な整数を指定してください');
|
|
154
|
+
}
|
|
155
|
+
return maxAge;
|
|
156
|
+
}
|
|
157
|
+
function appendVary(headers, value) {
|
|
158
|
+
const current = headers.get('vary');
|
|
159
|
+
if (current === null) {
|
|
160
|
+
headers.set('vary', value);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const values = current
|
|
164
|
+
.split(',')
|
|
165
|
+
.map((item) => item.trim())
|
|
166
|
+
.filter(Boolean);
|
|
167
|
+
if (!values.some((item) => item.toLowerCase() === value.toLowerCase())) {
|
|
168
|
+
values.push(value);
|
|
169
|
+
}
|
|
170
|
+
headers.set('vary', values.join(', '));
|
|
171
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { ContractDefinition, ContextProvidedBeforeTerminal, HasValidationBeforeTerminal, IsValidProtocolPipeline, PipelineItem, ProtocolDescriptor, SchemaInput, SchemaOutput, ShortCircuitDeclarationsOf, ShortCircuitResultOf, StandardSchemaV1, TerminalLayerDescriptor, ValidationLayerDescriptor } from '../core/index.js';
|
|
2
|
+
import type { Logger } from '../runtime/index.js';
|
|
3
|
+
import { type HttpDispatchKey, type IsValidHttpPath, type PathParamNames, type RawPathParams } from './path.js';
|
|
4
|
+
export type HttpParamsSchemas = Readonly<Record<string, StandardSchemaV1>>;
|
|
5
|
+
export interface HttpRequestBodyDefinition<TSchema extends StandardSchemaV1 = StandardSchemaV1> {
|
|
6
|
+
readonly contentType: string;
|
|
7
|
+
readonly schema: TSchema;
|
|
8
|
+
}
|
|
9
|
+
export interface HttpRequestDefinition {
|
|
10
|
+
readonly params?: HttpParamsSchemas;
|
|
11
|
+
readonly query?: StandardSchemaV1;
|
|
12
|
+
readonly headers?: StandardSchemaV1;
|
|
13
|
+
readonly body?: HttpRequestBodyDefinition;
|
|
14
|
+
}
|
|
15
|
+
export type HttpHeaderValue = string | readonly string[];
|
|
16
|
+
export type HttpHeaders = Readonly<Record<string, HttpHeaderValue | undefined>>;
|
|
17
|
+
export interface HttpErrorMatcher<TError extends {
|
|
18
|
+
readonly data: unknown;
|
|
19
|
+
}> {
|
|
20
|
+
is(error: unknown): error is TError;
|
|
21
|
+
}
|
|
22
|
+
export interface HttpErrorMapping<TError extends {
|
|
23
|
+
readonly data: unknown;
|
|
24
|
+
} = {
|
|
25
|
+
readonly data: unknown;
|
|
26
|
+
}, TResult = unknown> {
|
|
27
|
+
readonly kind: 'http-error-mapping';
|
|
28
|
+
readonly definition: HttpErrorMatcher<TError>;
|
|
29
|
+
readonly map: (error: TError) => TResult | Promise<TResult>;
|
|
30
|
+
}
|
|
31
|
+
export interface HttpResponseDefinition {
|
|
32
|
+
readonly status: number;
|
|
33
|
+
readonly description?: string;
|
|
34
|
+
readonly body: StandardSchemaV1;
|
|
35
|
+
readonly headers?: StandardSchemaV1;
|
|
36
|
+
readonly staticHeaders?: HttpHeaders;
|
|
37
|
+
readonly error?: HttpErrorMapping<any, any>;
|
|
38
|
+
readonly stream?: 'server';
|
|
39
|
+
}
|
|
40
|
+
export interface HttpProtocolDefinition {
|
|
41
|
+
readonly method: string;
|
|
42
|
+
readonly path: string;
|
|
43
|
+
readonly summary?: string;
|
|
44
|
+
readonly description?: string;
|
|
45
|
+
readonly tags?: readonly string[];
|
|
46
|
+
readonly deprecated?: boolean;
|
|
47
|
+
readonly request?: HttpRequestDefinition;
|
|
48
|
+
readonly responses: Readonly<Record<string, HttpResponseDefinition>>;
|
|
49
|
+
readonly pipeline: readonly PipelineItem[];
|
|
50
|
+
readonly interaction?: 'unary' | 'server-stream';
|
|
51
|
+
}
|
|
52
|
+
export interface HttpProtocol<TDefinition extends HttpProtocolDefinition = HttpProtocolDefinition> extends ProtocolDescriptor<'http', HttpControllerContextDefinition<TDefinition>, DeclaredHttpResults<TDefinition['responses']>, HttpDispatchKey<TDefinition['method'], TDefinition['path']>> {
|
|
53
|
+
readonly definition: TDefinition;
|
|
54
|
+
readonly interaction: TDefinition extends {
|
|
55
|
+
readonly interaction: infer TInteraction;
|
|
56
|
+
} ? TInteraction & ('unary' | 'server-stream') : 'unary';
|
|
57
|
+
}
|
|
58
|
+
type IsHttpPipelineCompatible<TPipeline extends readonly PipelineItem[], TResponses extends HttpProtocolDefinition['responses']> = number extends TPipeline['length'] ? false : TPipeline extends readonly [infer THead, ...infer TTail] ? IsLayerShortCircuitCompatible<ShortCircuitResultOf<THead>, TResponses> extends true ? IsShortCircuitDeclarationsCompatible<ShortCircuitDeclarationsOf<THead>, TResponses> extends true ? IsHttpPipelineCompatible<Extract<TTail, readonly PipelineItem[]>, TResponses> : false : false : true;
|
|
59
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
60
|
+
type IsUnknown<T> = unknown extends T ? [keyof T] extends [never] ? true : false : false;
|
|
61
|
+
type ResponseBodyOutput<TResponse extends HttpResponseDefinition> = TResponse extends {
|
|
62
|
+
readonly stream: 'server';
|
|
63
|
+
} ? AsyncIterable<SchemaOutput<TResponse['body']>> : SchemaOutput<TResponse['body']>;
|
|
64
|
+
type ResponseHeadersOutput<TResponse extends HttpResponseDefinition> = TResponse extends {
|
|
65
|
+
readonly headers: infer THeaders extends StandardSchemaV1;
|
|
66
|
+
} ? SchemaOutput<THeaders> : never;
|
|
67
|
+
type DeclaredHttpResults<TResponses extends HttpProtocolDefinition['responses']> = string extends keyof TResponses ? LogicalHttpResult : {
|
|
68
|
+
[TVariant in keyof TResponses & string]: NonNullable<TResponses[TVariant]> extends infer TResponse extends HttpResponseDefinition ? LogicalHttpResult<TVariant, ResponseBodyOutput<TResponse>, ResponseHeadersOutput<TResponse>> : never;
|
|
69
|
+
}[keyof TResponses & string];
|
|
70
|
+
type IsResultHeadersCompatible<TResult, TResponses extends HttpProtocolDefinition['responses']> = string extends keyof TResponses ? true : TResult extends {
|
|
71
|
+
readonly variant: infer TVariant extends keyof TResponses;
|
|
72
|
+
} ? TResult extends {
|
|
73
|
+
readonly headers: infer TResultHeaders;
|
|
74
|
+
} ? NonNullable<TResponses[TVariant]> extends infer TResponse extends HttpResponseDefinition ? [ResponseHeadersOutput<TResponse>] extends [never] ? false : Exclude<keyof TResultHeaders, keyof NonNullable<ResponseHeadersOutput<TResponse>>> extends never ? true : false : false : true : false;
|
|
75
|
+
type AreResultHeadersCompatible<TResult, TResponses extends HttpProtocolDefinition['responses']> = false extends (TResult extends unknown ? IsResultHeadersCompatible<TResult, TResponses> : never) ? false : true;
|
|
76
|
+
type IsLayerShortCircuitCompatible<TResult, TResponses extends HttpProtocolDefinition['responses']> = IsAny<TResult> extends true ? true : IsUnknown<TResult> extends true ? true : [TResult] extends [never] ? true : [TResult] extends [DeclaredHttpResults<TResponses>] ? AreResultHeadersCompatible<TResult, TResponses> : false;
|
|
77
|
+
type IsShortCircuitDeclarationsCompatible<TDeclarations, TResponses extends HttpProtocolDefinition['responses']> = IsAny<TDeclarations> extends true ? true : IsUnknown<TDeclarations> extends true ? true : TDeclarations extends readonly unknown[] ? number extends TDeclarations['length'] ? true : TDeclarations extends readonly [infer THead, ...infer TTail] ? IsHttpShortCircuitDeclarationCompatible<THead, TResponses> extends true ? IsShortCircuitDeclarationsCompatible<TTail, TResponses> : false : true : true;
|
|
78
|
+
type IsHttpShortCircuitDeclarationCompatible<TDeclaration, TResponses extends HttpProtocolDefinition['responses']> = TDeclaration extends {
|
|
79
|
+
readonly protocol: 'http';
|
|
80
|
+
readonly variant: infer TVariant extends string;
|
|
81
|
+
} ? string extends TVariant ? true : TVariant extends keyof TResponses ? NonNullable<TResponses[TVariant]> extends HttpResponseDefinition ? TDeclaration extends {
|
|
82
|
+
readonly response: {
|
|
83
|
+
readonly status: infer TStatus extends number;
|
|
84
|
+
};
|
|
85
|
+
} ? NonNullable<TResponses[TVariant]>['status'] extends TStatus ? true : number extends NonNullable<TResponses[TVariant]>['status'] ? true : false : true : false : false : true;
|
|
86
|
+
type HttpPipelineConstraint<TDefinition extends HttpProtocolDefinition> = IsValidProtocolPipeline<TDefinition['pipeline'], 'http'> extends true ? IsHttpPipelineCompatible<TDefinition['pipeline'], TDefinition['responses']> extends true ? unknown : {
|
|
87
|
+
readonly pipeline: never;
|
|
88
|
+
} : {
|
|
89
|
+
readonly pipeline: never;
|
|
90
|
+
};
|
|
91
|
+
type IsResponseHeadersSchemaCompatible<TResponse> = TResponse extends HttpResponseDefinition ? TResponse extends {
|
|
92
|
+
readonly headers: infer THeaders extends StandardSchemaV1;
|
|
93
|
+
} ? SchemaOutput<THeaders> extends HttpHeaders | undefined ? true : false : true : false;
|
|
94
|
+
type AreResponseHeadersSchemasCompatible<TResponses extends HttpProtocolDefinition['responses']> = false extends {
|
|
95
|
+
[TVariant in keyof TResponses]: IsResponseHeadersSchemaCompatible<TResponses[TVariant]>;
|
|
96
|
+
}[keyof TResponses] ? false : true;
|
|
97
|
+
type HttpResponseConstraint<TDefinition extends HttpProtocolDefinition> = string extends keyof TDefinition['responses'] ? {
|
|
98
|
+
readonly responses: never;
|
|
99
|
+
} : AreResponseHeadersSchemasCompatible<TDefinition['responses']> extends true ? AreErrorMappingsCompatible<TDefinition['responses']> extends true ? unknown : {
|
|
100
|
+
readonly responses: never;
|
|
101
|
+
} : {
|
|
102
|
+
readonly responses: never;
|
|
103
|
+
};
|
|
104
|
+
type IsExactParamsSchemaMap<TPath extends string, TSchemas extends HttpParamsSchemas> = Exclude<keyof TSchemas, PathParamNames<TPath>> extends never ? Exclude<PathParamNames<TPath>, keyof TSchemas> extends never ? true : false : false;
|
|
105
|
+
type DoParamsSchemasAcceptStrings<TSchemas extends HttpParamsSchemas> = false extends {
|
|
106
|
+
[TName in keyof TSchemas]: IsRawStringCompatible<SchemaInput<TSchemas[TName]>>;
|
|
107
|
+
}[keyof TSchemas] ? false : true;
|
|
108
|
+
type IsRawStringCompatible<TInput> = string extends TInput ? true : [Extract<TInput, string>] extends [never] ? false : true;
|
|
109
|
+
type IsUnion<TValue, TCandidate = TValue> = TValue extends unknown ? [TCandidate] extends [TValue] ? false : true : never;
|
|
110
|
+
type IsSingleStringLiteral<TValue extends string> = string extends TValue ? false : true extends IsUnion<TValue> ? false : true;
|
|
111
|
+
type HttpPathConstraint<TDefinition extends HttpProtocolDefinition> = IsSingleStringLiteral<TDefinition['method']> extends false ? {
|
|
112
|
+
readonly method: never;
|
|
113
|
+
} : IsSingleStringLiteral<TDefinition['path']> extends false ? {
|
|
114
|
+
readonly path: never;
|
|
115
|
+
} : IsValidHttpPath<TDefinition['path']> extends true ? TDefinition['request'] extends {
|
|
116
|
+
readonly params: infer TSchemas extends HttpParamsSchemas;
|
|
117
|
+
} ? IsExactParamsSchemaMap<TDefinition['path'], TSchemas> extends true ? DoParamsSchemasAcceptStrings<TSchemas> extends true ? unknown : {
|
|
118
|
+
readonly request: never;
|
|
119
|
+
} : {
|
|
120
|
+
readonly request: never;
|
|
121
|
+
} : HasValidationBeforeTerminal<TDefinition['pipeline'], 'params'> extends true ? {
|
|
122
|
+
readonly pipeline: never;
|
|
123
|
+
} : unknown : {
|
|
124
|
+
readonly path: never;
|
|
125
|
+
};
|
|
126
|
+
type ErrorMappingResult<TResponse> = TResponse extends {
|
|
127
|
+
readonly error: infer TMapping extends HttpErrorMapping<any, any>;
|
|
128
|
+
} ? Awaited<ReturnType<TMapping['map']>> : never;
|
|
129
|
+
type IsErrorMappingCompatible<TResponse> = TResponse extends HttpResponseDefinition ? TResponse extends {
|
|
130
|
+
readonly error: HttpErrorMapping<any, any>;
|
|
131
|
+
} ? ErrorMappingResult<TResponse> extends HttpResponseResult<ResponseBodyOutput<TResponse>, ResponseHeadersOutput<TResponse>> ? true : false : true : false;
|
|
132
|
+
type AreErrorMappingsCompatible<TResponses extends HttpProtocolDefinition['responses']> = false extends {
|
|
133
|
+
[TVariant in keyof TResponses]: IsErrorMappingCompatible<TResponses[TVariant]>;
|
|
134
|
+
}[keyof TResponses] ? false : true;
|
|
135
|
+
declare function defineHttp<const TDefinition extends HttpProtocolDefinition>(definition: TDefinition & HttpPipelineConstraint<TDefinition> & HttpResponseConstraint<TDefinition> & HttpPathConstraint<TDefinition>): HttpProtocol<TDefinition>;
|
|
136
|
+
export declare const http: typeof defineHttp & {
|
|
137
|
+
protocol: 'http';
|
|
138
|
+
controller: TerminalLayerDescriptor<"http">;
|
|
139
|
+
error: typeof httpError;
|
|
140
|
+
};
|
|
141
|
+
export declare const validate: Readonly<{
|
|
142
|
+
params: ValidationLayerDescriptor & {
|
|
143
|
+
readonly part: "params";
|
|
144
|
+
readonly name: "validate.params";
|
|
145
|
+
};
|
|
146
|
+
query: ValidationLayerDescriptor & {
|
|
147
|
+
readonly part: "query";
|
|
148
|
+
readonly name: "validate.query";
|
|
149
|
+
};
|
|
150
|
+
headers: ValidationLayerDescriptor & {
|
|
151
|
+
readonly part: "headers";
|
|
152
|
+
readonly name: "validate.headers";
|
|
153
|
+
};
|
|
154
|
+
body: ValidationLayerDescriptor & {
|
|
155
|
+
readonly part: "body";
|
|
156
|
+
readonly name: "validate.body";
|
|
157
|
+
};
|
|
158
|
+
}>;
|
|
159
|
+
type ProceduresForHttp<TContract extends ContractDefinition> = {
|
|
160
|
+
[K in keyof TContract['procedures']]: 'http' extends keyof TContract['procedures'][K]['protocols'] ? K : never;
|
|
161
|
+
}[keyof TContract['procedures']] & string;
|
|
162
|
+
type HttpProtocolAt<TContract extends ContractDefinition, TProcedure extends keyof TContract['procedures']> = TContract['procedures'][TProcedure]['protocols']['http' & keyof TContract['procedures'][TProcedure]['protocols']] extends infer TProtocol ? TProtocol extends {
|
|
163
|
+
readonly definition: infer TDefinition;
|
|
164
|
+
} ? TDefinition extends HttpProtocolDefinition ? HttpProtocol<TDefinition> : never : never : never;
|
|
165
|
+
type RequestBodySchema<TBody> = TBody extends HttpRequestBodyDefinition<infer TSchema> ? TSchema : never;
|
|
166
|
+
type PartOutput<TDefinition extends HttpProtocolDefinition, TPart extends keyof HttpRequestDefinition> = TPart extends 'params' ? HasValidationBeforeTerminal<TDefinition['pipeline'], 'params'> extends true ? TDefinition['request'] extends {
|
|
167
|
+
readonly params: infer TSchemas extends HttpParamsSchemas;
|
|
168
|
+
} ? ValidatedPathParams<TSchemas> : never : RawPathParams<TDefinition['path']> : HasValidationBeforeTerminal<TDefinition['pipeline'], TPart> extends false ? unknown : TDefinition['request'] extends HttpRequestDefinition ? TPart extends keyof TDefinition['request'] ? TPart extends 'body' ? SchemaOutput<RequestBodySchema<NonNullable<TDefinition['request'][TPart]>>> : NonNullable<TDefinition['request'][TPart]> extends StandardSchemaV1 ? SchemaOutput<NonNullable<TDefinition['request'][TPart]>> : unknown : unknown : unknown;
|
|
169
|
+
type ValidatedPathParams<TSchemas extends HttpParamsSchemas> = Readonly<{
|
|
170
|
+
[TName in keyof TSchemas]: SchemaOutput<TSchemas[TName]>;
|
|
171
|
+
}>;
|
|
172
|
+
type HttpResultHeaders<THeaders> = IsAny<THeaders> extends true ? {
|
|
173
|
+
readonly headers?: HttpHeaders;
|
|
174
|
+
} : IsUnknown<THeaders> extends true ? {
|
|
175
|
+
readonly headers?: HttpHeaders;
|
|
176
|
+
} : [THeaders] extends [never] ? {
|
|
177
|
+
readonly headers?: never;
|
|
178
|
+
} : undefined extends THeaders ? {
|
|
179
|
+
readonly headers?: Exclude<THeaders, undefined>;
|
|
180
|
+
} : {
|
|
181
|
+
readonly headers: THeaders;
|
|
182
|
+
};
|
|
183
|
+
export type HttpResponseResult<TBody, THeaders = unknown> = {
|
|
184
|
+
readonly body: TBody;
|
|
185
|
+
} & HttpResultHeaders<THeaders>;
|
|
186
|
+
type ErrorOf<TDefinition> = TDefinition extends HttpErrorMatcher<infer TError> ? TError : never;
|
|
187
|
+
export declare function httpError<TDefinition extends HttpErrorMatcher<{
|
|
188
|
+
readonly data: unknown;
|
|
189
|
+
}>>(definition: TDefinition): HttpErrorMapping<ErrorOf<TDefinition>, {
|
|
190
|
+
readonly body: ErrorOf<TDefinition>['data'];
|
|
191
|
+
}>;
|
|
192
|
+
export declare function httpError<TDefinition extends HttpErrorMatcher<{
|
|
193
|
+
readonly data: unknown;
|
|
194
|
+
}>, const TResult>(definition: TDefinition, map: (error: ErrorOf<TDefinition>) => TResult | Promise<TResult>): HttpErrorMapping<ErrorOf<TDefinition>, TResult>;
|
|
195
|
+
export type LogicalHttpResult<TVariant extends string = string, TBody = unknown, THeaders = unknown> = {
|
|
196
|
+
readonly kind: 'http-result';
|
|
197
|
+
readonly variant: TVariant;
|
|
198
|
+
readonly body: TBody;
|
|
199
|
+
} & HttpResultHeaders<THeaders>;
|
|
200
|
+
type ResponseHelpers<TDefinition extends HttpProtocolDefinition> = {
|
|
201
|
+
[TVariant in keyof TDefinition['responses'] & string]: (result: HttpResponseResult<ResponseBodyOutput<TDefinition['responses'][TVariant]>, ResponseHeadersOutput<TDefinition['responses'][TVariant]>>) => LogicalHttpResult<TVariant, ResponseBodyOutput<TDefinition['responses'][TVariant]>, ResponseHeadersOutput<TDefinition['responses'][TVariant]>>;
|
|
202
|
+
};
|
|
203
|
+
type HttpControllerContextDefinition<TDefinition extends HttpProtocolDefinition> = {
|
|
204
|
+
readonly params: PartOutput<TDefinition, 'params'>;
|
|
205
|
+
readonly query: PartOutput<TDefinition, 'query'>;
|
|
206
|
+
readonly headers: PartOutput<TDefinition, 'headers'>;
|
|
207
|
+
readonly body: PartOutput<TDefinition, 'body'>;
|
|
208
|
+
readonly response: ResponseHelpers<TDefinition>;
|
|
209
|
+
readonly logger: Logger;
|
|
210
|
+
readonly signal: AbortSignal;
|
|
211
|
+
} & ContextProvidedBeforeTerminal<TDefinition['pipeline']>;
|
|
212
|
+
export type HttpControllerContext<TProtocol extends HttpProtocol<any>> = HttpControllerContextDefinition<TProtocol['definition']>;
|
|
213
|
+
export type ControllerOf<TContract extends ContractDefinition, TProtocol extends 'http', TProcedures extends ProceduresForHttp<TContract> = ProceduresForHttp<TContract>> = TProtocol extends 'http' ? {
|
|
214
|
+
[K in TProcedures]: (context: HttpControllerContext<HttpProtocolAt<TContract, K>>) => DeclaredHttpResults<HttpProtocolAt<TContract, K>['definition']['responses']> | Promise<DeclaredHttpResults<HttpProtocolAt<TContract, K>['definition']['responses']>>;
|
|
215
|
+
} : never;
|
|
216
|
+
export type ContextOf<TController, TProcedure extends keyof TController> = TController[TProcedure] extends (context: infer TContext, ...args: any[]) => any ? TContext : never;
|
|
217
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { childPipelineOf } from '../core/index.js';
|
|
2
|
+
import { createHttpDispatchKey, parseHttpPath, } from './path.js';
|
|
3
|
+
const controller = Object.freeze({
|
|
4
|
+
kind: 'terminal',
|
|
5
|
+
name: 'http.controller',
|
|
6
|
+
role: 'terminal',
|
|
7
|
+
protocol: 'http',
|
|
8
|
+
});
|
|
9
|
+
function defineHttp(definition) {
|
|
10
|
+
const segments = parseHttpPath(definition.path);
|
|
11
|
+
const paramsSchemas = definition.request?.params;
|
|
12
|
+
if (paramsSchemas) {
|
|
13
|
+
const pathNames = segments.flatMap((segment) => segment.kind === 'param' ? [segment.name] : []);
|
|
14
|
+
const schemaNames = Object.keys(paramsSchemas);
|
|
15
|
+
if (pathNames.length !== schemaNames.length ||
|
|
16
|
+
pathNames.some((name) => !Object.hasOwn(paramsSchemas, name))) {
|
|
17
|
+
throw new Error('HTTP path parameter names and request.params keys must match');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else if (hasParamsValidation(definition.pipeline)) {
|
|
21
|
+
throw new Error('validate.params requires request.params');
|
|
22
|
+
}
|
|
23
|
+
const body = definition.request?.body;
|
|
24
|
+
if (body && body.contentType.trim().length === 0) {
|
|
25
|
+
throw new Error('HTTP request body contentType must not be empty');
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
kind: 'protocol',
|
|
29
|
+
protocol: 'http',
|
|
30
|
+
interaction: definition.interaction ?? 'unary',
|
|
31
|
+
dispatchKey: createHttpDispatchKey(definition.method, segments),
|
|
32
|
+
definition,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function hasParamsValidation(pipeline) {
|
|
36
|
+
return pipeline.some((item) => (item.kind === 'validation' && item.part === 'params') ||
|
|
37
|
+
(item.kind === 'layer' &&
|
|
38
|
+
hasParamsValidation(childPipelineOf(item) ?? [])));
|
|
39
|
+
}
|
|
40
|
+
export const http = Object.assign(defineHttp, {
|
|
41
|
+
protocol: 'http',
|
|
42
|
+
controller,
|
|
43
|
+
error: httpError,
|
|
44
|
+
});
|
|
45
|
+
function validationLayer(part) {
|
|
46
|
+
return Object.freeze({
|
|
47
|
+
kind: 'validation',
|
|
48
|
+
name: `validate.${part}`,
|
|
49
|
+
role: 'validation',
|
|
50
|
+
part,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export const validate = Object.freeze({
|
|
54
|
+
params: validationLayer('params'),
|
|
55
|
+
query: validationLayer('query'),
|
|
56
|
+
headers: validationLayer('headers'),
|
|
57
|
+
body: validationLayer('body'),
|
|
58
|
+
});
|
|
59
|
+
export function httpError(definition, map) {
|
|
60
|
+
return Object.freeze({
|
|
61
|
+
kind: 'http-error-mapping',
|
|
62
|
+
definition,
|
|
63
|
+
map: map ?? ((error) => ({ body: error.data })),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './application.js';
|
|
2
|
+
export * from './basic-auth.js';
|
|
3
|
+
export type { CorsLayerDescriptor, CorsOptions, CorsOrigin } from './cors.js';
|
|
4
|
+
export * from './definitions.js';
|
|
5
|
+
export { http } from './hosted-protocol.js';
|
|
6
|
+
export * from './request-body.js';
|
|
7
|
+
export { validate } from './validate.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SchemaValidationError, validateSchema } from '../core/index.js';
|
|
2
|
+
export async function validateHttpParamsSchemas(schemas, value) {
|
|
3
|
+
const output = {};
|
|
4
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
5
|
+
try {
|
|
6
|
+
output[name] = await validateSchema(schema, value[name]);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
if (error instanceof SchemaValidationError) {
|
|
10
|
+
throw new SchemaValidationError(error.issues.map((issue) => ({
|
|
11
|
+
...issue,
|
|
12
|
+
path: [name, ...(issue.path ?? [])],
|
|
13
|
+
})));
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return output;
|
|
19
|
+
}
|