@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.
Files changed (128) hide show
  1. package/components/identity.bans/operations/tsconfig.tsbuildinfo +1 -1
  2. package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
  3. package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
  4. package/components/identity.keys/operations/tsconfig.tsbuildinfo +1 -1
  5. package/components/identity.otp/operations/tsconfig.tsbuildinfo +1 -1
  6. package/components/identity.passkeys/operations/tsconfig.tsbuildinfo +1 -1
  7. package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
  8. package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
  9. package/documentation/io.md +42 -2
  10. package/documentation/notes/desync.jpg +0 -0
  11. package/documentation/notes/peers.md +59 -0
  12. package/documentation/notes/throttling.md +82 -0
  13. package/documentation/query.md +22 -0
  14. package/features/io.throttle.feature +40 -0
  15. package/features/query.feature +46 -0
  16. package/features/steps/components/pots/manifest.toa.yaml +3 -0
  17. package/package.json +4 -4
  18. package/schemas/io/throttle.cos.yaml +36 -0
  19. package/schemas/query.cos.yaml +1 -0
  20. package/schemas/querystring.cos.yaml +1 -0
  21. package/source/Directive.test.ts +4 -2
  22. package/source/Directive.ts +16 -1
  23. package/source/Gateway.ts +2 -0
  24. package/source/HTTP/Server.ts +47 -21
  25. package/source/HTTP/exceptions.ts +21 -15
  26. package/source/HTTP/messages.ts +1 -0
  27. package/source/Query.ts +5 -0
  28. package/source/RTD/Directives.ts +4 -0
  29. package/source/RTD/Tree.ts +4 -0
  30. package/source/RTD/syntax/types.ts +1 -0
  31. package/source/directives/io/Directive.ts +5 -2
  32. package/source/directives/io/IO.ts +17 -5
  33. package/source/directives/io/Input.ts +1 -1
  34. package/source/directives/io/Output.ts +1 -1
  35. package/source/directives/io/Throttle.ts +32 -0
  36. package/source/directives/io/lib/throttle/Configuration.test.ts +40 -0
  37. package/source/directives/io/lib/throttle/Configuration.ts +58 -0
  38. package/source/directives/io/lib/throttle/Interval.ts +31 -0
  39. package/source/directives/io/lib/throttle/Keys.ts +40 -0
  40. package/source/directives/io/lib/throttle/Quota.ts +22 -0
  41. package/source/directives/io/lib/throttle/Quotas.test.ts +136 -0
  42. package/source/directives/io/lib/throttle/Quotas.ts +83 -0
  43. package/source/directives/io/lib/throttle/components/Component.ts +5 -0
  44. package/source/directives/io/lib/throttle/components/IP.ts +40 -0
  45. package/source/directives/io/lib/throttle/components/Path.ts +8 -0
  46. package/source/directives/io/lib/throttle/components/index.ts +13 -0
  47. package/source/directives/io/lib/throttle/conditions/Condition.ts +5 -0
  48. package/source/directives/io/lib/throttle/conditions/Status.ts +17 -0
  49. package/source/directives/io/lib/throttle/conditions/index.ts +11 -0
  50. package/source/directives/io/lib/throttle/index.ts +2 -0
  51. package/source/directives/io/schemas.test.ts +9 -0
  52. package/source/directives/io/schemas.ts +3 -0
  53. package/transpiled/Directive.d.ts +3 -0
  54. package/transpiled/Directive.js +12 -1
  55. package/transpiled/Directive.js.map +1 -1
  56. package/transpiled/Gateway.js +1 -0
  57. package/transpiled/Gateway.js.map +1 -1
  58. package/transpiled/HTTP/Server.js +41 -16
  59. package/transpiled/HTTP/Server.js.map +1 -1
  60. package/transpiled/HTTP/exceptions.d.ts +11 -8
  61. package/transpiled/HTTP/exceptions.js +23 -17
  62. package/transpiled/HTTP/exceptions.js.map +1 -1
  63. package/transpiled/HTTP/messages.d.ts +1 -0
  64. package/transpiled/Query.d.ts +1 -0
  65. package/transpiled/Query.js +4 -0
  66. package/transpiled/Query.js.map +1 -1
  67. package/transpiled/RTD/Directives.d.ts +3 -0
  68. package/transpiled/RTD/Tree.d.ts +1 -0
  69. package/transpiled/RTD/Tree.js +3 -0
  70. package/transpiled/RTD/Tree.js.map +1 -1
  71. package/transpiled/RTD/syntax/types.d.ts +1 -0
  72. package/transpiled/RTD/syntax/types.js.map +1 -1
  73. package/transpiled/directives/io/Directive.d.ts +5 -2
  74. package/transpiled/directives/io/IO.d.ts +4 -2
  75. package/transpiled/directives/io/IO.js +13 -3
  76. package/transpiled/directives/io/IO.js.map +1 -1
  77. package/transpiled/directives/io/Input.d.ts +1 -1
  78. package/transpiled/directives/io/Input.js +1 -1
  79. package/transpiled/directives/io/Input.js.map +1 -1
  80. package/transpiled/directives/io/Output.d.ts +1 -1
  81. package/transpiled/directives/io/Output.js +1 -1
  82. package/transpiled/directives/io/Output.js.map +1 -1
  83. package/transpiled/directives/io/Throttle.d.ts +11 -0
  84. package/transpiled/directives/io/Throttle.js +51 -0
  85. package/transpiled/directives/io/Throttle.js.map +1 -0
  86. package/transpiled/directives/io/lib/throttle/Configuration.d.ts +23 -0
  87. package/transpiled/directives/io/lib/throttle/Configuration.js +27 -0
  88. package/transpiled/directives/io/lib/throttle/Configuration.js.map +1 -0
  89. package/transpiled/directives/io/lib/throttle/Interval.d.ts +9 -0
  90. package/transpiled/directives/io/lib/throttle/Interval.js +31 -0
  91. package/transpiled/directives/io/lib/throttle/Interval.js.map +1 -0
  92. package/transpiled/directives/io/lib/throttle/Keys.d.ts +12 -0
  93. package/transpiled/directives/io/lib/throttle/Keys.js +34 -0
  94. package/transpiled/directives/io/lib/throttle/Keys.js.map +1 -0
  95. package/transpiled/directives/io/lib/throttle/Quota.d.ts +8 -0
  96. package/transpiled/directives/io/lib/throttle/Quota.js +22 -0
  97. package/transpiled/directives/io/lib/throttle/Quota.js.map +1 -0
  98. package/transpiled/directives/io/lib/throttle/Quotas.d.ts +26 -0
  99. package/transpiled/directives/io/lib/throttle/Quotas.js +61 -0
  100. package/transpiled/directives/io/lib/throttle/Quotas.js.map +1 -0
  101. package/transpiled/directives/io/lib/throttle/components/Component.d.ts +4 -0
  102. package/transpiled/directives/io/lib/throttle/components/Component.js +3 -0
  103. package/transpiled/directives/io/lib/throttle/components/Component.js.map +1 -0
  104. package/transpiled/directives/io/lib/throttle/components/IP.d.ts +6 -0
  105. package/transpiled/directives/io/lib/throttle/components/IP.js +33 -0
  106. package/transpiled/directives/io/lib/throttle/components/IP.js.map +1 -0
  107. package/transpiled/directives/io/lib/throttle/components/Path.d.ts +5 -0
  108. package/transpiled/directives/io/lib/throttle/components/Path.js +10 -0
  109. package/transpiled/directives/io/lib/throttle/components/Path.js.map +1 -0
  110. package/transpiled/directives/io/lib/throttle/components/index.d.ts +5 -0
  111. package/transpiled/directives/io/lib/throttle/components/index.js +10 -0
  112. package/transpiled/directives/io/lib/throttle/components/index.js.map +1 -0
  113. package/transpiled/directives/io/lib/throttle/conditions/Condition.d.ts +4 -0
  114. package/transpiled/directives/io/lib/throttle/conditions/Condition.js +3 -0
  115. package/transpiled/directives/io/lib/throttle/conditions/Condition.js.map +1 -0
  116. package/transpiled/directives/io/lib/throttle/conditions/Status.d.ts +7 -0
  117. package/transpiled/directives/io/lib/throttle/conditions/Status.js +19 -0
  118. package/transpiled/directives/io/lib/throttle/conditions/Status.js.map +1 -0
  119. package/transpiled/directives/io/lib/throttle/conditions/index.d.ts +5 -0
  120. package/transpiled/directives/io/lib/throttle/conditions/index.js +8 -0
  121. package/transpiled/directives/io/lib/throttle/conditions/index.js.map +1 -0
  122. package/transpiled/directives/io/lib/throttle/index.d.ts +2 -0
  123. package/transpiled/directives/io/lib/throttle/index.js +8 -0
  124. package/transpiled/directives/io/lib/throttle/index.js.map +1 -0
  125. package/transpiled/directives/io/schemas.d.ts +2 -0
  126. package/transpiled/directives/io/schemas.js +2 -1
  127. package/transpiled/directives/io/schemas.js.map +1 -1
  128. package/transpiled/tsconfig.tsbuildinfo +1 -1
