@loutrejs/loutre 0.4.1 → 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.
Files changed (164) hide show
  1. package/README.md +63 -32
  2. package/dist/adapters/aws-lambda.d.ts +5 -3
  3. package/dist/adapters/aws-lambda.js +53 -23
  4. package/dist/adapters/bun.d.ts +7 -4
  5. package/dist/adapters/bun.js +52 -42
  6. package/dist/adapters/cloudflare-workers.d.ts +5 -3
  7. package/dist/adapters/cloudflare-workers.js +23 -28
  8. package/dist/adapters/deno.d.ts +7 -4
  9. package/dist/adapters/deno.js +94 -57
  10. package/dist/adapters/electron.d.ts +13 -5
  11. package/dist/adapters/electron.js +76 -28
  12. package/dist/application/definition.d.ts +31 -0
  13. package/dist/application/definition.js +16 -0
  14. package/dist/application/index.d.ts +4 -63
  15. package/dist/application/index.js +2 -11
  16. package/dist/application/kernel-internal.d.ts +2 -0
  17. package/dist/application/kernel-internal.js +10 -0
  18. package/dist/application/kernel.d.ts +21 -0
  19. package/dist/application/kernel.js +65 -0
  20. package/dist/core/diagnostic.d.ts +9 -0
  21. package/dist/core/diagnostic.js +9 -0
  22. package/dist/core/extension.d.ts +107 -0
  23. package/dist/core/extension.js +73 -0
  24. package/dist/core/generic-layer.d.ts +26 -0
  25. package/dist/core/generic-layer.js +116 -0
  26. package/dist/core/index.d.ts +31 -15
  27. package/dist/core/index.js +15 -15
  28. package/dist/core/injection.d.ts +4 -13
  29. package/dist/core/injection.js +63 -0
  30. package/dist/core/model/context.d.ts +18 -0
  31. package/dist/core/model/context.js +34 -0
  32. package/dist/core/model/lifecycle.d.ts +5 -0
  33. package/dist/core/model/lifecycle.js +44 -0
  34. package/dist/core/model/modules.d.ts +2 -0
  35. package/dist/core/model/modules.js +101 -0
  36. package/dist/core/model/providers.d.ts +4 -0
  37. package/dist/core/model/providers.js +74 -0
  38. package/dist/core/model/snapshot.d.ts +6 -0
  39. package/dist/core/model/snapshot.js +31 -0
  40. package/dist/core/model/types.d.ts +78 -0
  41. package/dist/core/model/validation.d.ts +4 -0
  42. package/dist/core/model/validation.js +40 -0
  43. package/dist/core/model/visibility.d.ts +4 -0
  44. package/dist/core/model/visibility.js +20 -0
  45. package/dist/core/model.d.ts +10 -0
  46. package/dist/core/model.js +346 -0
  47. package/dist/core/module.d.ts +8 -13
  48. package/dist/core/source-location.d.ts +8 -0
  49. package/dist/core/source-location.js +31 -0
  50. package/dist/core/token-internal.d.ts +3 -0
  51. package/dist/core/token-internal.js +4 -0
  52. package/dist/core/type.d.ts +7 -0
  53. package/dist/core/type.js +4 -0
  54. package/dist/generated/version.d.ts +1 -1
  55. package/dist/generated/version.js +1 -1
  56. package/dist/graph/index.d.ts +2 -3
  57. package/dist/graph/index.js +1 -3
  58. package/dist/graph/model.d.ts +34 -0
  59. package/dist/graph/model.js +127 -0
  60. package/dist/http/auth.d.ts +46 -0
  61. package/dist/http/auth.js +96 -0
  62. package/dist/http/client.d.ts +28 -31
  63. package/dist/http/client.js +60 -41
  64. package/dist/http/cors-internal.d.ts +29 -0
  65. package/dist/http/cors-internal.js +160 -0
  66. package/dist/http/cors.d.ts +5 -8
  67. package/dist/http/cors.js +7 -165
  68. package/dist/http/extension.d.ts +348 -0
  69. package/dist/http/extension.js +947 -0
  70. package/dist/http/index.d.ts +9 -7
  71. package/dist/http/index.js +5 -7
  72. package/dist/{application → http}/openapi.d.ts +12 -3
  73. package/dist/{application → http}/openapi.js +164 -101
  74. package/dist/http/path.d.ts +2 -39
  75. package/dist/http/path.js +7 -2
  76. package/dist/http/runtime/request.d.ts +17 -0
  77. package/dist/http/runtime/request.js +109 -0
  78. package/dist/http/runtime/response.d.ts +18 -0
  79. package/dist/http/runtime/response.js +108 -0
  80. package/dist/index.d.ts +4 -0
  81. package/dist/index.js +2 -0
  82. package/dist/message-port/extension.d.ts +66 -0
  83. package/dist/message-port/extension.js +395 -0
  84. package/dist/message-port/index.d.ts +4 -73
  85. package/dist/message-port/index.js +2 -189
  86. package/dist/message-port/transport.d.ts +11 -0
  87. package/dist/message-port/transport.js +45 -0
  88. package/dist/runtime/async-iterator-lifecycle.d.ts +12 -0
  89. package/dist/runtime/async-iterator-lifecycle.js +95 -0
  90. package/dist/runtime/capability.d.ts +11 -11
  91. package/dist/runtime/capability.js +3 -3
  92. package/dist/runtime/di.d.ts +2 -22
  93. package/dist/runtime/di.js +8 -285
  94. package/dist/runtime/execution-tracker.d.ts +8 -0
  95. package/dist/runtime/execution-tracker.js +54 -0
  96. package/dist/runtime/index.d.ts +15 -9
  97. package/dist/runtime/index.js +9 -9
  98. package/dist/runtime/ingress-gate.d.ts +7 -0
  99. package/dist/runtime/ingress-gate.js +33 -0
  100. package/dist/runtime/kernel.d.ts +23 -0
  101. package/dist/runtime/kernel.js +354 -0
  102. package/dist/runtime/logger.d.ts +2 -0
  103. package/dist/runtime/logger.js +2 -0
  104. package/dist/runtime/node-capabilities.d.ts +1 -1
  105. package/dist/runtime/node-capabilities.js +1 -1
  106. package/dist/runtime/server-port.d.ts +2 -0
  107. package/dist/runtime/server-port.js +11 -0
  108. package/dist/tasks/extension.d.ts +144 -0
  109. package/dist/tasks/extension.js +499 -0
  110. package/dist/tasks/index.d.ts +2 -0
  111. package/dist/tasks/index.js +1 -0
  112. package/dist/tasks/runtime/cron.d.ts +6 -0
  113. package/dist/tasks/runtime/cron.js +89 -0
  114. package/dist/websocket/errors.d.ts +9 -0
  115. package/dist/websocket/errors.js +18 -0
  116. package/dist/websocket/extension.d.ts +154 -0
  117. package/dist/websocket/extension.js +524 -0
  118. package/dist/websocket/index.d.ts +3 -0
  119. package/dist/websocket/index.js +2 -0
  120. package/package.json +12 -16
  121. package/dist/application/binding.d.ts +0 -33
  122. package/dist/application/binding.js +0 -295
  123. package/dist/application/host.d.ts +0 -8
  124. package/dist/application/host.js +0 -14
  125. package/dist/application/queue.d.ts +0 -3
  126. package/dist/application/queue.js +0 -4
  127. package/dist/application/scheduler.d.ts +0 -2
  128. package/dist/application/scheduler.js +0 -45
  129. package/dist/core/context-key.d.ts +0 -14
  130. package/dist/core/context-key.js +0 -14
  131. package/dist/core/contract.d.ts +0 -94
  132. package/dist/core/contract.js +0 -79
  133. package/dist/core/execution.d.ts +0 -78
  134. package/dist/core/execution.js +0 -61
  135. package/dist/core/implementation.d.ts +0 -34
  136. package/dist/core/implementation.js +0 -30
  137. package/dist/core/layer.d.ts +0 -109
  138. package/dist/core/layer.js +0 -52
  139. package/dist/graph/compiler.d.ts +0 -4
  140. package/dist/graph/compiler.js +0 -228
  141. package/dist/graph/graph.d.ts +0 -17
  142. package/dist/graph/graph.js +0 -1103
  143. package/dist/graph/ir.d.ts +0 -175
  144. package/dist/http/application.d.ts +0 -18
  145. package/dist/http/application.js +0 -506
  146. package/dist/http/basic-auth.d.ts +0 -37
  147. package/dist/http/basic-auth.js +0 -67
  148. package/dist/http/definitions.d.ts +0 -225
  149. package/dist/http/definitions.js +0 -69
  150. package/dist/http/hosted-protocol.d.ts +0 -4
  151. package/dist/http/hosted-protocol.js +0 -4
  152. package/dist/http/params.d.ts +0 -2
  153. package/dist/http/params.js +0 -19
  154. package/dist/http/request-body.d.ts +0 -4
  155. package/dist/http/request-body.js +0 -6
  156. package/dist/http/validate.d.ts +0 -20
  157. package/dist/http/validate.js +0 -6
  158. package/dist/message-port/environment.d.ts +0 -2
  159. package/dist/message-port/environment.js +0 -4
  160. package/dist/runtime/application.d.ts +0 -38
  161. package/dist/runtime/application.js +0 -339
  162. package/dist/runtime/pipeline.d.ts +0 -11
  163. package/dist/runtime/pipeline.js +0 -161
  164. /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
