@toa.io/extensions.exposition 0.24.0-alpha.17 → 0.24.0-alpha.19
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/components/context.toa.yaml +12 -0
- package/components/identity.bans/manifest.toa.yaml +1 -1
- package/components/identity.basic/manifest.toa.yaml +1 -1
- package/components/identity.basic/operations/authenticate.js +1 -2
- package/components/identity.basic/operations/authenticate.js.map +1 -1
- package/components/identity.basic/operations/transit.js.map +1 -1
- package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.basic/source/authenticate.ts +0 -1
- package/components/identity.federation/events/principal.js +22 -0
- package/components/identity.federation/manifest.toa.yaml +88 -0
- package/components/identity.federation/operations/assertions-as-values.cjs +45 -0
- package/components/identity.federation/operations/assertions-as-values.cjs.map +1 -0
- package/components/identity.federation/operations/assertions-as-values.d.cts +4 -0
- package/components/identity.federation/operations/authenticate.d.ts +3 -0
- package/components/identity.federation/operations/authenticate.js +20 -0
- package/components/identity.federation/operations/authenticate.js.map +1 -0
- package/components/identity.federation/operations/create.d.ts +10 -0
- package/components/identity.federation/operations/create.js +15 -0
- package/components/identity.federation/operations/create.js.map +1 -0
- package/components/identity.federation/operations/jwt.cjs +112 -0
- package/components/identity.federation/operations/jwt.cjs.map +1 -0
- package/components/identity.federation/operations/jwt.d.cts +19 -0
- package/components/identity.federation/operations/schemas.d.ts +43 -0
- package/components/identity.federation/operations/schemas.js +9 -0
- package/components/identity.federation/operations/schemas.js.map +1 -0
- package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -0
- package/components/identity.federation/operations/types.d.ts +51 -0
- package/components/identity.federation/operations/types.js +3 -0
- package/components/identity.federation/operations/types.js.map +1 -0
- package/components/identity.federation/source/assertions-as-values.cts +20 -0
- package/components/identity.federation/source/authenticate.ts +28 -0
- package/components/identity.federation/source/create.ts +26 -0
- package/components/identity.federation/source/jwt.cts +143 -0
- package/components/identity.federation/source/schemas.ts +45 -0
- package/components/identity.federation/source/types.ts +56 -0
- package/components/identity.federation/tsconfig.json +9 -0
- package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/manifest.toa.yaml +1 -1
- package/components/identity.tokens/operations/authenticate.js.map +1 -1
- package/components/identity.tokens/operations/decrypt.js.map +1 -1
- package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
- package/cucumber.js +0 -1
- package/documentation/components.md +24 -1
- package/documentation/identity.md +7 -7
- package/documentation/protocol.md +20 -1
- package/documentation/query.md +1 -1
- package/documentation/vary.md +69 -0
- package/features/cors.feature +6 -26
- package/features/identity.feature +17 -1
- package/features/identity.federation.feature +125 -0
- package/features/response.feature +0 -0
- package/features/steps/Captures.ts +5 -0
- package/features/steps/Components.ts +5 -0
- package/features/steps/HTTP.ts +33 -5
- package/features/steps/IdP.ts +120 -0
- package/features/steps/Parameters.ts +8 -2
- package/features/steps/Workspace.ts +5 -7
- package/features/vary.feature +120 -0
- package/package.json +17 -18
- package/source/Directive.test.ts +8 -2
- package/source/Directive.ts +19 -16
- package/source/Factory.ts +8 -7
- package/source/Gateway.ts +22 -8
- package/source/HTTP/Server.fixtures.ts +0 -1
- package/source/HTTP/Server.test.ts +61 -138
- package/source/HTTP/Server.ts +45 -31
- package/source/HTTP/formats/text.ts +1 -1
- package/source/HTTP/formats/yaml.ts +1 -1
- package/source/HTTP/messages.ts +8 -2
- package/source/Interception.ts +24 -0
- package/source/RTD/Directives.ts +2 -2
- package/source/RTD/syntax/parse.ts +6 -6
- package/source/RTD/syntax/types.ts +1 -1
- package/source/directives/auth/{Family.ts → Authorization.ts} +29 -33
- package/source/directives/auth/Incept.ts +1 -1
- package/source/directives/auth/Rule.ts +2 -2
- package/source/directives/auth/index.ts +2 -2
- package/source/directives/auth/schemes.ts +2 -1
- package/source/directives/auth/types.ts +9 -6
- package/source/directives/cache/{Family.ts → Cache.ts} +4 -5
- package/source/directives/cache/index.ts +2 -2
- package/source/directives/cache/types.ts +1 -1
- package/source/directives/cors/CORS.ts +52 -0
- package/source/directives/cors/index.ts +3 -0
- package/source/directives/dev/{Family.ts → Development.ts} +3 -4
- package/source/directives/dev/Stub.ts +4 -4
- package/source/directives/dev/Throw.ts +4 -4
- package/source/directives/dev/index.ts +2 -2
- package/source/directives/dev/types.ts +1 -1
- package/source/directives/index.ts +10 -6
- package/source/directives/octets/Context.ts +1 -1
- package/source/directives/octets/Delete.ts +1 -2
- package/source/directives/octets/Fetch.ts +1 -1
- package/source/directives/octets/List.ts +1 -1
- package/source/directives/octets/{Family.ts → Octets.ts} +3 -4
- package/source/directives/octets/Permute.ts +1 -1
- package/source/directives/octets/Store.ts +3 -3
- package/source/directives/octets/index.ts +2 -2
- package/source/directives/octets/types.ts +3 -3
- package/source/directives/vary/Directive.ts +6 -0
- package/source/directives/vary/Embed.ts +62 -0
- package/source/directives/vary/Properties.ts +17 -0
- package/source/directives/vary/Vary.ts +48 -0
- package/source/directives/vary/embeddings/Embedding.ts +6 -0
- package/source/directives/vary/embeddings/Header.ts +30 -0
- package/source/directives/vary/embeddings/Language.ts +31 -0
- package/source/directives/vary/embeddings/index.ts +11 -0
- package/source/directives/vary/index.ts +3 -0
- package/source/io.ts +4 -0
- package/transpiled/Composition.js.map +1 -1
- package/transpiled/Directive.d.ts +6 -7
- package/transpiled/Directive.js +12 -10
- package/transpiled/Directive.js.map +1 -1
- package/transpiled/Factory.d.ts +0 -1
- package/transpiled/Factory.js +7 -6
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Gateway.d.ts +8 -5
- package/transpiled/Gateway.js +12 -2
- package/transpiled/Gateway.js.map +1 -1
- package/transpiled/HTTP/Server.d.ts +4 -2
- package/transpiled/HTTP/Server.fixtures.d.ts +0 -1
- package/transpiled/HTTP/Server.fixtures.js +1 -2
- package/transpiled/HTTP/Server.fixtures.js.map +1 -1
- package/transpiled/HTTP/Server.js +33 -22
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/HTTP/formats/text.d.ts +3 -1
- package/transpiled/HTTP/formats/text.js.map +1 -1
- package/transpiled/HTTP/formats/yaml.js +1 -1
- package/transpiled/HTTP/formats/yaml.js.map +1 -1
- package/transpiled/HTTP/messages.d.ts +4 -0
- package/transpiled/HTTP/messages.js +4 -2
- package/transpiled/HTTP/messages.js.map +1 -1
- package/transpiled/Interception.d.ts +9 -0
- package/transpiled/Interception.js +19 -0
- package/transpiled/Interception.js.map +1 -0
- package/transpiled/Query.js.map +1 -1
- package/transpiled/RTD/Directives.d.ts +2 -2
- package/transpiled/RTD/Node.js.map +1 -1
- package/transpiled/RTD/Route.js.map +1 -1
- package/transpiled/RTD/syntax/parse.js +1 -1
- package/transpiled/RTD/syntax/parse.js.map +1 -1
- package/transpiled/RTD/syntax/types.js +1 -1
- package/transpiled/RTD/syntax/types.js.map +1 -1
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/directives/auth/{Family.d.ts → Authorization.d.ts} +4 -4
- package/transpiled/directives/auth/{Family.js → Authorization.js} +15 -8
- package/transpiled/directives/auth/Authorization.js.map +1 -0
- package/transpiled/directives/auth/Incept.js.map +1 -1
- package/transpiled/directives/auth/Role.js.map +1 -1
- package/transpiled/directives/auth/Rule.d.ts +2 -2
- package/transpiled/directives/auth/Rule.js.map +1 -1
- package/transpiled/directives/auth/index.d.ts +2 -2
- package/transpiled/directives/auth/index.js +4 -5
- package/transpiled/directives/auth/index.js.map +1 -1
- package/transpiled/directives/auth/schemes.js +2 -1
- package/transpiled/directives/auth/schemes.js.map +1 -1
- package/transpiled/directives/auth/types.d.ts +4 -4
- package/transpiled/directives/cache/{Family.d.ts → Cache.d.ts} +4 -5
- package/transpiled/directives/cache/{Family.js → Cache.js} +4 -2
- package/transpiled/directives/cache/{Family.js.map → Cache.js.map} +1 -1
- package/transpiled/directives/cache/index.d.ts +2 -2
- package/transpiled/directives/cache/index.js +4 -5
- package/transpiled/directives/cache/index.js.map +1 -1
- package/transpiled/directives/cache/types.d.ts +1 -1
- package/transpiled/directives/cors/CORS.d.ts +14 -0
- package/transpiled/directives/cors/CORS.js +42 -0
- package/transpiled/directives/cors/CORS.js.map +1 -0
- package/transpiled/directives/cors/index.d.ts +2 -0
- package/transpiled/directives/cors/index.js +6 -0
- package/transpiled/directives/cors/index.js.map +1 -0
- package/transpiled/directives/dev/{Family.d.ts → Development.d.ts} +3 -4
- package/transpiled/directives/dev/{Family.js → Development.js} +4 -2
- package/transpiled/directives/dev/Development.js.map +1 -0
- package/transpiled/directives/dev/Stub.d.ts +3 -3
- package/transpiled/directives/dev/Stub.js.map +1 -1
- package/transpiled/directives/dev/Throw.d.ts +3 -3
- package/transpiled/directives/dev/Throw.js.map +1 -1
- package/transpiled/directives/dev/index.d.ts +2 -2
- package/transpiled/directives/dev/index.js +4 -5
- package/transpiled/directives/dev/index.js.map +1 -1
- package/transpiled/directives/dev/types.d.ts +1 -1
- package/transpiled/directives/index.d.ts +3 -1
- package/transpiled/directives/index.js +9 -9
- package/transpiled/directives/index.js.map +1 -1
- package/transpiled/directives/octets/Context.d.ts +1 -1
- package/transpiled/directives/octets/Delete.d.ts +1 -1
- package/transpiled/directives/octets/Delete.js.map +1 -1
- package/transpiled/directives/octets/Fetch.d.ts +1 -1
- package/transpiled/directives/octets/List.d.ts +1 -1
- package/transpiled/directives/octets/{Family.d.ts → Octets.d.ts} +3 -4
- package/transpiled/directives/octets/{Family.js → Octets.js} +4 -2
- package/transpiled/directives/octets/Octets.js.map +1 -0
- package/transpiled/directives/octets/Permute.d.ts +1 -1
- package/transpiled/directives/octets/Store.d.ts +1 -1
- package/transpiled/directives/octets/Store.js.map +1 -1
- package/transpiled/directives/octets/index.d.ts +2 -2
- package/transpiled/directives/octets/index.js +4 -5
- package/transpiled/directives/octets/index.js.map +1 -1
- package/transpiled/directives/octets/types.d.ts +3 -3
- package/transpiled/directives/vary/Directive.d.ts +5 -0
- package/transpiled/directives/vary/Directive.js +3 -0
- package/transpiled/directives/vary/Directive.js.map +1 -0
- package/transpiled/directives/vary/Embed.d.ts +10 -0
- package/transpiled/directives/vary/Embed.js +49 -0
- package/transpiled/directives/vary/Embed.js.map +1 -0
- package/transpiled/directives/vary/Properties.d.ts +9 -0
- package/transpiled/directives/vary/Properties.js +16 -0
- package/transpiled/directives/vary/Properties.js.map +1 -0
- package/transpiled/directives/vary/Vary.d.ts +10 -0
- package/transpiled/directives/vary/Vary.js +36 -0
- package/transpiled/directives/vary/Vary.js.map +1 -0
- package/transpiled/directives/vary/embeddings/Embedding.d.ts +5 -0
- package/transpiled/directives/vary/embeddings/Embedding.js +3 -0
- package/transpiled/directives/vary/embeddings/Embedding.js.map +1 -0
- package/transpiled/directives/vary/embeddings/Header.d.ts +7 -0
- package/transpiled/directives/vary/embeddings/Header.js +26 -0
- package/transpiled/directives/vary/embeddings/Header.js.map +1 -0
- package/transpiled/directives/vary/embeddings/Language.d.ts +7 -0
- package/transpiled/directives/vary/embeddings/Language.js +28 -0
- package/transpiled/directives/vary/embeddings/Language.js.map +1 -0
- package/transpiled/directives/vary/embeddings/index.d.ts +5 -0
- package/transpiled/directives/vary/embeddings/index.js +10 -0
- package/transpiled/directives/vary/embeddings/index.js.map +1 -0
- package/transpiled/directives/vary/index.d.ts +2 -0
- package/transpiled/directives/vary/index.js +6 -0
- package/transpiled/directives/vary/index.js.map +1 -0
- package/transpiled/io.d.ts +3 -0
- package/transpiled/io.js +3 -0
- package/transpiled/io.js.map +1 -0
- package/transpiled/manifest.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
- package/transpiled/directives/auth/Family.js.map +0 -1
- package/transpiled/directives/dev/Family.js.map +0 -1
- package/transpiled/directives/octets/Family.js.map +0 -1
package/source/Directive.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { IncomingMessage, OutgoingMessage } from './HTTP'
|
|
2
|
+
import type { Remotes } from './Remotes'
|
|
3
|
+
import type { Output } from './io'
|
|
3
4
|
import type * as RTD from './RTD'
|
|
4
5
|
|
|
5
6
|
export class Directives implements RTD.Directives<Directives> {
|
|
6
|
-
private readonly
|
|
7
|
+
private readonly sets: DirectiveSet[]
|
|
7
8
|
|
|
8
|
-
public constructor (
|
|
9
|
-
this.
|
|
9
|
+
public constructor (sets: DirectiveSet[]) {
|
|
10
|
+
this.sets = sets
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
public async preflight (request: IncomingMessage, parameters: RTD.Parameter[]): Promise<Output> {
|
|
13
|
-
for (const
|
|
14
|
-
|
|
14
|
+
for (const set of this.sets) {
|
|
15
|
+
if (set.family.preflight === undefined)
|
|
16
|
+
continue
|
|
17
|
+
|
|
18
|
+
const output = await set.family.preflight(set.directives, request, parameters)
|
|
15
19
|
|
|
16
20
|
if (output !== null) {
|
|
17
21
|
await this.settle(request, output)
|
|
@@ -24,13 +28,13 @@ export class Directives implements RTD.Directives<Directives> {
|
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
public async settle (request: IncomingMessage, response: OutgoingMessage): Promise<void> {
|
|
27
|
-
for (const
|
|
28
|
-
if (
|
|
29
|
-
await
|
|
31
|
+
for (const set of this.sets)
|
|
32
|
+
if (set.family.settle !== undefined)
|
|
33
|
+
await set.family.settle(set.directives, request, response)
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
public merge (directives: Directives): void {
|
|
33
|
-
this.
|
|
37
|
+
this.sets.push(...directives.sets)
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -61,7 +65,7 @@ export class DirectivesFactory implements RTD.DirectivesFactory<Directives> {
|
|
|
61
65
|
const family = this.families[declaration.family]
|
|
62
66
|
|
|
63
67
|
if (family === undefined)
|
|
64
|
-
throw new Error(`Directive family '${declaration.family}' not found.`)
|
|
68
|
+
throw new Error(`Directive family '${declaration.family}' is not found.`)
|
|
65
69
|
|
|
66
70
|
const directive = family.create(declaration.name, declaration.value, this.remtoes)
|
|
67
71
|
|
|
@@ -100,9 +104,11 @@ export interface Family<TDirective = any, TExtension = any> {
|
|
|
100
104
|
readonly name: string
|
|
101
105
|
readonly mandatory: boolean
|
|
102
106
|
|
|
107
|
+
// produce: (declarations: RTD.syntax.Directive[], remotes: Remotes) => TDirective[]
|
|
108
|
+
|
|
103
109
|
create: (name: string, value: any, remotes: Remotes) => TDirective
|
|
104
110
|
|
|
105
|
-
preflight
|
|
111
|
+
preflight?: (directives: TDirective[],
|
|
106
112
|
request: IncomingMessage & TExtension,
|
|
107
113
|
parameters: RTD.Parameter[]) => Output | Promise<Output>
|
|
108
114
|
|
|
@@ -115,6 +121,3 @@ interface DirectiveSet {
|
|
|
115
121
|
family: Family
|
|
116
122
|
directives: any[]
|
|
117
123
|
}
|
|
118
|
-
|
|
119
|
-
export type Input = IncomingMessage
|
|
120
|
-
export type Output = OutgoingMessage | null
|
package/source/Factory.ts
CHANGED
|
@@ -4,19 +4,18 @@ import { Remotes } from './Remotes'
|
|
|
4
4
|
import { Tree, syntax } from './RTD'
|
|
5
5
|
import { Server } from './HTTP'
|
|
6
6
|
import { type Endpoint, EndpointsFactory } from './Endpoint'
|
|
7
|
-
import
|
|
8
|
-
import { type Directives, DirectivesFactory
|
|
7
|
+
import { families, interceptors } from './directives'
|
|
8
|
+
import { type Directives, DirectivesFactory } from './Directive'
|
|
9
9
|
import { Composition } from './Composition'
|
|
10
10
|
import * as root from './root'
|
|
11
|
+
import { Interception } from './Interception'
|
|
11
12
|
import type { Connector, Locator, extensions } from '@toa.io/core'
|
|
12
13
|
|
|
13
14
|
export class Factory implements extensions.Factory {
|
|
14
15
|
private readonly boot: Bootloader
|
|
15
|
-
private readonly families: Family[]
|
|
16
16
|
|
|
17
17
|
public constructor (boot: Bootloader) {
|
|
18
18
|
this.boot = boot
|
|
19
|
-
this.families = directives.families
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
public tenant (locator: Locator, node: syntax.Node): Connector {
|
|
@@ -32,16 +31,18 @@ export class Factory implements extensions.Factory {
|
|
|
32
31
|
const remotes = new Remotes(this.boot)
|
|
33
32
|
const node = root.resolve()
|
|
34
33
|
const methods = new EndpointsFactory(remotes)
|
|
35
|
-
const directives = new DirectivesFactory(
|
|
34
|
+
const directives = new DirectivesFactory(families, remotes)
|
|
35
|
+
const interception = new Interception(interceptors)
|
|
36
36
|
const tree = new Tree<Endpoint, Directives>(node, methods, directives)
|
|
37
37
|
|
|
38
38
|
const composition = new Composition(this.boot)
|
|
39
|
-
const gateway = new Gateway(broadcast, server, tree)
|
|
39
|
+
const gateway = new Gateway(broadcast, server, tree, interception)
|
|
40
40
|
|
|
41
41
|
gateway.depends(remotes)
|
|
42
42
|
gateway.depends(composition)
|
|
43
|
+
server.depends(gateway)
|
|
43
44
|
|
|
44
|
-
return
|
|
45
|
+
return server
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
package/source/Gateway.ts
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
import { type bindings, Connector } from '@toa.io/core'
|
|
2
|
-
import { type Maybe } from '@toa.io/types'
|
|
3
2
|
import * as http from './HTTP'
|
|
4
3
|
import { rethrow } from './exceptions'
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
4
|
+
import type { Interceptor } from './Interception'
|
|
5
|
+
import type { Maybe } from '@toa.io/types'
|
|
6
|
+
import type { Method, Parameter, Tree } from './RTD'
|
|
7
|
+
import type { Label } from './discovery'
|
|
8
|
+
import type { Branch } from './Branch'
|
|
9
|
+
import type { Endpoint } from './Endpoint'
|
|
10
|
+
import type { Directives } from './Directive'
|
|
10
11
|
|
|
11
12
|
export class Gateway extends Connector {
|
|
12
13
|
private readonly broadcast: Broadcast
|
|
13
14
|
private readonly tree: Tree<Endpoint, Directives>
|
|
15
|
+
private readonly interceptor: Interceptor
|
|
16
|
+
private readonly server: Connector
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
// eslint-disable-next-line max-params, max-len
|
|
19
|
+
public constructor (broadcast: Broadcast, server: http.Server, tree: Tree<Endpoint, Directives>, interception: Interceptor) {
|
|
16
20
|
super()
|
|
17
21
|
|
|
18
22
|
this.broadcast = broadcast
|
|
19
23
|
this.tree = tree
|
|
24
|
+
this.interceptor = interception
|
|
25
|
+
this.server = server
|
|
20
26
|
|
|
21
27
|
this.depends(broadcast)
|
|
22
|
-
this.depends(server)
|
|
28
|
+
// this.depends(server)
|
|
23
29
|
|
|
24
30
|
server.attach(this.process.bind(this))
|
|
25
31
|
}
|
|
@@ -35,6 +41,11 @@ export class Gateway extends Connector {
|
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
private async process (request: http.IncomingMessage): Promise<http.OutgoingMessage> {
|
|
44
|
+
const interception = await this.interceptor.intercept(request)
|
|
45
|
+
|
|
46
|
+
if (interception !== null)
|
|
47
|
+
return interception
|
|
48
|
+
|
|
38
49
|
const match = this.tree.match(request.path)
|
|
39
50
|
|
|
40
51
|
if (match === null)
|
|
@@ -68,6 +79,9 @@ export class Gateway extends Connector {
|
|
|
68
79
|
|
|
69
80
|
const body = await request.parse()
|
|
70
81
|
|
|
82
|
+
if ('embed' in request && typeof body === 'object' && body !== null)
|
|
83
|
+
Object.assign(body, request.embed)
|
|
84
|
+
|
|
71
85
|
const reply = await method.endpoint
|
|
72
86
|
.call(body, request.query, parameters)
|
|
73
87
|
.catch(rethrow) as Maybe<unknown>
|
|
@@ -1,203 +1,126 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
1
2
|
import { Connector } from '@toa.io/core'
|
|
2
|
-
import { immediate } from '@toa.io/generic'
|
|
3
|
-
import { generate } from 'randomstring'
|
|
4
3
|
import { type Processing, Server } from './Server'
|
|
5
4
|
import { type OutgoingMessage } from './messages'
|
|
6
|
-
import { express, cors, createRequest, res, next } from './Server.fixtures'
|
|
7
5
|
import { BadRequest } from './exceptions'
|
|
8
|
-
import type { Express, Request, RequestHandler } from 'express'
|
|
9
|
-
import type { CorsOptions } from 'cors'
|
|
10
|
-
import type http from 'node:http'
|
|
11
|
-
|
|
12
|
-
jest.mock('express', () => () => express())
|
|
13
|
-
jest.mock('cors', () => (options: CorsOptions) => cors(options))
|
|
14
6
|
|
|
15
7
|
let server: Server
|
|
16
|
-
let app: jest.MockedObject<Express>
|
|
17
|
-
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
jest.clearAllMocks()
|
|
20
8
|
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
server = Server.create({ port: 0 })
|
|
23
11
|
})
|
|
24
12
|
|
|
25
13
|
it('should instance of connector', async () => {
|
|
26
14
|
expect(server).toBeInstanceOf(Connector)
|
|
27
15
|
})
|
|
28
16
|
|
|
29
|
-
it('should create express app', async () => {
|
|
30
|
-
expect(express).toHaveBeenCalled()
|
|
31
|
-
expect(app.disable).toHaveBeenCalledWith('x-powered-by')
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('should support cors', async () => {
|
|
35
|
-
expect(cors).toHaveBeenCalledWith(expect.objectContaining({
|
|
36
|
-
credentials: true,
|
|
37
|
-
maxAge: 86400,
|
|
38
|
-
allowedHeaders: ['accept', 'authorization', 'content-type']
|
|
39
|
-
} satisfies CorsOptions))
|
|
40
|
-
|
|
41
|
-
const middleware = cors.mock.results[0].value
|
|
42
|
-
|
|
43
|
-
expect(app.use).toHaveBeenCalledWith(middleware)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
17
|
it('should start HTTP server', async () => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
await immediate()
|
|
50
|
-
|
|
51
|
-
expect(app.listen).toHaveBeenCalledWith(8000, expect.anything())
|
|
52
|
-
|
|
53
|
-
const done = app.listen.mock.calls[0][1]
|
|
54
|
-
|
|
55
|
-
if (done !== undefined) done()
|
|
18
|
+
await server.connect()
|
|
56
19
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
it('should stop HTTP server', async () => {
|
|
61
|
-
const started = server.connect()
|
|
62
|
-
|
|
63
|
-
await immediate()
|
|
64
|
-
|
|
65
|
-
app.listen.mock.calls[0][1]?.() // `listen` callback
|
|
66
|
-
|
|
67
|
-
await started
|
|
68
|
-
|
|
69
|
-
const stopped = server.disconnect()
|
|
70
|
-
const httpServer: jest.MockedObject<http.Server> = app.listen.mock.results[0].value
|
|
71
|
-
|
|
72
|
-
await immediate()
|
|
73
|
-
|
|
74
|
-
expect(httpServer.close).toHaveBeenCalled()
|
|
75
|
-
|
|
76
|
-
httpServer.close.mock.calls[0][0]?.() // `close` callback
|
|
77
|
-
|
|
78
|
-
await stopped
|
|
20
|
+
expect(server.connected).toBeTruthy()
|
|
21
|
+
expect(server.port).toBeGreaterThan(0)
|
|
79
22
|
})
|
|
80
23
|
|
|
81
24
|
it('should register request handler', async () => {
|
|
82
|
-
const process = jest.fn(
|
|
83
|
-
const req = createRequest()
|
|
25
|
+
const process: Processing = jest.fn().mockResolvedValue(undefined)
|
|
84
26
|
|
|
85
27
|
server.attach(process)
|
|
86
28
|
|
|
87
|
-
await
|
|
29
|
+
const res = await fetch(`http://localhost:${server.port}`)
|
|
30
|
+
|
|
31
|
+
await res.text()
|
|
88
32
|
|
|
89
|
-
expect(process).
|
|
33
|
+
expect(process).toHaveBeenCalledTimes(1)
|
|
90
34
|
})
|
|
91
35
|
|
|
92
36
|
it('should send 501 on unknown method', async () => {
|
|
93
|
-
const
|
|
37
|
+
const head = await fetch(`http://localhost:${server.port}/`, { method: 'COPY' })
|
|
94
38
|
|
|
95
|
-
await
|
|
39
|
+
await head.text()
|
|
40
|
+
expect(head.status).toBe(501)
|
|
41
|
+
})
|
|
96
42
|
|
|
97
|
-
|
|
43
|
+
it('should stop HTTP server', async () => {
|
|
44
|
+
await server.disconnect()
|
|
45
|
+
expect(server.port).toBe(0)
|
|
46
|
+
expect(server.connected).toBeFalsy()
|
|
98
47
|
})
|
|
99
48
|
|
|
100
49
|
describe('result', () => {
|
|
101
|
-
|
|
102
|
-
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
server = Server.create({ port: 0 })
|
|
52
|
+
await server.connect()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
afterEach(async () => {
|
|
56
|
+
await server.disconnect()
|
|
57
|
+
})
|
|
103
58
|
|
|
59
|
+
it('should send status code 200 if the result has a value', async () => {
|
|
104
60
|
server.attach(async (): Promise<OutgoingMessage> => ({
|
|
105
|
-
headers: new Headers(), body:
|
|
61
|
+
headers: new Headers(), body: randomUUID()
|
|
106
62
|
}))
|
|
107
|
-
await use(req)
|
|
108
63
|
|
|
109
|
-
|
|
64
|
+
const res = await fetch(`http://localhost:${server.port}/`)
|
|
65
|
+
|
|
66
|
+
await res.text()
|
|
67
|
+
expect(res.status).toBe(200)
|
|
110
68
|
})
|
|
111
69
|
|
|
112
70
|
it('should send status code 204 if the result has no value', async () => {
|
|
113
|
-
const req = createRequest()
|
|
114
|
-
|
|
115
71
|
server.attach(async (): Promise<OutgoingMessage> => ({ headers: new Headers() }))
|
|
116
|
-
await use(req)
|
|
117
72
|
|
|
118
|
-
|
|
73
|
+
const res = await fetch(`http://localhost:${server.port}/`)
|
|
74
|
+
|
|
75
|
+
await res.text()
|
|
76
|
+
expect(res.status).toBe(204)
|
|
119
77
|
})
|
|
120
78
|
|
|
121
79
|
it('should send result', async () => {
|
|
122
|
-
const body = { [
|
|
123
|
-
const json = JSON.stringify(body)
|
|
124
|
-
const buf = Buffer.from(json)
|
|
125
|
-
const req = createRequest({ headers: { accept: 'application/json' } })
|
|
80
|
+
const body = { [randomUUID()]: randomUUID() }
|
|
126
81
|
|
|
127
|
-
server.attach(async (): Promise<OutgoingMessage> =>
|
|
128
|
-
|
|
82
|
+
server.attach(async (): Promise<OutgoingMessage> =>
|
|
83
|
+
({ headers: new Headers(), body }))
|
|
129
84
|
|
|
130
|
-
|
|
131
|
-
|
|
85
|
+
const res = await fetch(`http://localhost:${server.port}/`,
|
|
86
|
+
{ headers: { accept: 'application/json' } })
|
|
132
87
|
|
|
133
|
-
|
|
134
|
-
async function process (): Promise<OutgoingMessage> {
|
|
135
|
-
throw new Error('Bad')
|
|
136
|
-
}
|
|
88
|
+
const result = await res.json()
|
|
137
89
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
server.attach(process)
|
|
141
|
-
await use(req)
|
|
142
|
-
|
|
143
|
-
expect(res.status).toHaveBeenCalledWith(500)
|
|
90
|
+
expect(result).toEqual(body)
|
|
144
91
|
})
|
|
145
92
|
|
|
146
|
-
it('should
|
|
147
|
-
jest.
|
|
148
|
-
|
|
149
|
-
server = Server.create({ debug: true })
|
|
150
|
-
app = express.mock.results[0]?.value
|
|
151
|
-
|
|
152
|
-
const message = generate()
|
|
153
|
-
const req = createRequest()
|
|
154
|
-
|
|
155
|
-
async function process (): Promise<OutgoingMessage> {
|
|
156
|
-
throw new Error(message)
|
|
157
|
-
}
|
|
93
|
+
it('should return 500 on exception', async () => {
|
|
94
|
+
server.attach(jest.fn().mockRejectedValue(new Error('Bad')))
|
|
158
95
|
|
|
159
|
-
server.
|
|
160
|
-
await use(req)
|
|
96
|
+
const res = await fetch(`http://localhost:${server.port}/`)
|
|
161
97
|
|
|
162
|
-
|
|
98
|
+
await res.text()
|
|
99
|
+
expect(res.status).toBe(500)
|
|
163
100
|
})
|
|
164
101
|
|
|
165
102
|
it('should send client error', async () => {
|
|
166
|
-
const
|
|
167
|
-
const message = generate()
|
|
103
|
+
const message = randomUUID()
|
|
168
104
|
|
|
169
|
-
|
|
170
|
-
throw new BadRequest(message)
|
|
171
|
-
}
|
|
105
|
+
server.attach(jest.fn().mockRejectedValueOnce(new BadRequest(message)))
|
|
172
106
|
|
|
173
|
-
server.
|
|
174
|
-
await
|
|
107
|
+
const res = await fetch(`http://localhost:${server.port}/`)
|
|
108
|
+
const text = await res.text()
|
|
175
109
|
|
|
176
|
-
expect(res.status).
|
|
110
|
+
expect(res.status).toBe(400)
|
|
111
|
+
expect(text).toContain(message)
|
|
177
112
|
})
|
|
178
113
|
})
|
|
179
114
|
|
|
180
115
|
describe('options', () => {
|
|
181
116
|
it('should send 501 on unspecified method', async () => {
|
|
182
|
-
|
|
117
|
+
server = Server.create({ methods: new Set(['COPY']), port: 0 })
|
|
118
|
+
await server.connect()
|
|
183
119
|
|
|
184
|
-
|
|
185
|
-
app = express.mock.results[0]?.value
|
|
120
|
+
const res = await fetch(`http://localhost:${server.port}/`)
|
|
186
121
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
expect(res.sendStatus).toHaveBeenCalledWith(501)
|
|
122
|
+
await res.text()
|
|
123
|
+
await server.disconnect()
|
|
124
|
+
expect(res.status).toBe(501)
|
|
192
125
|
})
|
|
193
126
|
})
|
|
194
|
-
|
|
195
|
-
async function use (req: Request): Promise<void> {
|
|
196
|
-
for (const call of app.use.mock.calls) {
|
|
197
|
-
const usage = call[0] as unknown as RequestHandler
|
|
198
|
-
|
|
199
|
-
usage(req, res, next)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
await immediate()
|
|
203
|
-
}
|
package/source/HTTP/Server.ts
CHANGED
|
@@ -1,57 +1,64 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import express from 'express'
|
|
4
|
-
import cors from 'cors'
|
|
5
4
|
import { Connector } from '@toa.io/core'
|
|
6
5
|
import { promex } from '@toa.io/generic'
|
|
7
6
|
import Negotiator from 'negotiator'
|
|
8
7
|
import { read, write, type IncomingMessage, type OutgoingMessage } from './messages'
|
|
9
8
|
import { ClientError, Exception } from './exceptions'
|
|
10
9
|
import { formats, types } from './formats'
|
|
11
|
-
import type { CorsOptions } from 'cors'
|
|
12
10
|
import type { Format } from './formats'
|
|
13
11
|
import type * as http from 'node:http'
|
|
14
12
|
import type { Express, Request, Response, NextFunction } from 'express'
|
|
15
13
|
|
|
16
14
|
export class Server extends Connector {
|
|
17
|
-
private readonly debug: boolean
|
|
18
|
-
private readonly app: Express
|
|
19
15
|
private server?: http.Server
|
|
20
16
|
|
|
21
|
-
private constructor (app: Express,
|
|
17
|
+
private constructor (private readonly app: Express,
|
|
18
|
+
private readonly debug: boolean,
|
|
19
|
+
private readonly requestedPort: number) {
|
|
22
20
|
super()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public get port (): number {
|
|
24
|
+
if (this.server === undefined) return this.requestedPort
|
|
25
|
+
|
|
26
|
+
const address = this.server.address()
|
|
27
|
+
|
|
28
|
+
if (address === null || typeof address === 'string')
|
|
29
|
+
throw new Error('Server is not listening on a port.')
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
this.debug = debug
|
|
31
|
+
return address.port
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
public static create (options?: Partial<Properties>): Server {
|
|
29
|
-
const properties
|
|
35
|
+
const properties = options === undefined
|
|
30
36
|
? DEFAULTS
|
|
31
37
|
: { ...DEFAULTS, ...options }
|
|
32
38
|
|
|
33
39
|
const app = express()
|
|
34
40
|
|
|
35
41
|
app.disable('x-powered-by')
|
|
36
|
-
app.use(cors(CORS))
|
|
42
|
+
// app.use(cors(CORS))
|
|
37
43
|
app.use(supportedMethods(properties.methods))
|
|
38
44
|
|
|
39
|
-
return new Server(app, properties.debug)
|
|
45
|
+
return new Server(app, properties.debug, properties.port)
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
public attach (process: Processing): void {
|
|
43
|
-
this.app.use((request:
|
|
44
|
-
this.extend(request)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.
|
|
49
|
+
this.app.use((request: Request, response: Response) => {
|
|
50
|
+
const message = this.extend(request)
|
|
51
|
+
|
|
52
|
+
process(message)
|
|
53
|
+
.then(this.success(message, response))
|
|
54
|
+
.catch(this.fail(message, response))
|
|
48
55
|
})
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
protected override async open (): Promise<void> {
|
|
52
59
|
const listening = promex()
|
|
53
60
|
|
|
54
|
-
this.server = this.app.listen(
|
|
61
|
+
this.server = this.app.listen(this.requestedPort, listening.callback)
|
|
55
62
|
|
|
56
63
|
await listening
|
|
57
64
|
|
|
@@ -64,23 +71,35 @@ export class Server extends Connector {
|
|
|
64
71
|
this.server?.close(stopped.callback)
|
|
65
72
|
|
|
66
73
|
await stopped
|
|
67
|
-
}
|
|
68
74
|
|
|
69
|
-
|
|
75
|
+
this.server = undefined
|
|
76
|
+
|
|
70
77
|
console.info('HTTP Server has been stopped.')
|
|
71
78
|
}
|
|
72
79
|
|
|
73
|
-
private
|
|
74
|
-
const message = request as
|
|
80
|
+
private extend (request: Request): IncomingMessage {
|
|
81
|
+
const message = request as IncomingMessage
|
|
75
82
|
|
|
76
83
|
message.encoder = negotiate(request)
|
|
77
|
-
message.
|
|
84
|
+
message.pipelines = { body: [], response: [] }
|
|
85
|
+
|
|
86
|
+
message.parse = async <T> (): Promise<T> => {
|
|
87
|
+
const value = await read(request)
|
|
88
|
+
|
|
89
|
+
if (message.pipelines.body.length === 0)
|
|
90
|
+
return value
|
|
91
|
+
|
|
92
|
+
return message.pipelines.body.reduce((value, transform) => transform(value), value)
|
|
93
|
+
}
|
|
78
94
|
|
|
79
95
|
return message
|
|
80
96
|
}
|
|
81
97
|
|
|
82
98
|
private success (request: IncomingMessage, response: Response) {
|
|
83
99
|
return (message: OutgoingMessage) => {
|
|
100
|
+
for (const transform of request.pipelines.response)
|
|
101
|
+
transform(message)
|
|
102
|
+
|
|
84
103
|
let status = message.status
|
|
85
104
|
|
|
86
105
|
if (status === undefined)
|
|
@@ -150,21 +169,16 @@ async function adam (request: Request): Promise<void> {
|
|
|
150
169
|
return await completed
|
|
151
170
|
}
|
|
152
171
|
|
|
153
|
-
const DEFAULTS = {
|
|
154
|
-
methods: new Set<string>(['GET', '
|
|
155
|
-
debug: false
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const CORS: CorsOptions = {
|
|
159
|
-
credentials: true,
|
|
160
|
-
maxAge: 86400,
|
|
161
|
-
allowedHeaders: ['accept', 'authorization', 'content-type'],
|
|
162
|
-
origin: (origin: string | undefined, callback) => callback(null, origin)
|
|
172
|
+
const DEFAULTS: Properties = {
|
|
173
|
+
methods: new Set<string>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']),
|
|
174
|
+
debug: false,
|
|
175
|
+
port: 8000
|
|
163
176
|
}
|
|
164
177
|
|
|
165
178
|
interface Properties {
|
|
166
179
|
methods: Set<string>
|
|
167
180
|
debug: boolean
|
|
181
|
+
port: number
|
|
168
182
|
}
|
|
169
183
|
|
|
170
184
|
export type Processing = (input: IncomingMessage) => Promise<OutgoingMessage>
|
package/source/HTTP/messages.ts
CHANGED
|
@@ -47,8 +47,10 @@ function send (message: OutgoingMessage, request: IncomingMessage, response: Res
|
|
|
47
47
|
|
|
48
48
|
const buf = request.encoder.encode(message.body)
|
|
49
49
|
|
|
50
|
-
response
|
|
51
|
-
|
|
50
|
+
response
|
|
51
|
+
.set('content-type', request.encoder.type)
|
|
52
|
+
.append('vary', 'accept')
|
|
53
|
+
.end(buf)
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
function stream
|
|
@@ -97,6 +99,10 @@ export interface IncomingMessage extends Request {
|
|
|
97
99
|
query: Query
|
|
98
100
|
parse: <T> () => Promise<T>
|
|
99
101
|
encoder: Format | null
|
|
102
|
+
pipelines: {
|
|
103
|
+
body: Array<(input: unknown) => unknown>
|
|
104
|
+
response: Array<(output: OutgoingMessage) => void>
|
|
105
|
+
}
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
export interface OutgoingMessage {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Input, Output } from './io'
|
|
2
|
+
|
|
3
|
+
export class Interception implements Interceptor {
|
|
4
|
+
private readonly interceptors: Interceptor[]
|
|
5
|
+
|
|
6
|
+
public constructor (interceptors: Interceptor[]) {
|
|
7
|
+
this.interceptors = interceptors
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public async intercept (input: Input): Promise<Output> {
|
|
11
|
+
for (const interceptor of this.interceptors) {
|
|
12
|
+
const output = await interceptor.intercept(input)
|
|
13
|
+
|
|
14
|
+
if (output !== null)
|
|
15
|
+
return output
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Interceptor {
|
|
23
|
+
intercept: (input: Input) => Output | Promise<Output>
|
|
24
|
+
}
|
package/source/RTD/Directives.ts
CHANGED