@@ -5,6 +5,9 @@ entity:
5
5
  title: string(64)
6
6
  volume: number(0, 1000]
7
7
  temperature?: number(0, 300]
8
+ index:
9
+ text:
10
+ title: text
8
11
 
9
12
  operations:
10
13
  create:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.exposition",
3
- "version": "1.0.0-alpha.152",
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.136",
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.143",
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": "066053005ffced9760789e1db1628b6526c1fae3"
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
@@ -1,6 +1,7 @@
1
1
  id?: string
2
2
  criteria?: string
3
3
  sort?: string
4
+ search?: boolean
4
5
  omit?:
5
6
  $ref: range
6
7
  limit?:
@@ -1,5 +1,6 @@
1
1
  id: string
2
2
  criteria: string
3
+ search: text
3
4
  sort: string
4
5
  omit: number
5
6
  limit: number
@@ -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
 
@@ -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
- return new Directives(sets)
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
@@ -65,6 +65,8 @@ export class Gateway extends Connector {
65
65
  }
66
66
 
67
67
  protected override dispose (): void {
68
+ this.tree.dispose()
69
+
68
70
  console.info('Gateway is closed')
69
71
  }
70
72
 
@@ -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
- socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
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
- if (!context.request.complete)
143
- await adam(context.request)
144
-
145
- response.statusCode = exception instanceof Exception ? exception.status : 500
146
-
147
- const message: OutgoingMessage = { status: response.statusCode }
148
-
149
- if (context.encoder === null)
150
- message.body = undefined
151
- else if (exception instanceof ClientError || this.properties.debug)
152
- message.body =
153
- exception instanceof Exception
154
- ? exception.body
155
- : exception.stack ?? exception.message
156
-
157
- await write(context, response, message)
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 UnsupportedMediaType extends ClientError {
64
- public constructor () {
65
- super(415)
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
+ }
@@ -138,6 +138,7 @@ export interface Query {
138
138
 
139
139
  id?: string
140
140
  criteria?: string
141
+ search?: string
141
142
  sort?: string
142
143
  omit?: string
143
144
  limit?: string
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
@@ -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
  }
@@ -39,6 +39,10 @@ export class Tree {
39
39
  this.trunk.merge(branch)
40
40
  }
41
41
 
42
+ public dispose (): void {
43
+ this.directives.dispose()
44
+ }
45
+
42
46
  private createNode
43
47
  (node: syntax.Node, protect: boolean, extension?: unknown): Node {
44
48
  const context: Context = {
@@ -34,6 +34,7 @@ export interface Mapping {
34
34
  export interface Query {
35
35
  id?: string
36
36
  criteria?: string
37
+ search?: boolean
37
38
  sort?: string
38
39
  omit?: Range
39
40
  limit?: Range
@@ -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
- attach: (context: Input) => void
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.attach(context)
29
+ directive.preflight(context)
29
30
  }
30
31
 
31
32
  if (!restricted)
32
- DENIAL.attach(context)
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
- input: Input
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 attach (context: Context): void {
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 attach (context: Context): void {
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
+ }