@toa.io/extensions.exposition 1.0.0-alpha.152 → 1.0.0-alpha.154
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/identity.bans/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.keys/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.otp/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
- package/documentation/io.md +42 -2
- package/documentation/notes/desync.jpg +0 -0
- package/documentation/notes/peers.md +59 -0
- package/documentation/notes/throttling.md +82 -0
- package/documentation/query.md +22 -0
- package/features/io.throttle.feature +40 -0
- package/features/query.feature +46 -0
- package/features/steps/components/pots/manifest.toa.yaml +3 -0
- package/package.json +4 -4
- package/schemas/io/throttle.cos.yaml +36 -0
- package/schemas/query.cos.yaml +1 -0
- package/schemas/querystring.cos.yaml +1 -0
- package/source/Directive.test.ts +4 -2
- package/source/Directive.ts +16 -1
- package/source/Gateway.ts +2 -0
- package/source/HTTP/Server.ts +47 -21
- package/source/HTTP/exceptions.ts +21 -15
- package/source/HTTP/messages.ts +1 -0
- package/source/Query.ts +5 -0
- package/source/RTD/Directives.ts +4 -0
- package/source/RTD/Tree.ts +4 -0
- package/source/RTD/syntax/types.ts +1 -0
- package/source/directives/io/Directive.ts +5 -2
- package/source/directives/io/IO.ts +17 -5
- package/source/directives/io/Input.ts +1 -1
- package/source/directives/io/Output.ts +1 -1
- package/source/directives/io/Throttle.ts +32 -0
- package/source/directives/io/lib/throttle/Configuration.test.ts +40 -0
- package/source/directives/io/lib/throttle/Configuration.ts +58 -0
- package/source/directives/io/lib/throttle/Interval.ts +31 -0
- package/source/directives/io/lib/throttle/Keys.ts +40 -0
- package/source/directives/io/lib/throttle/Quota.ts +22 -0
- package/source/directives/io/lib/throttle/Quotas.test.ts +136 -0
- package/source/directives/io/lib/throttle/Quotas.ts +83 -0
- package/source/directives/io/lib/throttle/components/Component.ts +5 -0
- package/source/directives/io/lib/throttle/components/IP.ts +40 -0
- package/source/directives/io/lib/throttle/components/Path.ts +8 -0
- package/source/directives/io/lib/throttle/components/index.ts +13 -0
- package/source/directives/io/lib/throttle/conditions/Condition.ts +5 -0
- package/source/directives/io/lib/throttle/conditions/Status.ts +17 -0
- package/source/directives/io/lib/throttle/conditions/index.ts +11 -0
- package/source/directives/io/lib/throttle/index.ts +2 -0
- package/source/directives/io/schemas.test.ts +9 -0
- package/source/directives/io/schemas.ts +3 -0
- package/transpiled/Directive.d.ts +3 -0
- package/transpiled/Directive.js +12 -1
- package/transpiled/Directive.js.map +1 -1
- package/transpiled/Gateway.js +1 -0
- package/transpiled/Gateway.js.map +1 -1
- package/transpiled/HTTP/Server.js +41 -16
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/HTTP/exceptions.d.ts +11 -8
- package/transpiled/HTTP/exceptions.js +23 -17
- package/transpiled/HTTP/exceptions.js.map +1 -1
- package/transpiled/HTTP/messages.d.ts +1 -0
- package/transpiled/Query.d.ts +1 -0
- package/transpiled/Query.js +4 -0
- package/transpiled/Query.js.map +1 -1
- package/transpiled/RTD/Directives.d.ts +3 -0
- package/transpiled/RTD/Tree.d.ts +1 -0
- package/transpiled/RTD/Tree.js +3 -0
- package/transpiled/RTD/Tree.js.map +1 -1
- package/transpiled/RTD/syntax/types.d.ts +1 -0
- package/transpiled/RTD/syntax/types.js.map +1 -1
- package/transpiled/directives/io/Directive.d.ts +5 -2
- package/transpiled/directives/io/IO.d.ts +4 -2
- package/transpiled/directives/io/IO.js +13 -3
- package/transpiled/directives/io/IO.js.map +1 -1
- package/transpiled/directives/io/Input.d.ts +1 -1
- package/transpiled/directives/io/Input.js +1 -1
- package/transpiled/directives/io/Input.js.map +1 -1
- package/transpiled/directives/io/Output.d.ts +1 -1
- package/transpiled/directives/io/Output.js +1 -1
- package/transpiled/directives/io/Output.js.map +1 -1
- package/transpiled/directives/io/Throttle.d.ts +11 -0
- package/transpiled/directives/io/Throttle.js +51 -0
- package/transpiled/directives/io/Throttle.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/Configuration.d.ts +23 -0
- package/transpiled/directives/io/lib/throttle/Configuration.js +27 -0
- package/transpiled/directives/io/lib/throttle/Configuration.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/Interval.d.ts +9 -0
- package/transpiled/directives/io/lib/throttle/Interval.js +31 -0
- package/transpiled/directives/io/lib/throttle/Interval.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/Keys.d.ts +12 -0
- package/transpiled/directives/io/lib/throttle/Keys.js +34 -0
- package/transpiled/directives/io/lib/throttle/Keys.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/Quota.d.ts +8 -0
- package/transpiled/directives/io/lib/throttle/Quota.js +22 -0
- package/transpiled/directives/io/lib/throttle/Quota.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/Quotas.d.ts +26 -0
- package/transpiled/directives/io/lib/throttle/Quotas.js +61 -0
- package/transpiled/directives/io/lib/throttle/Quotas.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/components/Component.d.ts +4 -0
- package/transpiled/directives/io/lib/throttle/components/Component.js +3 -0
- package/transpiled/directives/io/lib/throttle/components/Component.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/components/IP.d.ts +6 -0
- package/transpiled/directives/io/lib/throttle/components/IP.js +33 -0
- package/transpiled/directives/io/lib/throttle/components/IP.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/components/Path.d.ts +5 -0
- package/transpiled/directives/io/lib/throttle/components/Path.js +10 -0
- package/transpiled/directives/io/lib/throttle/components/Path.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/components/index.d.ts +5 -0
- package/transpiled/directives/io/lib/throttle/components/index.js +10 -0
- package/transpiled/directives/io/lib/throttle/components/index.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/conditions/Condition.d.ts +4 -0
- package/transpiled/directives/io/lib/throttle/conditions/Condition.js +3 -0
- package/transpiled/directives/io/lib/throttle/conditions/Condition.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/conditions/Status.d.ts +7 -0
- package/transpiled/directives/io/lib/throttle/conditions/Status.js +19 -0
- package/transpiled/directives/io/lib/throttle/conditions/Status.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/conditions/index.d.ts +5 -0
- package/transpiled/directives/io/lib/throttle/conditions/index.js +8 -0
- package/transpiled/directives/io/lib/throttle/conditions/index.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/index.d.ts +2 -0
- package/transpiled/directives/io/lib/throttle/index.js +8 -0
- package/transpiled/directives/io/lib/throttle/index.js.map +1 -0
- package/transpiled/directives/io/schemas.d.ts +2 -0
- package/transpiled/directives/io/schemas.js +2 -1
- package/transpiled/directives/io/schemas.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.exposition",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.154",
|
|
4
4
|
"description": "Toa Exposition",
|
|
5
5
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@aws-sdk/protocol-http": "3.370.0",
|
|
21
21
|
"@simplewebauthn/server": "13.1.1",
|
|
22
|
-
"@toa.io/core": "1.0.0-alpha.
|
|
22
|
+
"@toa.io/core": "1.0.0-alpha.154",
|
|
23
23
|
"@toa.io/generic": "1.0.0-alpha.93",
|
|
24
24
|
"@toa.io/schemas": "1.0.0-alpha.143",
|
|
25
25
|
"bcryptjs": "2.4.3",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@toa.io/agent": "1.0.0-alpha.124",
|
|
54
|
-
"@toa.io/extensions.storages": "1.0.0-alpha.
|
|
54
|
+
"@toa.io/extensions.storages": "1.0.0-alpha.153",
|
|
55
55
|
"@types/bcryptjs": "2.4.3",
|
|
56
56
|
"@types/cors": "2.8.13",
|
|
57
57
|
"@types/negotiator": "0.6.1",
|
|
@@ -63,5 +63,5 @@
|
|
|
63
63
|
},
|
|
64
64
|
"testEnvironment": "node"
|
|
65
65
|
},
|
|
66
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "01832fc47f9906d6a63180d5c2813db86763506a"
|
|
67
67
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
definitions:
|
|
2
|
+
component: &component
|
|
3
|
+
type: string
|
|
4
|
+
enum: [ip, path]
|
|
5
|
+
condition: &condition
|
|
6
|
+
type: object
|
|
7
|
+
properties:
|
|
8
|
+
status:
|
|
9
|
+
type: number
|
|
10
|
+
type: object
|
|
11
|
+
properties:
|
|
12
|
+
key:
|
|
13
|
+
anyOf:
|
|
14
|
+
- *component
|
|
15
|
+
- type: array
|
|
16
|
+
items: *component
|
|
17
|
+
minItems: 1
|
|
18
|
+
condition:
|
|
19
|
+
anyOf:
|
|
20
|
+
- *condition
|
|
21
|
+
- type: array
|
|
22
|
+
items: *condition
|
|
23
|
+
minItems: 1
|
|
24
|
+
requests:
|
|
25
|
+
type: integer
|
|
26
|
+
minimum: 1
|
|
27
|
+
interval: &interval
|
|
28
|
+
description: Seconds
|
|
29
|
+
type: integer
|
|
30
|
+
minimum: 1
|
|
31
|
+
cooldown: *interval
|
|
32
|
+
required:
|
|
33
|
+
- key
|
|
34
|
+
- requests
|
|
35
|
+
- interval
|
|
36
|
+
- cooldown
|
package/schemas/query.cos.yaml
CHANGED
package/source/Directive.test.ts
CHANGED
|
@@ -11,14 +11,16 @@ const families: Array<jest.MockedObjectDeep<DirectiveFamily>> = [
|
|
|
11
11
|
mandatory: true,
|
|
12
12
|
create: jest.fn((_0: any, _1: any, _2: any) => generate() as any),
|
|
13
13
|
preflight: jest.fn(),
|
|
14
|
-
settle: jest.fn()
|
|
14
|
+
settle: jest.fn(),
|
|
15
|
+
dispose: jest.fn()
|
|
15
16
|
},
|
|
16
17
|
{
|
|
17
18
|
name: 'bar',
|
|
18
19
|
mandatory: false,
|
|
19
20
|
create: jest.fn((_0: string, _1: any, _2: any) => generate() as any),
|
|
20
21
|
preflight: jest.fn(),
|
|
21
|
-
settle: jest.fn()
|
|
22
|
+
settle: jest.fn(),
|
|
23
|
+
dispose: jest.fn()
|
|
22
24
|
}
|
|
23
25
|
]
|
|
24
26
|
|
package/source/Directive.ts
CHANGED
|
@@ -36,12 +36,18 @@ export class Directives implements RTD.Directives {
|
|
|
36
36
|
if (set.family.settle !== undefined)
|
|
37
37
|
await set.family.settle(set.directives, context, response)
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
public dispose (): void {
|
|
41
|
+
for (const set of this.sets)
|
|
42
|
+
set.family.dispose?.(set.directives)
|
|
43
|
+
}
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
export class DirectivesFactory implements RTD.DirectiveFactory {
|
|
42
47
|
private readonly remotes: Remotes
|
|
43
48
|
private readonly families: Record<string, RTD.DirectiveFamily> = {}
|
|
44
49
|
private readonly mandatory: string[] = []
|
|
50
|
+
private readonly instances: Directives[] = []
|
|
45
51
|
|
|
46
52
|
public constructor (families: RTD.DirectiveFamily[], remotes: Remotes) {
|
|
47
53
|
for (const family of families) {
|
|
@@ -88,7 +94,16 @@ export class DirectivesFactory implements RTD.DirectiveFactory {
|
|
|
88
94
|
directives
|
|
89
95
|
})
|
|
90
96
|
|
|
91
|
-
|
|
97
|
+
const directives = new Directives(sets)
|
|
98
|
+
|
|
99
|
+
this.instances.push(directives)
|
|
100
|
+
|
|
101
|
+
return directives
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public dispose (): void {
|
|
105
|
+
for (const directives of this.instances)
|
|
106
|
+
directives.dispose()
|
|
92
107
|
}
|
|
93
108
|
}
|
|
94
109
|
|
package/source/Gateway.ts
CHANGED
package/source/HTTP/Server.ts
CHANGED
|
@@ -29,7 +29,9 @@ export class Server extends Connector {
|
|
|
29
29
|
|
|
30
30
|
this.server.on('clientError', (error, socket) => {
|
|
31
31
|
console.warn('Client connection error', error)
|
|
32
|
-
|
|
32
|
+
|
|
33
|
+
if (socket.writable) socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
|
|
34
|
+
else socket.destroy()
|
|
33
35
|
})
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -70,6 +72,20 @@ export class Server extends Connector {
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
private listener (request: http.IncomingMessage, response: http.ServerResponse): void {
|
|
75
|
+
request.once('error', (error) => {
|
|
76
|
+
console.warn('Request error', errorAttributes(request, error))
|
|
77
|
+
|
|
78
|
+
if (!response.writableEnded)
|
|
79
|
+
response.destroy()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
request.socket.once('error', (error) => {
|
|
83
|
+
console.warn('Socket error', errorAttributes(request, error))
|
|
84
|
+
|
|
85
|
+
if (!response.writableEnded)
|
|
86
|
+
response.destroy()
|
|
87
|
+
})
|
|
88
|
+
|
|
73
89
|
const invalid = validate(request)
|
|
74
90
|
|
|
75
91
|
if (invalid !== null) {
|
|
@@ -80,9 +96,6 @@ export class Server extends Connector {
|
|
|
80
96
|
return
|
|
81
97
|
}
|
|
82
98
|
|
|
83
|
-
request.once('error', (error) => console.warn('Request error', errorAttributes(request, error)))
|
|
84
|
-
request.socket.once('error', (error) => console.warn('Socket error', errorAttributes(request, error)))
|
|
85
|
-
|
|
86
99
|
if (request.method === undefined || !this.properties.methods.has(request.method)) {
|
|
87
100
|
response.writeHead(501).end()
|
|
88
101
|
|
|
@@ -113,7 +126,6 @@ export class Server extends Connector {
|
|
|
113
126
|
.finally(() => {
|
|
114
127
|
request.removeAllListeners('error')
|
|
115
128
|
request.socket.removeAllListeners('error')
|
|
116
|
-
response.removeAllListeners('error')
|
|
117
129
|
})
|
|
118
130
|
}
|
|
119
131
|
|
|
@@ -139,22 +151,36 @@ export class Server extends Connector {
|
|
|
139
151
|
|
|
140
152
|
private fail (context: Context, response: http.ServerResponse) {
|
|
141
153
|
return async (exception: Error) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
try {
|
|
155
|
+
if (!context.request.complete)
|
|
156
|
+
await adam(context.request)
|
|
157
|
+
|
|
158
|
+
if (!response.writableEnded) {
|
|
159
|
+
response.statusCode = exception instanceof Exception ? exception.status : 500
|
|
160
|
+
|
|
161
|
+
const message: OutgoingMessage = { status: response.statusCode }
|
|
162
|
+
|
|
163
|
+
// eslint-disable-next-line max-depth
|
|
164
|
+
if (context.encoder === null)
|
|
165
|
+
message.body = undefined
|
|
166
|
+
else if (exception instanceof ClientError || this.properties.debug)
|
|
167
|
+
message.body =
|
|
168
|
+
exception instanceof Exception
|
|
169
|
+
? exception.body
|
|
170
|
+
: exception.stack ?? exception.message
|
|
171
|
+
|
|
172
|
+
await write(context, response, message)
|
|
173
|
+
}
|
|
174
|
+
} catch (final) {
|
|
175
|
+
console.error('Error in error handler', final)
|
|
176
|
+
|
|
177
|
+
if (!response.writableEnded)
|
|
178
|
+
try {
|
|
179
|
+
response.writeHead(500).end()
|
|
180
|
+
} catch (e) {
|
|
181
|
+
// Nothing more we can do
|
|
182
|
+
}
|
|
183
|
+
}
|
|
158
184
|
}
|
|
159
185
|
}
|
|
160
186
|
}
|
|
@@ -36,18 +36,6 @@ export class NotFound extends ClientError {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export class Conflict extends ClientError {
|
|
40
|
-
public constructor (body?: any) {
|
|
41
|
-
super(409, body)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export class UnprocessableEntity extends ClientError {
|
|
46
|
-
public constructor (body?: any) {
|
|
47
|
-
super(422, body)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
39
|
export class MethodNotAllowed extends ClientError {
|
|
52
40
|
public constructor () {
|
|
53
41
|
super(405)
|
|
@@ -60,9 +48,9 @@ export class NotAcceptable extends ClientError {
|
|
|
60
48
|
}
|
|
61
49
|
}
|
|
62
50
|
|
|
63
|
-
export class
|
|
64
|
-
public constructor () {
|
|
65
|
-
super(
|
|
51
|
+
export class Conflict extends ClientError {
|
|
52
|
+
public constructor (body?: any) {
|
|
53
|
+
super(409, body)
|
|
66
54
|
}
|
|
67
55
|
}
|
|
68
56
|
|
|
@@ -77,3 +65,21 @@ export class RequestEntityTooLarge extends ClientError {
|
|
|
77
65
|
super(413, body)
|
|
78
66
|
}
|
|
79
67
|
}
|
|
68
|
+
|
|
69
|
+
export class UnsupportedMediaType extends ClientError {
|
|
70
|
+
public constructor () {
|
|
71
|
+
super(415)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class UnprocessableEntity extends ClientError {
|
|
76
|
+
public constructor (body?: any) {
|
|
77
|
+
super(422, body)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class TooManyRequests extends ClientError {
|
|
82
|
+
public constructor () {
|
|
83
|
+
super(429)
|
|
84
|
+
}
|
|
85
|
+
}
|
package/source/HTTP/messages.ts
CHANGED
package/source/Query.ts
CHANGED
|
@@ -12,9 +12,11 @@ export class Query {
|
|
|
12
12
|
private readonly closed: boolean = false
|
|
13
13
|
private readonly prepend: ',' | ';' = ';'
|
|
14
14
|
private readonly queryable: boolean
|
|
15
|
+
private readonly searchable: boolean
|
|
15
16
|
|
|
16
17
|
public constructor (query: syntax.Query) {
|
|
17
18
|
this.queryable = queryable(query)
|
|
19
|
+
this.searchable = query?.search === true
|
|
18
20
|
|
|
19
21
|
if (this.queryable) {
|
|
20
22
|
query.omit ??= { value: 0, range: [0, 1000] }
|
|
@@ -104,6 +106,9 @@ export class Query {
|
|
|
104
106
|
query = null!
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
if (query.search !== undefined && !this.searchable)
|
|
110
|
+
throw new http.BadRequest('Query search is not allowed')
|
|
111
|
+
|
|
107
112
|
return {
|
|
108
113
|
query,
|
|
109
114
|
parameters
|
package/source/RTD/Directives.ts
CHANGED
|
@@ -6,10 +6,12 @@ import type { Output } from '../io'
|
|
|
6
6
|
export interface Directives {
|
|
7
7
|
preflight: (context: Context, parameters: Parameter[]) => Promise<Output>
|
|
8
8
|
settle: (context: Context, response: OutgoingMessage) => Promise<void>
|
|
9
|
+
dispose: () => void
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export interface DirectiveFactory {
|
|
12
13
|
create: (directives: syntax.Directive[]) => Directives
|
|
14
|
+
dispose: () => void
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export interface DirectiveSet {
|
|
@@ -30,4 +32,6 @@ export interface DirectiveFamily<TDirective = any, TExtension = any> {
|
|
|
30
32
|
settle?: (directives: TDirective[],
|
|
31
33
|
request: Context & TExtension,
|
|
32
34
|
response: OutgoingMessage) => void | Promise<void>
|
|
35
|
+
|
|
36
|
+
dispose?: (directives: TDirective[]) => void
|
|
33
37
|
}
|
package/source/RTD/Tree.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { Input } from '../../io'
|
|
1
|
+
import type { Input as Context } from '../../io'
|
|
2
|
+
import type * as http from '../../HTTP'
|
|
2
3
|
|
|
3
4
|
export interface Directive {
|
|
4
|
-
|
|
5
|
+
preflight: (context: Context) => void
|
|
6
|
+
settle?: (context: Context, response: http.OutgoingMessage) => Promise<void> | void
|
|
7
|
+
dispose?: () => void
|
|
5
8
|
}
|
|
6
9
|
|
|
7
10
|
export interface Constructor {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Output } from './Output'
|
|
2
2
|
import { Input } from './Input'
|
|
3
|
+
import { Throttle } from './Throttle'
|
|
4
|
+
import type * as http from '../../HTTP'
|
|
3
5
|
import type { Constructor, Directive } from './Directive'
|
|
4
|
-
import type { Input as Context } from '../../io'
|
|
5
6
|
import type { DirectiveFamily } from '../../RTD'
|
|
6
7
|
|
|
7
8
|
export class IO implements DirectiveFamily<Directive> {
|
|
@@ -19,25 +20,36 @@ export class IO implements DirectiveFamily<Directive> {
|
|
|
19
20
|
return new Directive(value)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
public preflight (directives: Directive[], context: Context): null {
|
|
23
|
+
public preflight (directives: Directive[], context: http.Context): null {
|
|
23
24
|
let restricted = false
|
|
24
25
|
|
|
25
26
|
for (const directive of directives) {
|
|
26
27
|
restricted ||= directive instanceof Output
|
|
27
28
|
|
|
28
|
-
directive.
|
|
29
|
+
directive.preflight(context)
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
if (!restricted)
|
|
32
|
-
DENIAL.
|
|
33
|
+
DENIAL.preflight(context)
|
|
33
34
|
|
|
34
35
|
return null
|
|
35
36
|
}
|
|
37
|
+
|
|
38
|
+
public async settle (directives: Directive[], context: http.Context, output: http.OutgoingMessage): Promise<void> {
|
|
39
|
+
for (const directive of directives)
|
|
40
|
+
await directive.settle?.(context, output)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public dispose (directives: Directive[]): void {
|
|
44
|
+
for (const directive of directives)
|
|
45
|
+
directive.dispose?.()
|
|
46
|
+
}
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
const constructors: Record<string, Constructor> = {
|
|
50
|
+
input: Input,
|
|
39
51
|
output: Output,
|
|
40
|
-
|
|
52
|
+
throttle: Throttle
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
const DENIAL: Output = new Output([])
|
|
@@ -15,7 +15,7 @@ export class Input implements Directive {
|
|
|
15
15
|
schemas.input.validate<Permissions>(permissions, 'Incorrect \'io:input\' format')
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
public
|
|
18
|
+
public preflight (context: Context): void {
|
|
19
19
|
context.pipelines.body.push((body) => this.check(body))
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -26,7 +26,7 @@ export class Output implements Directive {
|
|
|
26
26
|
schemas.output.validate(permissions, 'Incorrect \'io:output\' format')
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
public
|
|
29
|
+
public preflight (context: Context): void {
|
|
30
30
|
context.pipelines.response.push(this.restriction(context))
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { TooManyRequests } from '../../HTTP'
|
|
2
|
+
import * as schemas from './schemas'
|
|
3
|
+
import { parse, Quotas, type Declaration } from './lib/throttle'
|
|
4
|
+
import type * as http from '../../HTTP'
|
|
5
|
+
import type { Directive } from './Directive'
|
|
6
|
+
|
|
7
|
+
export class Throttle implements Directive {
|
|
8
|
+
private readonly quotas: Quotas
|
|
9
|
+
|
|
10
|
+
public constructor (declaration: Declaration) {
|
|
11
|
+
const configuration = parse(declaration)
|
|
12
|
+
|
|
13
|
+
this.quotas = Quotas.create(configuration)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public static validate (declaration: unknown): asserts declaration is Declaration {
|
|
17
|
+
schemas.throttle.validate(declaration, 'Incorrect \'io:throttle\' format')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public preflight (context: http.Context): void {
|
|
21
|
+
if (!this.quotas.ok(context))
|
|
22
|
+
throw new TooManyRequests()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public settle (context: http.Context, output: http.OutgoingMessage): void {
|
|
26
|
+
this.quotas.use(context, output)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public dispose (): void {
|
|
30
|
+
this.quotas.dispose()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { parse, type Configuration } from './Configuration'
|
|
2
|
+
|
|
3
|
+
const rest: Omit<Configuration, 'key' | 'condition'> = {
|
|
4
|
+
interval: 1,
|
|
5
|
+
requests: 1,
|
|
6
|
+
cooldown: 1
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
it('should convert key', () => {
|
|
10
|
+
const result: Partial<Configuration> = {
|
|
11
|
+
key: [
|
|
12
|
+
{
|
|
13
|
+
method: 'ip'
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
expect(parse({ key: 'ip', ...rest })).toMatchObject(result)
|
|
19
|
+
expect(parse({ key: ['ip'], ...rest })).toMatchObject(result)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should convert condition', () => {
|
|
23
|
+
expect(parse({ key: ['ip', 'path'], condition: { status: '404' }, ...rest })).toMatchObject({
|
|
24
|
+
condition: [
|
|
25
|
+
{
|
|
26
|
+
method: 'status',
|
|
27
|
+
options: '404'
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
expect(parse({ key: 'ip', condition: { status: '404' }, ...rest })).toMatchObject({
|
|
33
|
+
condition: [
|
|
34
|
+
{
|
|
35
|
+
method: 'status',
|
|
36
|
+
options: '404'
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface Configuration {
|
|
2
|
+
key: KeyComponent[]
|
|
3
|
+
condition?: KeyCondition[]
|
|
4
|
+
requests: number
|
|
5
|
+
interval: number
|
|
6
|
+
cooldown: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Rule<T, K = unknown> {
|
|
10
|
+
method: T
|
|
11
|
+
options?: K
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type KeyComponentMethod = 'ip' | 'path'
|
|
15
|
+
export type KeyConditionMethod = 'status'
|
|
16
|
+
|
|
17
|
+
export type KeyComponent = Rule<KeyComponentMethod>
|
|
18
|
+
export type KeyCondition = Rule<KeyConditionMethod>
|
|
19
|
+
|
|
20
|
+
type KeyDeclaration = KeyComponentMethod | KeyComponentMethod[]
|
|
21
|
+
|
|
22
|
+
type ConditionDeclaration =
|
|
23
|
+
Record<KeyConditionMethod, unknown>
|
|
24
|
+
| Array<Record<KeyConditionMethod, unknown>>
|
|
25
|
+
|
|
26
|
+
export interface Declaration extends Omit<Configuration, 'key' | 'condition'> {
|
|
27
|
+
key: KeyDeclaration
|
|
28
|
+
condition?: ConditionDeclaration
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parse (declaration: Declaration): Configuration {
|
|
32
|
+
const { key, condition, requests, interval, cooldown } = declaration
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
key: mapKey(key),
|
|
36
|
+
condition: mapCondition(condition),
|
|
37
|
+
requests,
|
|
38
|
+
interval: interval * 1000,
|
|
39
|
+
cooldown: cooldown * 1000
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function mapKey (declaration: KeyDeclaration): KeyComponent[] {
|
|
44
|
+
const methods = Array.isArray(declaration) ? declaration : [declaration]
|
|
45
|
+
|
|
46
|
+
return methods.map((method) => ({ method }))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function mapCondition (declaration?: ConditionDeclaration): KeyCondition[] | undefined {
|
|
50
|
+
if (declaration === undefined)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
// reduce to a single object, then map entries to rules
|
|
54
|
+
const conditions = Array.isArray(declaration) ? declaration : [declaration]
|
|
55
|
+
const single = conditions.reduce((acc, condition) => ({ ...acc, ...condition }), {})
|
|
56
|
+
|
|
57
|
+
return Object.entries(single).map(([method, options]) => ({ method, options })) as KeyCondition[]
|
|
58
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class Interval extends EventEmitter {
|
|
5
|
+
public number: number = 0
|
|
6
|
+
private interval: ReturnType<typeof setInterval> | null = null
|
|
7
|
+
|
|
8
|
+
public constructor (interval: number) {
|
|
9
|
+
super()
|
|
10
|
+
|
|
11
|
+
const number = Math.ceil(Date.now() / interval)
|
|
12
|
+
const shift = number * interval - Date.now()
|
|
13
|
+
|
|
14
|
+
assert.ok(shift >= 0, 'shift must be positive')
|
|
15
|
+
|
|
16
|
+
setTimeout(() => this.start(interval), shift)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public start (interval: number): void {
|
|
20
|
+
this.interval = setInterval(() => this.emit('tick'), interval)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public dispose (): void {
|
|
24
|
+
if (this.interval !== null) {
|
|
25
|
+
clearInterval(this.interval)
|
|
26
|
+
this.interval = null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.removeAllListeners()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { Components, type Component } from './components'
|
|
3
|
+
import { Conditions, type Condition } from './conditions'
|
|
4
|
+
import type { KeyComponent, KeyCondition } from './Configuration'
|
|
5
|
+
import type { Input as Context, Output } from '../../../../io'
|
|
6
|
+
|
|
7
|
+
export class Keys {
|
|
8
|
+
private readonly components: Component[]
|
|
9
|
+
private readonly conditions?: Condition[]
|
|
10
|
+
|
|
11
|
+
public constructor (components: Component[], conditions?: Condition[]) {
|
|
12
|
+
this.components = components
|
|
13
|
+
this.conditions = conditions
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public static create (componentRules: KeyComponent[], conditionRules?: KeyCondition[]): Keys {
|
|
17
|
+
const components = componentRules.map((rule) => new Components[rule.method](rule.options))
|
|
18
|
+
const conditions = conditionRules?.map((rule) => new Conditions[rule.method](rule.options))
|
|
19
|
+
|
|
20
|
+
return new this(components, conditions)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public get (context: Context): string {
|
|
24
|
+
const hash = createHash('sha256')
|
|
25
|
+
|
|
26
|
+
for (const component of this.components)
|
|
27
|
+
hash.update(component.get(context))
|
|
28
|
+
|
|
29
|
+
return hash.digest('hex')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public match (input: Context, output: Output): string | null {
|
|
33
|
+
const miss = this.conditions?.some((condition) => !condition.match(input, output))
|
|
34
|
+
|
|
35
|
+
if (miss === true)
|
|
36
|
+
return null
|
|
37
|
+
else
|
|
38
|
+
return this.get(input)
|
|
39
|
+
}
|
|
40
|
+
}
|