+ }
@@ -1,13 +1,12 @@
1
- import { type ContractDefinition, type ProcedureDefinition, type SchemaInput, type SchemaOutput, type StandardSchemaV1 } from '../core/index.js';
2
- import type { HttpHeaders, HttpParamsSchemas, HttpProtocol, HttpProtocolDefinition, HttpRequestBodyDefinition, HttpResponseDefinition } from './definitions.js';
3
- import { type PathParamNames, type RawPathParams } from './path.js';
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 HttpProtocolOf<TProcedure extends ProcedureDefinition> = TProcedure['protocols'] extends {
19
- readonly http: infer TProtocol;
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
- }> : RawPathParams<TDefinition['path']>;
31
- type RequestPartInput<TDefinition extends HttpProtocolDefinition, TPart extends 'query' | 'headers'> = TDefinition['request'] extends infer TRequest ? TRequest extends Record<TPart, infer TSchema extends StandardSchemaV1> ? SchemaInput<TSchema> : never : never;
32
- type RequestBodyInput<TDefinition extends HttpProtocolDefinition> = TDefinition['request'] extends {
33
- readonly body: infer TBody extends HttpRequestBodyDefinition;
34
- } ? SchemaInput<RequestBodySchema<TBody>> : never;
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 HttpProtocolDefinition> = RequestField<'params', PathParamsInput<TDefinition>> & RequestField<'query', RequestPartInput<TDefinition, 'query'>> & RequestField<'headers', RequestPartInput<TDefinition, 'headers'>> & RequestField<'body', RequestBodyInput<TDefinition>>;
41
- type ResponseBodyOutput<TResponse extends HttpResponseDefinition> = 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
- } ? AsyncIterable<SchemaOutput<TResponse['body']>> : SchemaOutput<TResponse['body']>;
44
- type ResponseHeadersOutput<TResponse extends HttpResponseDefinition> = TResponse extends {
45
- readonly headers: infer THeaders extends StandardSchemaV1;
46
- } ? SchemaOutput<THeaders> : HttpHeaders;
47
- export type HttpClientResponse<TDefinition extends HttpProtocolDefinition> = {
48
- [TVariant in keyof TDefinition['responses'] & string]: NonNullable<TDefinition['responses'][TVariant]> extends infer TResponse extends HttpResponseDefinition ? {
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 HttpProtocolDefinition> = keyof HttpClientRequest<TDefinition> extends never ? () => Promise<HttpClientResponse<TDefinition>> : (request: HttpClientRequest<TDefinition>) => Promise<HttpClientResponse<TDefinition>>;
55
- export type HttpClient<TContract extends ContractDefinition> = {
56
- readonly [TProcedure in HttpProcedureNames<TContract>]: HttpClientMethod<HttpDefinitionOf<TContract, TProcedure>>;
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 TContract extends ContractDefinition>(contract: TContract, transport: HttpClientTransport): HttpClient<TContract>;
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;
@@ -1,5 +1,5 @@
1
1
  import { SchemaValidationError, validateSchema, } from '../core/index.js';
2
- import { parseHttpPath, } from './path.js';
2
+ import { parseHttpPath } from './path.js';
3
3
  export class HttpClientResponseError extends Error {
4
4
  status;
5
5
  method;
@@ -16,10 +16,7 @@ export class HttpClientResponseError extends Error {
16
16
  }
17
17
  export function createHttpClient(contract, transport) {
18
18
  const client = {};
19
- for (const [procedureName, procedure] of Object.entries(contract.procedures)) {
20
- const definition = httpDefinitionOf(procedure);
21
- if (!definition)
22
- continue;
19
+ for (const [procedureName, definition] of Object.entries(contract.routes)) {
23
20
  client[procedureName] = async (request = {}) => {
24
21
  const response = await transport({
25
22
  method: definition.method,
@@ -27,24 +24,12 @@ export function createHttpClient(contract, transport) {
27
24
  ...(request.query === undefined ? {} : { query: request.query }),
28
25
  ...(request.headers === undefined ? {} : { headers: request.headers }),
29
26
  ...(request.body === undefined ? {} : { body: request.body }),
30
- ...(definition.request?.body === undefined
31
- ? {}
32
- : { contentType: definition.request.body.contentType }),
33
27
  });
34
28
  return decodeResponse(procedureName, definition, response);
35
29
  };
36
30
  }
37
31
  return Object.freeze(client);
38
32
  }
39
- function httpDefinitionOf(procedure) {
40
- const protocol = procedure.protocols.http;
41
- if (!protocol ||
42
- protocol.protocol !== 'http' ||
43
- !('definition' in protocol)) {
44
- return undefined;
45
- }
46
- return protocol.definition;
47
- }
48
33
  function interpolatePath(path, params) {
49
34
  const segments = parseHttpPath(path);
50
35
  if (segments.length === 0)
@@ -70,11 +55,23 @@ async function decodeResponse(procedure, definition, response) {
70
55
  let lastValidationError;
71
56
  for (const candidate of candidates) {
72
57
  try {
73
- const body = candidate.stream
74
- ? validatedResponseStream(candidate.body, response.body, procedure, definition, response.status)
75
- : await validateSchema(candidate.body, response.body);
76
- const headers = candidate.headers
77
- ? await validateSchema(candidate.headers, response.headers ?? {})
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 ?? {})
78
75
  : (response.headers ?? {});
79
76
  return Object.freeze({ status: response.status, body, headers });
80
77
  }
@@ -86,6 +83,22 @@ async function decodeResponse(procedure, definition, response) {
86
83
  }
87
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);
88
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
+ }
89
102
  function validatedResponseStream(schema, value, procedure, definition, status) {
90
103
  const target = describeHttpTarget(definition, procedure);
91
104
  if (!isAsyncIterable(value)) {
@@ -119,15 +132,18 @@ export function fetchHttpTransport(options) {
119
132
  appendQuery(url.searchParams, request.query);
120
133
  const headers = new Headers(options.headers);
121
134
  appendHeaders(headers, request.headers);
122
- if (request.contentType && !headers.has('content-type')) {
123
- headers.set('content-type', request.contentType);
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');
124
142
  }
125
143
  const response = await fetchImplementation(url, {
126
144
  method: request.method,
127
145
  headers,
128
- ...(request.body === undefined
129
- ? {}
130
- : { body: encodeBody(request.body, request.contentType) }),
146
+ ...(body === undefined ? {} : { body }),
131
147
  });
132
148
  return {
133
149
  status: response.status,
@@ -189,8 +205,12 @@ function encodeBody(body, contentType) {
189
205
  return body;
190
206
  }
191
207
  async function decodeFetchBody(response) {
192
- if (response.status === 204 || response.status === 205)
208
+ if (response.body === null ||
209
+ response.status === 204 ||
210
+ response.status === 205 ||
211
+ response.status === 304) {
193
212
  return undefined;
213
+ }
194
214
  const mediaType = normalizeMediaType(response.headers.get('content-type'));
195
215
  if (mediaType === 'text/event-stream') {
196
216
  if (!response.body) {
@@ -212,16 +232,25 @@ async function* decodeServerSentEvents(stream) {
212
232
  const decoder = new TextDecoder();
213
233
  let buffer = '';
214
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
+ };
215
245
  try {
216
246
  while (true) {
217
247
  const next = await reader.read();
218
248
  if (next.done) {
219
249
  completed = true;
220
- buffer += decoder.decode();
250
+ appendDecoded(decoder.decode(), true);
221
251
  break;
222
252
  }
223
- buffer += decoder.decode(next.value, { stream: true });
224
- buffer = normalizeEventStreamNewlines(buffer);
253
+ appendDecoded(decoder.decode(next.value, { stream: true }), false);
225
254
  let boundary = buffer.indexOf('\n\n');
226
255
  while (boundary >= 0) {
227
256
  const event = buffer.slice(0, boundary);
@@ -236,16 +265,6 @@ async function* decodeServerSentEvents(stream) {
236
265
  boundary = buffer.indexOf('\n\n');
237
266
  }
238
267
  }
239
- buffer = normalizeEventStreamNewlines(buffer).trim();
240
- if (buffer.length > 0) {
241
- const data = buffer
242
- .split('\n')
243
- .filter((line) => line.startsWith('data:'))
244
- .map((line) => line.slice('data:'.length).trimStart())
245
- .join('\n');
246
- if (data.length > 0)
247
- yield JSON.parse(data);
248
- }
249
268
  }
250
269
  finally {
251
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
+ }
@@ -1,5 +1,6 @@
1
- import { type LayerDescriptor, type PipelineItem } from '../core/index.js';
2
- export type CorsOrigin = string | readonly string[] | ((origin: string) => boolean | Promise<boolean>);
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 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>;
13
+ export declare function cors(options?: CorsOptions): import("../core/generic-layer.js").GenericLayer<HttpMiddlewareContext, {}, HttpExecutionResult>;
14
+ export type CorsLayerDescriptor = ReturnType<typeof cors>;