@loutrejs/loutre 0.5.0 → 0.6.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 +21 -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 +107 -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 +31 -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/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/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/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 +947 -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 +395 -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 +2 -22
- package/dist/runtime/di.js +8 -285
- 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 +23 -0
- package/dist/runtime/kernel.js +354 -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 +499 -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 +524 -0
- package/dist/websocket/index.d.ts +3 -0
- package/dist/websocket/index.js +2 -0
- package/package.json +12 -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/model/types.js} +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { HttpExecutionResult, HttpMiddleware, HttpMiddlewareContext } from './extension.js';
|
|
2
|
+
export interface BasicAuthCredentials {
|
|
3
|
+
readonly username: string;
|
|
4
|
+
readonly password: string;
|
|
5
|
+
}
|
|
6
|
+
export interface HttpAuthenticationFailure<TResponse extends string, TBody> {
|
|
7
|
+
readonly response: TResponse;
|
|
8
|
+
readonly body: TBody;
|
|
9
|
+
}
|
|
10
|
+
export type BasicAuthUnauthorized<TResponse extends string, TBody> = HttpAuthenticationFailure<TResponse, TBody>;
|
|
11
|
+
export type BearerAuthUnauthorized<TResponse extends string, TBody> = HttpAuthenticationFailure<TResponse, TBody>;
|
|
12
|
+
export interface BasicAuthContext {
|
|
13
|
+
readonly input: {
|
|
14
|
+
readonly headers: Readonly<Record<string, string | undefined>>;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface BearerAuthContext {
|
|
18
|
+
readonly input: {
|
|
19
|
+
readonly headers: Readonly<Record<string, string | undefined>>;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
type AuthenticationChallengeHeaders = {
|
|
23
|
+
readonly 'www-authenticate': string;
|
|
24
|
+
};
|
|
25
|
+
type AuthenticationShortCircuit<TResponse extends string, TBody> = HttpExecutionResult<TResponse, TBody, AuthenticationChallengeHeaders>;
|
|
26
|
+
export interface BasicAuthDefinition<TContribution extends object, TResponse extends string, TUnauthorizedBody> {
|
|
27
|
+
readonly name?: string;
|
|
28
|
+
readonly realm: string;
|
|
29
|
+
readonly factory: () => BasicAuthRuntime<TContribution, TResponse, TUnauthorizedBody>;
|
|
30
|
+
}
|
|
31
|
+
export interface BasicAuthRuntime<TContribution extends object, TResponse extends string, TUnauthorizedBody> {
|
|
32
|
+
authenticate(credentials: BasicAuthCredentials): TContribution | null | undefined | Promise<TContribution | null | undefined>;
|
|
33
|
+
unauthorized(): HttpAuthenticationFailure<TResponse, TUnauthorizedBody>;
|
|
34
|
+
}
|
|
35
|
+
export declare function basicAuth<const TContribution extends object, const TResponse extends string, TUnauthorizedBody>(definition: BasicAuthDefinition<TContribution, TResponse, TUnauthorizedBody>): HttpMiddleware<TContribution, HttpMiddlewareContext, AuthenticationShortCircuit<TResponse, TUnauthorizedBody>>;
|
|
36
|
+
export interface BearerAuthDefinition<TContribution extends object, TResponse extends string, TUnauthorizedBody> {
|
|
37
|
+
readonly name?: string;
|
|
38
|
+
readonly realm: string;
|
|
39
|
+
readonly factory: () => BearerAuthRuntime<TContribution, TResponse, TUnauthorizedBody>;
|
|
40
|
+
}
|
|
41
|
+
export interface BearerAuthRuntime<TContribution extends object, TResponse extends string, TUnauthorizedBody> {
|
|
42
|
+
authenticate(token: string): TContribution | null | undefined | Promise<TContribution | null | undefined>;
|
|
43
|
+
unauthorized(): HttpAuthenticationFailure<TResponse, TUnauthorizedBody>;
|
|
44
|
+
}
|
|
45
|
+
export declare function bearerAuth<const TContribution extends object, const TResponse extends string, TUnauthorizedBody>(definition: BearerAuthDefinition<TContribution, TResponse, TUnauthorizedBody>): HttpMiddleware<TContribution, HttpMiddlewareContext, AuthenticationShortCircuit<TResponse, TUnauthorizedBody>>;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { defineLayer } from '../core/index.js';
|
|
2
|
+
export function basicAuth(definition) {
|
|
3
|
+
const challenge = formatBasicChallenge(definition.realm);
|
|
4
|
+
return defineLayer({
|
|
5
|
+
name: definition.name ?? 'basicAuth',
|
|
6
|
+
factory: () => {
|
|
7
|
+
const runtime = definition.factory();
|
|
8
|
+
return async (context, next) => {
|
|
9
|
+
const credentials = decodeBasicCredentials(context.request.headers.get('authorization'));
|
|
10
|
+
if (!credentials) {
|
|
11
|
+
return authenticationFailure(runtime.unauthorized(), challenge);
|
|
12
|
+
}
|
|
13
|
+
const contribution = await runtime.authenticate(credentials);
|
|
14
|
+
if (contribution == null) {
|
|
15
|
+
return authenticationFailure(runtime.unauthorized(), challenge);
|
|
16
|
+
}
|
|
17
|
+
await next(contribution);
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function bearerAuth(definition) {
|
|
23
|
+
const challenge = formatBearerChallenge(definition.realm);
|
|
24
|
+
return defineLayer({
|
|
25
|
+
name: definition.name ?? 'bearerAuth',
|
|
26
|
+
factory: () => {
|
|
27
|
+
const runtime = definition.factory();
|
|
28
|
+
return async (context, next) => {
|
|
29
|
+
const token = readBearerToken(context.request.headers.get('authorization'));
|
|
30
|
+
if (!token) {
|
|
31
|
+
return authenticationFailure(runtime.unauthorized(), challenge);
|
|
32
|
+
}
|
|
33
|
+
const contribution = await runtime.authenticate(token);
|
|
34
|
+
if (contribution == null) {
|
|
35
|
+
return authenticationFailure(runtime.unauthorized(), challenge);
|
|
36
|
+
}
|
|
37
|
+
await next(contribution);
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function authenticationFailure(failure, challenge) {
|
|
43
|
+
return {
|
|
44
|
+
kind: 'http-result',
|
|
45
|
+
response: failure.response,
|
|
46
|
+
body: failure.body,
|
|
47
|
+
headers: { 'www-authenticate': challenge },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function formatBasicChallenge(realm) {
|
|
51
|
+
assertValidRealm(realm, 'Basic');
|
|
52
|
+
return `Basic realm="${escapeChallengeValue(realm)}", charset="UTF-8"`;
|
|
53
|
+
}
|
|
54
|
+
function formatBearerChallenge(realm) {
|
|
55
|
+
assertValidRealm(realm, 'Bearer');
|
|
56
|
+
return `Bearer realm="${escapeChallengeValue(realm)}"`;
|
|
57
|
+
}
|
|
58
|
+
function assertValidRealm(realm, scheme) {
|
|
59
|
+
if (realm.length === 0 || containsControlCharacter(realm)) {
|
|
60
|
+
throw new TypeError(`${scheme} authentication realm cannot be empty or contain control characters`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function containsControlCharacter(value) {
|
|
64
|
+
for (const character of value) {
|
|
65
|
+
const code = character.charCodeAt(0);
|
|
66
|
+
if (code <= 0x1f || code === 0x7f)
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
function decodeBasicCredentials(authorization) {
|
|
72
|
+
const encoded = /^Basic +([A-Za-z0-9+/]+={0,2})$/i.exec(authorization ?? '')?.[1];
|
|
73
|
+
if (!encoded)
|
|
74
|
+
return undefined;
|
|
75
|
+
try {
|
|
76
|
+
const binary = atob(encoded);
|
|
77
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
78
|
+
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
79
|
+
const separator = decoded.indexOf(':');
|
|
80
|
+
if (separator < 0)
|
|
81
|
+
return undefined;
|
|
82
|
+
return {
|
|
83
|
+
username: decoded.slice(0, separator),
|
|
84
|
+
password: decoded.slice(separator + 1),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function readBearerToken(authorization) {
|
|
92
|
+
return /^Bearer +([^\s]+)$/i.exec(authorization ?? '')?.[1];
|
|
93
|
+
}
|
|
94
|
+
function escapeChallengeValue(value) {
|
|
95
|
+
return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
96
|
+
}
|
package/dist/http/client.d.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import type {
|
|
3
|
-
import { type PathParamNames
|
|
1
|
+
import { type SchemaInput, type SchemaOutput, type StandardSchemaV1 } from '../core/index.js';
|
|
2
|
+
import type { HttpContract, HttpExecutionResponseDefinition, HttpExecutionRouteDefinition, HttpHeaders } from './extension.js';
|
|
3
|
+
import { type PathParamNames } from './path.js';
|
|
4
4
|
export interface HttpClientTransportRequest {
|
|
5
5
|
readonly method: string;
|
|
6
6
|
readonly path: string;
|
|
7
7
|
readonly query?: unknown;
|
|
8
8
|
readonly headers?: unknown;
|
|
9
9
|
readonly body?: unknown;
|
|
10
|
-
readonly contentType?: string;
|
|
11
10
|
}
|
|
12
11
|
export interface HttpClientTransportResponse {
|
|
13
12
|
readonly status: number;
|
|
@@ -15,45 +14,43 @@ export interface HttpClientTransportResponse {
|
|
|
15
14
|
readonly headers?: HttpHeaders;
|
|
16
15
|
}
|
|
17
16
|
export type HttpClientTransport = (request: HttpClientTransportRequest) => Promise<HttpClientTransportResponse>;
|
|
18
|
-
type
|
|
19
|
-
readonly
|
|
20
|
-
} ? TProtocol extends HttpProtocol<infer TDefinition> ? HttpProtocol<TDefinition> : never : never;
|
|
21
|
-
type HttpProcedureNames<TContract extends ContractDefinition> = {
|
|
22
|
-
[TProcedure in keyof TContract['procedures'] & string]: HttpProtocolOf<TContract['procedures'][TProcedure]> extends never ? never : TProcedure;
|
|
23
|
-
}[keyof TContract['procedures'] & string];
|
|
24
|
-
type HttpDefinitionOf<TContract extends ContractDefinition, TProcedure extends HttpProcedureNames<TContract>> = HttpProtocolOf<TContract['procedures'][TProcedure]>['definition'];
|
|
25
|
-
type RequestBodySchema<TBody> = TBody extends HttpRequestBodyDefinition<infer TSchema> ? TSchema : never;
|
|
26
|
-
type PathParamsInput<TDefinition extends HttpProtocolDefinition> = PathParamNames<TDefinition['path']> extends never ? never : TDefinition['request'] extends {
|
|
27
|
-
readonly params: infer TSchemas extends HttpParamsSchemas;
|
|
17
|
+
type PathParamsInput<TDefinition extends HttpExecutionRouteDefinition> = PathParamNames<TDefinition['path']> extends never ? never : TDefinition['request'] extends {
|
|
18
|
+
readonly params: infer TSchemas extends Readonly<Record<string, StandardSchemaV1>>;
|
|
28
19
|
} ? Readonly<{
|
|
29
20
|
[TName in keyof TSchemas]: SchemaInput<TSchemas[TName]>;
|
|
30
|
-
}> :
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
21
|
+
}> : Readonly<{
|
|
22
|
+
[TName in PathParamNames<TDefinition['path']>]: string;
|
|
23
|
+
}>;
|
|
24
|
+
type RequestPartInput<TDefinition extends HttpExecutionRouteDefinition, TPart extends 'query' | 'headers'> = TDefinition['request'] extends infer TRequest ? TRequest extends Record<TPart, infer TSchema extends StandardSchemaV1> ? SchemaInput<TSchema> : never : never;
|
|
25
|
+
type RequestBodyInput<TDefinition extends HttpExecutionRouteDefinition> = TDefinition['request'] extends {
|
|
26
|
+
readonly body: infer TBody extends StandardSchemaV1;
|
|
27
|
+
} ? SchemaInput<TBody> : never;
|
|
35
28
|
type RequestField<TName extends string, TValue> = [TValue] extends [never] ? object : undefined extends TValue ? {
|
|
36
29
|
readonly [TKey in TName]?: Exclude<TValue, undefined>;
|
|
37
30
|
} : {
|
|
38
31
|
readonly [TKey in TName]: TValue;
|
|
39
32
|
};
|
|
40
|
-
export type HttpClientRequest<TDefinition extends
|
|
41
|
-
type ResponseBodyOutput<TResponse extends
|
|
33
|
+
export type HttpClientRequest<TDefinition extends HttpExecutionRouteDefinition> = RequestField<'params', PathParamsInput<TDefinition>> & RequestField<'query', RequestPartInput<TDefinition, 'query'>> & RequestField<'headers', RequestPartInput<TDefinition, 'headers'>> & RequestField<'body', RequestBodyInput<TDefinition>>;
|
|
34
|
+
type ResponseBodyOutput<TResponse extends HttpExecutionResponseDefinition> = TResponse extends {
|
|
42
35
|
readonly stream: 'server';
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
readonly body: infer TBody extends StandardSchemaV1;
|
|
37
|
+
} ? AsyncIterable<SchemaOutput<TBody>> : TResponse['body'] extends StandardSchemaV1 ? SchemaOutput<TResponse['body']> : undefined;
|
|
38
|
+
type ResponseHeadersSchema<TResponse extends HttpExecutionResponseDefinition> = TResponse extends {
|
|
39
|
+
readonly headers: infer THeaders;
|
|
40
|
+
} ? THeaders extends StandardSchemaV1 ? THeaders : THeaders extends {
|
|
41
|
+
readonly schema: infer TSchema extends StandardSchemaV1;
|
|
42
|
+
} ? TSchema : never : never;
|
|
43
|
+
type ResponseHeadersOutput<TResponse extends HttpExecutionResponseDefinition> = ResponseHeadersSchema<TResponse> extends infer TSchema ? [TSchema] extends [never] ? HttpHeaders : TSchema extends StandardSchemaV1 ? SchemaOutput<TSchema> : HttpHeaders : HttpHeaders;
|
|
44
|
+
export type HttpClientResponse<TDefinition extends HttpExecutionRouteDefinition> = {
|
|
45
|
+
[TVariant in keyof TDefinition['responses'] & string]: TDefinition['responses'][TVariant] extends infer TResponse extends HttpExecutionResponseDefinition ? {
|
|
49
46
|
readonly status: TResponse['status'];
|
|
50
47
|
readonly body: ResponseBodyOutput<TResponse>;
|
|
51
48
|
readonly headers: ResponseHeadersOutput<TResponse>;
|
|
52
49
|
} : never;
|
|
53
50
|
}[keyof TDefinition['responses'] & string];
|
|
54
|
-
type HttpClientMethod<TDefinition extends
|
|
55
|
-
export type HttpClient<
|
|
56
|
-
readonly [
|
|
51
|
+
type HttpClientMethod<TDefinition extends HttpExecutionRouteDefinition> = keyof HttpClientRequest<TDefinition> extends never ? () => Promise<HttpClientResponse<TDefinition>> : (request: HttpClientRequest<TDefinition>) => Promise<HttpClientResponse<TDefinition>>;
|
|
52
|
+
export type HttpClient<TContract extends HttpContract> = {
|
|
53
|
+
readonly [TName in keyof TContract['routes']]: HttpClientMethod<TContract['routes'][TName]>;
|
|
57
54
|
};
|
|
58
55
|
export declare class HttpClientResponseError extends Error {
|
|
59
56
|
readonly status: number;
|
|
@@ -62,7 +59,7 @@ export declare class HttpClientResponseError extends Error {
|
|
|
62
59
|
readonly procedure: string;
|
|
63
60
|
constructor(status: number, method: string, path: string, procedure: string, message: string, options?: ErrorOptions);
|
|
64
61
|
}
|
|
65
|
-
export declare function createHttpClient<const
|
|
62
|
+
export declare function createHttpClient<const TContract extends HttpContract>(contract: TContract, transport: HttpClientTransport): HttpClient<TContract>;
|
|
66
63
|
export interface FetchHttpTransportOptions {
|
|
67
64
|
readonly baseUrl: string | URL;
|
|
68
65
|
readonly fetch?: typeof globalThis.fetch;
|
package/dist/http/client.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { SchemaValidationError, validateSchema, } from '../core/index.js';
|
|
2
|
-
import {
|
|
3
|
-
import { parseHttpPath, } from './path.js';
|
|
2
|
+
import { parseHttpPath } from './path.js';
|
|
4
3
|
export class HttpClientResponseError extends Error {
|
|
5
4
|
status;
|
|
6
5
|
method;
|
|
@@ -15,13 +14,9 @@ export class HttpClientResponseError extends Error {
|
|
|
15
14
|
this.name = 'HttpClientResponseError';
|
|
16
15
|
}
|
|
17
16
|
}
|
|
18
|
-
export function createHttpClient(
|
|
19
|
-
const contract = contractOfBinding(binding);
|
|
17
|
+
export function createHttpClient(contract, transport) {
|
|
20
18
|
const client = {};
|
|
21
|
-
for (const [procedureName,
|
|
22
|
-
const definition = httpDefinitionOf(procedure);
|
|
23
|
-
if (!definition)
|
|
24
|
-
continue;
|
|
19
|
+
for (const [procedureName, definition] of Object.entries(contract.routes)) {
|
|
25
20
|
client[procedureName] = async (request = {}) => {
|
|
26
21
|
const response = await transport({
|
|
27
22
|
method: definition.method,
|
|
@@ -29,24 +24,12 @@ export function createHttpClient(binding, transport) {
|
|
|
29
24
|
...(request.query === undefined ? {} : { query: request.query }),
|
|
30
25
|
...(request.headers === undefined ? {} : { headers: request.headers }),
|
|
31
26
|
...(request.body === undefined ? {} : { body: request.body }),
|
|
32
|
-
...(definition.request?.body === undefined
|
|
33
|
-
? {}
|
|
34
|
-
: { contentType: definition.request.body.contentType }),
|
|
35
27
|
});
|
|
36
28
|
return decodeResponse(procedureName, definition, response);
|
|
37
29
|
};
|
|
38
30
|
}
|
|
39
31
|
return Object.freeze(client);
|
|
40
32
|
}
|
|
41
|
-
function httpDefinitionOf(procedure) {
|
|
42
|
-
const protocol = procedure.protocols.http;
|
|
43
|
-
if (!protocol ||
|
|
44
|
-
protocol.protocol !== 'http' ||
|
|
45
|
-
!('definition' in protocol)) {
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
return protocol.definition;
|
|
49
|
-
}
|
|
50
33
|
function interpolatePath(path, params) {
|
|
51
34
|
const segments = parseHttpPath(path);
|
|
52
35
|
if (segments.length === 0)
|
|
@@ -72,11 +55,23 @@ async function decodeResponse(procedure, definition, response) {
|
|
|
72
55
|
let lastValidationError;
|
|
73
56
|
for (const candidate of candidates) {
|
|
74
57
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
58
|
+
let body;
|
|
59
|
+
if (candidate.stream === 'server') {
|
|
60
|
+
if (!candidate.body)
|
|
61
|
+
continue;
|
|
62
|
+
body = validatedResponseStream(candidate.body, response.body, procedure, definition, response.status);
|
|
63
|
+
}
|
|
64
|
+
else if (candidate.body) {
|
|
65
|
+
body = await validateSchema(candidate.body, response.body);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
if (response.body !== undefined && response.body !== null)
|
|
69
|
+
continue;
|
|
70
|
+
body = undefined;
|
|
71
|
+
}
|
|
72
|
+
const headerSchema = responseHeadersSchema(candidate.headers);
|
|
73
|
+
const headers = headerSchema
|
|
74
|
+
? await validateSchema(headerSchema, response.headers ?? {})
|
|
80
75
|
: (response.headers ?? {});
|
|
81
76
|
return Object.freeze({ status: response.status, body, headers });
|
|
82
77
|
}
|
|
@@ -88,6 +83,22 @@ async function decodeResponse(procedure, definition, response) {
|
|
|
88
83
|
}
|
|
89
84
|
throw new HttpClientResponseError(response.status, definition.method, definition.path, procedure, `${target} returned a response that does not match its Contract`, lastValidationError ? { cause: lastValidationError } : undefined);
|
|
90
85
|
}
|
|
86
|
+
function responseHeadersSchema(headers) {
|
|
87
|
+
if (isStandardSchema(headers))
|
|
88
|
+
return headers;
|
|
89
|
+
if (isResponseHeadersWithDefaults(headers))
|
|
90
|
+
return headers.schema;
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
function isStandardSchema(value) {
|
|
94
|
+
return typeof value === 'object' && value !== null && '~standard' in value;
|
|
95
|
+
}
|
|
96
|
+
function isResponseHeadersWithDefaults(value) {
|
|
97
|
+
return (typeof value === 'object' &&
|
|
98
|
+
value !== null &&
|
|
99
|
+
'schema' in value &&
|
|
100
|
+
isStandardSchema(value.schema));
|
|
101
|
+
}
|
|
91
102
|
function validatedResponseStream(schema, value, procedure, definition, status) {
|
|
92
103
|
const target = describeHttpTarget(definition, procedure);
|
|
93
104
|
if (!isAsyncIterable(value)) {
|
|
@@ -121,15 +132,18 @@ export function fetchHttpTransport(options) {
|
|
|
121
132
|
appendQuery(url.searchParams, request.query);
|
|
122
133
|
const headers = new Headers(options.headers);
|
|
123
134
|
appendHeaders(headers, request.headers);
|
|
124
|
-
|
|
125
|
-
|
|
135
|
+
const contentType = headers.get('content-type') ?? undefined;
|
|
136
|
+
const body = request.body === undefined
|
|
137
|
+
? undefined
|
|
138
|
+
: encodeBody(request.body, contentType);
|
|
139
|
+
if (body instanceof FormData &&
|
|
140
|
+
normalizeMediaType(contentType) === 'multipart/form-data') {
|
|
141
|
+
headers.delete('content-type');
|
|
126
142
|
}
|
|
127
143
|
const response = await fetchImplementation(url, {
|
|
128
144
|
method: request.method,
|
|
129
145
|
headers,
|
|
130
|
-
...(
|
|
131
|
-
? {}
|
|
132
|
-
: { body: encodeBody(request.body, request.contentType) }),
|
|
146
|
+
...(body === undefined ? {} : { body }),
|
|
133
147
|
});
|
|
134
148
|
return {
|
|
135
149
|
status: response.status,
|
|
@@ -191,7 +205,8 @@ function encodeBody(body, contentType) {
|
|
|
191
205
|
return body;
|
|
192
206
|
}
|
|
193
207
|
async function decodeFetchBody(response) {
|
|
194
|
-
if (response.
|
|
208
|
+
if (response.body === null ||
|
|
209
|
+
response.status === 204 ||
|
|
195
210
|
response.status === 205 ||
|
|
196
211
|
response.status === 304) {
|
|
197
212
|
return undefined;
|
|
@@ -217,16 +232,25 @@ async function* decodeServerSentEvents(stream) {
|
|
|
217
232
|
const decoder = new TextDecoder();
|
|
218
233
|
let buffer = '';
|
|
219
234
|
let completed = false;
|
|
235
|
+
let pendingCarriageReturn = false;
|
|
236
|
+
const appendDecoded = (value, final) => {
|
|
237
|
+
let decoded = pendingCarriageReturn ? `\r${value}` : value;
|
|
238
|
+
pendingCarriageReturn = false;
|
|
239
|
+
if (!final && decoded.endsWith('\r')) {
|
|
240
|
+
pendingCarriageReturn = true;
|
|
241
|
+
decoded = decoded.slice(0, -1);
|
|
242
|
+
}
|
|
243
|
+
buffer += normalizeEventStreamNewlines(decoded);
|
|
244
|
+
};
|
|
220
245
|
try {
|
|
221
246
|
while (true) {
|
|
222
247
|
const next = await reader.read();
|
|
223
248
|
if (next.done) {
|
|
224
249
|
completed = true;
|
|
225
|
-
|
|
250
|
+
appendDecoded(decoder.decode(), true);
|
|
226
251
|
break;
|
|
227
252
|
}
|
|
228
|
-
|
|
229
|
-
buffer = normalizeEventStreamNewlines(buffer);
|
|
253
|
+
appendDecoded(decoder.decode(next.value, { stream: true }), false);
|
|
230
254
|
let boundary = buffer.indexOf('\n\n');
|
|
231
255
|
while (boundary >= 0) {
|
|
232
256
|
const event = buffer.slice(0, boundary);
|
|
@@ -241,16 +265,6 @@ async function* decodeServerSentEvents(stream) {
|
|
|
241
265
|
boundary = buffer.indexOf('\n\n');
|
|
242
266
|
}
|
|
243
267
|
}
|
|
244
|
-
buffer = normalizeEventStreamNewlines(buffer).trim();
|
|
245
|
-
if (buffer.length > 0) {
|
|
246
|
-
const data = buffer
|
|
247
|
-
.split('\n')
|
|
248
|
-
.filter((line) => line.startsWith('data:'))
|
|
249
|
-
.map((line) => line.slice('data:'.length).trimStart())
|
|
250
|
-
.join('\n');
|
|
251
|
-
if (data.length > 0)
|
|
252
|
-
yield JSON.parse(data);
|
|
253
|
-
}
|
|
254
268
|
}
|
|
255
269
|
finally {
|
|
256
270
|
if (!completed)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type CorsOrigin = string | readonly string[] | ((origin: string) => boolean | Promise<boolean>);
|
|
2
|
+
export interface CorsPolicyOptions {
|
|
3
|
+
readonly origin?: CorsOrigin;
|
|
4
|
+
readonly allowMethods?: readonly string[];
|
|
5
|
+
readonly allowHeaders?: readonly string[];
|
|
6
|
+
readonly exposeHeaders?: readonly string[];
|
|
7
|
+
readonly credentials?: boolean;
|
|
8
|
+
readonly maxAge?: number;
|
|
9
|
+
}
|
|
10
|
+
export type CorsPolicy = Readonly<{
|
|
11
|
+
origin: {
|
|
12
|
+
readonly kind: 'wildcard';
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: 'exact';
|
|
15
|
+
readonly origins: ReadonlySet<string>;
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: 'predicate';
|
|
18
|
+
readonly predicate: (origin: string) => boolean | Promise<boolean>;
|
|
19
|
+
};
|
|
20
|
+
allowMethods: readonly string[] | undefined;
|
|
21
|
+
allowHeaders: readonly string[] | undefined;
|
|
22
|
+
exposeHeaders: readonly string[];
|
|
23
|
+
credentials: boolean;
|
|
24
|
+
maxAge: number | undefined;
|
|
25
|
+
}>;
|
|
26
|
+
export declare function normalizeCorsPolicy(options: CorsPolicyOptions): CorsPolicy;
|
|
27
|
+
export declare function registerCorsPolicy(middleware: object, policy: CorsPolicy): void;
|
|
28
|
+
export declare function createCorsActualResponseHeaders(middlewares: readonly object[], request: Request): Promise<Headers | undefined>;
|
|
29
|
+
export declare function createCorsPreflightResponseHeaders(middlewares: readonly object[], request: Request, targetMethod: string): Promise<Headers | undefined>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const policies = new WeakMap();
|
|
2
|
+
const httpToken = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
3
|
+
export function normalizeCorsPolicy(options) {
|
|
4
|
+
const credentials = options.credentials ?? false;
|
|
5
|
+
return Object.freeze({
|
|
6
|
+
origin: normalizeOrigin(options.origin ?? '*', credentials),
|
|
7
|
+
allowMethods: options.allowMethods === undefined
|
|
8
|
+
? undefined
|
|
9
|
+
: normalizeTokens(options.allowMethods, 'allowMethods', true),
|
|
10
|
+
allowHeaders: options.allowHeaders === undefined
|
|
11
|
+
? undefined
|
|
12
|
+
: normalizeTokens(options.allowHeaders, 'allowHeaders', false),
|
|
13
|
+
exposeHeaders: normalizeTokens(options.exposeHeaders ?? [], 'exposeHeaders', false),
|
|
14
|
+
credentials,
|
|
15
|
+
maxAge: normalizeMaxAge(options.maxAge),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function registerCorsPolicy(middleware, policy) {
|
|
19
|
+
policies.set(middleware, policy);
|
|
20
|
+
}
|
|
21
|
+
export async function createCorsActualResponseHeaders(middlewares, request) {
|
|
22
|
+
const policy = corsPolicyOfMiddlewares(middlewares);
|
|
23
|
+
if (!policy)
|
|
24
|
+
return undefined;
|
|
25
|
+
const origin = request.headers.get('origin');
|
|
26
|
+
const headers = new Headers();
|
|
27
|
+
if (policy.origin.kind !== 'wildcard')
|
|
28
|
+
appendVary(headers, 'Origin');
|
|
29
|
+
if (origin === null)
|
|
30
|
+
return headers;
|
|
31
|
+
await applyOrigin(headers, policy, origin);
|
|
32
|
+
if (!headers.has('access-control-allow-origin'))
|
|
33
|
+
return headers;
|
|
34
|
+
if (policy.credentials) {
|
|
35
|
+
headers.set('access-control-allow-credentials', 'true');
|
|
36
|
+
}
|
|
37
|
+
if (policy.exposeHeaders.length > 0) {
|
|
38
|
+
headers.set('access-control-expose-headers', policy.exposeHeaders.join(', '));
|
|
39
|
+
}
|
|
40
|
+
return headers;
|
|
41
|
+
}
|
|
42
|
+
export async function createCorsPreflightResponseHeaders(middlewares, request, targetMethod) {
|
|
43
|
+
const policy = corsPolicyOfMiddlewares(middlewares);
|
|
44
|
+
if (!policy)
|
|
45
|
+
return undefined;
|
|
46
|
+
const headers = new Headers();
|
|
47
|
+
appendVary(headers, 'Access-Control-Request-Method');
|
|
48
|
+
const origin = request.headers.get('origin');
|
|
49
|
+
if (origin === null)
|
|
50
|
+
return headers;
|
|
51
|
+
await applyOrigin(headers, policy, origin);
|
|
52
|
+
if (!headers.has('access-control-allow-origin'))
|
|
53
|
+
return headers;
|
|
54
|
+
if (policy.credentials) {
|
|
55
|
+
headers.set('access-control-allow-credentials', 'true');
|
|
56
|
+
}
|
|
57
|
+
const methods = policy.allowMethods ?? [targetMethod.toUpperCase()];
|
|
58
|
+
if (methods.length > 0) {
|
|
59
|
+
headers.set('access-control-allow-methods', methods.join(', '));
|
|
60
|
+
}
|
|
61
|
+
if (policy.allowHeaders === undefined) {
|
|
62
|
+
const requested = request.headers.get('access-control-request-headers');
|
|
63
|
+
if (requested !== null && requested.trim().length > 0) {
|
|
64
|
+
headers.set('access-control-allow-headers', requested);
|
|
65
|
+
appendVary(headers, 'Access-Control-Request-Headers');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (policy.allowHeaders.length > 0) {
|
|
69
|
+
headers.set('access-control-allow-headers', policy.allowHeaders.join(', '));
|
|
70
|
+
}
|
|
71
|
+
if (policy.maxAge !== undefined) {
|
|
72
|
+
headers.set('access-control-max-age', String(policy.maxAge));
|
|
73
|
+
}
|
|
74
|
+
return headers;
|
|
75
|
+
}
|
|
76
|
+
function corsPolicyOfMiddlewares(middlewares) {
|
|
77
|
+
let found;
|
|
78
|
+
for (const middleware of middlewares) {
|
|
79
|
+
const policy = policies.get(middleware);
|
|
80
|
+
if (policy)
|
|
81
|
+
found = policy;
|
|
82
|
+
}
|
|
83
|
+
return found;
|
|
84
|
+
}
|
|
85
|
+
async function applyOrigin(headers, policy, origin) {
|
|
86
|
+
if (policy.origin.kind === 'wildcard') {
|
|
87
|
+
headers.set('access-control-allow-origin', '*');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
appendVary(headers, 'Origin');
|
|
91
|
+
const allowed = policy.origin.kind === 'exact'
|
|
92
|
+
? policy.origin.origins.has(origin)
|
|
93
|
+
: await policy.origin.predicate(origin);
|
|
94
|
+
if (allowed)
|
|
95
|
+
headers.set('access-control-allow-origin', origin);
|
|
96
|
+
}
|
|
97
|
+
function normalizeOrigin(origin, credentials) {
|
|
98
|
+
if (typeof origin === 'function') {
|
|
99
|
+
return Object.freeze({ kind: 'predicate', predicate: origin });
|
|
100
|
+
}
|
|
101
|
+
const origins = typeof origin === 'string' ? [origin] : [...origin];
|
|
102
|
+
if (origins.length === 0) {
|
|
103
|
+
return Object.freeze({ kind: 'exact', origins: new Set() });
|
|
104
|
+
}
|
|
105
|
+
for (const value of origins) {
|
|
106
|
+
if (value.length === 0 || containsControlCharacter(value)) {
|
|
107
|
+
throw new TypeError('CORS origin cannot be empty or contain control characters');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (origins.includes('*')) {
|
|
111
|
+
if (credentials) {
|
|
112
|
+
throw new TypeError('CORS origin cannot be a wildcard when credentials are enabled');
|
|
113
|
+
}
|
|
114
|
+
return Object.freeze({ kind: 'wildcard' });
|
|
115
|
+
}
|
|
116
|
+
return Object.freeze({
|
|
117
|
+
kind: 'exact',
|
|
118
|
+
origins: new Set(origins),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function containsControlCharacter(value) {
|
|
122
|
+
for (const character of value) {
|
|
123
|
+
const code = character.charCodeAt(0);
|
|
124
|
+
if (code <= 0x1f || code === 0x7f)
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
function normalizeTokens(values, option, uppercase) {
|
|
130
|
+
return Object.freeze(values.map((value) => {
|
|
131
|
+
const token = value.trim();
|
|
132
|
+
if (!httpToken.test(token)) {
|
|
133
|
+
throw new TypeError(`CORS ${option} contains an invalid HTTP token`);
|
|
134
|
+
}
|
|
135
|
+
return uppercase ? token.toUpperCase() : token;
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
function normalizeMaxAge(maxAge) {
|
|
139
|
+
if (maxAge === undefined)
|
|
140
|
+
return undefined;
|
|
141
|
+
if (!Number.isSafeInteger(maxAge) || maxAge < 0) {
|
|
142
|
+
throw new TypeError('CORS maxAge must be a non-negative safe integer');
|
|
143
|
+
}
|
|
144
|
+
return maxAge;
|
|
145
|
+
}
|
|
146
|
+
function appendVary(headers, value) {
|
|
147
|
+
const current = headers.get('vary');
|
|
148
|
+
if (current === null) {
|
|
149
|
+
headers.set('vary', value);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const values = current
|
|
153
|
+
.split(',')
|
|
154
|
+
.map((item) => item.trim())
|
|
155
|
+
.filter(Boolean);
|
|
156
|
+
if (!values.some((item) => item.toLowerCase() === value.toLowerCase())) {
|
|
157
|
+
values.push(value);
|
|
158
|
+
}
|
|
159
|
+
headers.set('vary', values.join(', '));
|
|
160
|
+
}
|
package/dist/http/cors.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
|
|
1
|
+
import { type CorsOrigin } from './cors-internal.js';
|
|
2
|
+
import type { HttpExecutionResult, HttpMiddlewareContext } from './extension.js';
|
|
3
|
+
export type { CorsOrigin } from './cors-internal.js';
|
|
3
4
|
export interface CorsOptions {
|
|
4
5
|
readonly origin?: CorsOrigin;
|
|
5
6
|
readonly allowMethods?: readonly string[];
|
|
@@ -9,9 +10,5 @@ export interface CorsOptions {
|
|
|
9
10
|
readonly maxAge?: number;
|
|
10
11
|
readonly name?: string;
|
|
11
12
|
}
|
|
12
|
-
export
|
|
13
|
-
|
|
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>;
|
|
13
|
+
export declare function cors(options?: CorsOptions): import("../core/generic-layer.js").GenericLayer<HttpMiddlewareContext, {}, HttpExecutionResult>;
|
|
14
|
+
export type CorsLayerDescriptor = ReturnType<typeof cors>;
|