@toa.io/extensions.exposition 0.23.0-dev.0 → 0.24.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/documentation/access.md +2 -3
  2. package/documentation/cache.md +42 -0
  3. package/documentation/octets.md +3 -3
  4. package/documentation/tree.md +1 -5
  5. package/features/cache.feature +160 -0
  6. package/features/steps/HTTP.ts +8 -2
  7. package/features/steps/Parameters.ts +1 -2
  8. package/features/steps/Workspace.ts +5 -3
  9. package/package.json +13 -10
  10. package/readme.md +1 -0
  11. package/source/HTTP/Server.test.ts +5 -3
  12. package/source/HTTP/Server.ts +30 -18
  13. package/source/HTTP/messages.ts +4 -4
  14. package/source/RTD/syntax/parse.ts +2 -1
  15. package/source/Tenant.ts +5 -0
  16. package/source/directives/auth/Family.ts +2 -2
  17. package/source/directives/cache/Control.ts +59 -0
  18. package/source/directives/cache/Exact.ts +7 -0
  19. package/source/directives/cache/Family.ts +36 -0
  20. package/source/directives/cache/index.ts +3 -0
  21. package/source/directives/cache/types.ts +9 -0
  22. package/source/directives/index.ts +2 -1
  23. package/source/directives/octets/Fetch.ts +3 -3
  24. package/transpiled/Annotation.d.ts +7 -0
  25. package/transpiled/Annotation.js +3 -0
  26. package/transpiled/Annotation.js.map +1 -0
  27. package/transpiled/Branch.d.ts +7 -0
  28. package/transpiled/Branch.js +3 -0
  29. package/transpiled/Branch.js.map +1 -0
  30. package/transpiled/Composition.d.ts +14 -0
  31. package/transpiled/Composition.js +43 -0
  32. package/transpiled/Composition.js.map +1 -0
  33. package/transpiled/Context.d.ts +5 -0
  34. package/transpiled/Context.js +3 -0
  35. package/transpiled/Context.js.map +1 -0
  36. package/transpiled/Directive.d.ts +32 -0
  37. package/transpiled/Directive.js +76 -0
  38. package/transpiled/Directive.js.map +1 -0
  39. package/transpiled/Endpoint.d.ts +20 -0
  40. package/transpiled/Endpoint.js +45 -0
  41. package/transpiled/Endpoint.js.map +1 -0
  42. package/transpiled/Factory.d.ts +10 -0
  43. package/transpiled/Factory.js +66 -0
  44. package/transpiled/Factory.js.map +1 -0
  45. package/transpiled/Gateway.d.ts +19 -0
  46. package/transpiled/Gateway.js +92 -0
  47. package/transpiled/Gateway.js.map +1 -0
  48. package/transpiled/HTTP/Server.d.ts +22 -0
  49. package/transpiled/HTTP/Server.fixtures.d.ts +11 -0
  50. package/transpiled/HTTP/Server.fixtures.js +32 -0
  51. package/transpiled/HTTP/Server.fixtures.js.map +1 -0
  52. package/transpiled/HTTP/Server.js +131 -0
  53. package/transpiled/HTTP/Server.js.map +1 -0
  54. package/transpiled/HTTP/exceptions.d.ts +34 -0
  55. package/transpiled/HTTP/exceptions.js +71 -0
  56. package/transpiled/HTTP/exceptions.js.map +1 -0
  57. package/transpiled/HTTP/formats/index.d.ts +10 -0
  58. package/transpiled/HTTP/formats/index.js +38 -0
  59. package/transpiled/HTTP/formats/index.js.map +1 -0
  60. package/transpiled/HTTP/formats/json.d.ts +6 -0
  61. package/transpiled/HTTP/formats/json.js +17 -0
  62. package/transpiled/HTTP/formats/json.js.map +1 -0
  63. package/transpiled/HTTP/formats/msgpack.d.ts +6 -0
  64. package/transpiled/HTTP/formats/msgpack.js +38 -0
  65. package/transpiled/HTTP/formats/msgpack.js.map +1 -0
  66. package/transpiled/HTTP/formats/text.d.ts +6 -0
  67. package/transpiled/HTTP/formats/text.js +15 -0
  68. package/transpiled/HTTP/formats/text.js.map +1 -0
  69. package/transpiled/HTTP/formats/yaml.d.ts +6 -0
  70. package/transpiled/HTTP/formats/yaml.js +41 -0
  71. package/transpiled/HTTP/formats/yaml.js.map +1 -0
  72. package/transpiled/HTTP/index.d.ts +3 -0
  73. package/transpiled/HTTP/index.js +20 -0
  74. package/transpiled/HTTP/index.js.map +1 -0
  75. package/transpiled/HTTP/messages.d.ts +28 -0
  76. package/transpiled/HTTP/messages.js +70 -0
  77. package/transpiled/HTTP/messages.js.map +1 -0
  78. package/transpiled/Mapping.d.ts +8 -0
  79. package/transpiled/Mapping.js +38 -0
  80. package/transpiled/Mapping.js.map +1 -0
  81. package/transpiled/Query.d.ts +13 -0
  82. package/transpiled/Query.js +107 -0
  83. package/transpiled/Query.js.map +1 -0
  84. package/transpiled/RTD/Context.d.ts +11 -0
  85. package/transpiled/RTD/Context.js +3 -0
  86. package/transpiled/RTD/Context.js.map +1 -0
  87. package/transpiled/RTD/Directives.d.ts +7 -0
  88. package/transpiled/RTD/Directives.js +3 -0
  89. package/transpiled/RTD/Directives.js.map +1 -0
  90. package/transpiled/RTD/Endpoint.d.ts +9 -0
  91. package/transpiled/RTD/Endpoint.js +3 -0
  92. package/transpiled/RTD/Endpoint.js.map +1 -0
  93. package/transpiled/RTD/Match.d.ts +11 -0
  94. package/transpiled/RTD/Match.js +3 -0
  95. package/transpiled/RTD/Match.js.map +1 -0
  96. package/transpiled/RTD/Method.d.ts +9 -0
  97. package/transpiled/RTD/Method.js +16 -0
  98. package/transpiled/RTD/Method.js.map +1 -0
  99. package/transpiled/RTD/Node.d.ts +21 -0
  100. package/transpiled/RTD/Node.js +61 -0
  101. package/transpiled/RTD/Node.js.map +1 -0
  102. package/transpiled/RTD/Route.d.ts +14 -0
  103. package/transpiled/RTD/Route.js +49 -0
  104. package/transpiled/RTD/Route.js.map +1 -0
  105. package/transpiled/RTD/Tree.d.ts +14 -0
  106. package/transpiled/RTD/Tree.js +40 -0
  107. package/transpiled/RTD/Tree.js.map +1 -0
  108. package/transpiled/RTD/factory.d.ts +6 -0
  109. package/transpiled/RTD/factory.js +36 -0
  110. package/transpiled/RTD/factory.js.map +1 -0
  111. package/transpiled/RTD/index.d.ts +8 -0
  112. package/transpiled/RTD/index.js +38 -0
  113. package/transpiled/RTD/index.js.map +1 -0
  114. package/transpiled/RTD/segment.d.ts +8 -0
  115. package/transpiled/RTD/segment.js +25 -0
  116. package/transpiled/RTD/segment.js.map +1 -0
  117. package/transpiled/RTD/syntax/index.d.ts +2 -0
  118. package/transpiled/RTD/syntax/index.js +19 -0
  119. package/transpiled/RTD/syntax/index.js.map +1 -0
  120. package/transpiled/RTD/syntax/parse.d.ts +4 -0
  121. package/transpiled/RTD/syntax/parse.js +128 -0
  122. package/transpiled/RTD/syntax/parse.js.map +1 -0
  123. package/transpiled/RTD/syntax/types.d.ts +41 -0
  124. package/transpiled/RTD/syntax/types.js +5 -0
  125. package/transpiled/RTD/syntax/types.js.map +1 -0
  126. package/transpiled/Remotes.d.ts +9 -0
  127. package/transpiled/Remotes.js +25 -0
  128. package/transpiled/Remotes.js.map +1 -0
  129. package/transpiled/Tenant.d.ts +13 -0
  130. package/transpiled/Tenant.js +34 -0
  131. package/transpiled/Tenant.js.map +1 -0
  132. package/transpiled/deployment.d.ts +3 -0
  133. package/transpiled/deployment.js +67 -0
  134. package/transpiled/deployment.js.map +1 -0
  135. package/transpiled/directives/auth/Anonymous.d.ts +6 -0
  136. package/transpiled/directives/auth/Anonymous.js +17 -0
  137. package/transpiled/directives/auth/Anonymous.js.map +1 -0
  138. package/transpiled/directives/auth/Echo.d.ts +6 -0
  139. package/transpiled/directives/auth/Echo.js +13 -0
  140. package/transpiled/directives/auth/Echo.js.map +1 -0
  141. package/transpiled/directives/auth/Family.d.ts +20 -0
  142. package/transpiled/directives/auth/Family.js +118 -0
  143. package/transpiled/directives/auth/Family.js.map +1 -0
  144. package/transpiled/directives/auth/Id.d.ts +7 -0
  145. package/transpiled/directives/auth/Id.js +17 -0
  146. package/transpiled/directives/auth/Id.js.map +1 -0
  147. package/transpiled/directives/auth/Incept.d.ts +10 -0
  148. package/transpiled/directives/auth/Incept.js +58 -0
  149. package/transpiled/directives/auth/Incept.js.map +1 -0
  150. package/transpiled/directives/auth/Role.d.ts +11 -0
  151. package/transpiled/directives/auth/Role.js +44 -0
  152. package/transpiled/directives/auth/Role.js.map +1 -0
  153. package/transpiled/directives/auth/Rule.d.ts +9 -0
  154. package/transpiled/directives/auth/Rule.js +22 -0
  155. package/transpiled/directives/auth/Rule.js.map +1 -0
  156. package/transpiled/directives/auth/Scheme.d.ts +7 -0
  157. package/transpiled/directives/auth/Scheme.js +47 -0
  158. package/transpiled/directives/auth/Scheme.js.map +1 -0
  159. package/transpiled/directives/auth/index.d.ts +2 -0
  160. package/transpiled/directives/auth/index.js +7 -0
  161. package/transpiled/directives/auth/index.js.map +1 -0
  162. package/transpiled/directives/auth/schemes.d.ts +3 -0
  163. package/transpiled/directives/auth/schemes.js +9 -0
  164. package/transpiled/directives/auth/schemes.js.map +1 -0
  165. package/transpiled/directives/auth/split.d.ts +2 -0
  166. package/transpiled/directives/auth/split.js +38 -0
  167. package/transpiled/directives/auth/split.js.map +1 -0
  168. package/transpiled/directives/auth/types.d.ts +31 -0
  169. package/transpiled/directives/auth/types.js +3 -0
  170. package/transpiled/directives/auth/types.js.map +1 -0
  171. package/transpiled/directives/cache/Control.d.ts +9 -0
  172. package/transpiled/directives/cache/Control.js +42 -0
  173. package/transpiled/directives/cache/Control.js.map +1 -0
  174. package/transpiled/directives/cache/Exact.d.ts +4 -0
  175. package/transpiled/directives/cache/Exact.js +11 -0
  176. package/transpiled/directives/cache/Exact.js.map +1 -0
  177. package/transpiled/directives/cache/Family.d.ts +12 -0
  178. package/transpiled/directives/cache/Family.js +26 -0
  179. package/transpiled/directives/cache/Family.js.map +1 -0
  180. package/transpiled/directives/cache/index.d.ts +2 -0
  181. package/transpiled/directives/cache/index.js +7 -0
  182. package/transpiled/directives/cache/index.js.map +1 -0
  183. package/transpiled/directives/cache/types.d.ts +7 -0
  184. package/transpiled/directives/cache/types.js +3 -0
  185. package/transpiled/directives/cache/types.js.map +1 -0
  186. package/transpiled/directives/dev/Family.d.ts +10 -0
  187. package/transpiled/directives/dev/Family.js +27 -0
  188. package/transpiled/directives/dev/Family.js.map +1 -0
  189. package/transpiled/directives/dev/Stub.d.ts +7 -0
  190. package/transpiled/directives/dev/Stub.js +14 -0
  191. package/transpiled/directives/dev/Stub.js.map +1 -0
  192. package/transpiled/directives/dev/Throw.d.ts +7 -0
  193. package/transpiled/directives/dev/Throw.js +14 -0
  194. package/transpiled/directives/dev/Throw.js.map +1 -0
  195. package/transpiled/directives/dev/index.d.ts +2 -0
  196. package/transpiled/directives/dev/index.js +7 -0
  197. package/transpiled/directives/dev/index.js.map +1 -0
  198. package/transpiled/directives/dev/types.d.ts +4 -0
  199. package/transpiled/directives/dev/types.js +3 -0
  200. package/transpiled/directives/dev/types.js.map +1 -0
  201. package/transpiled/directives/index.d.ts +2 -0
  202. package/transpiled/directives/index.js +12 -0
  203. package/transpiled/directives/index.js.map +1 -0
  204. package/transpiled/directives/octets/Context.d.ts +8 -0
  205. package/transpiled/directives/octets/Context.js +40 -0
  206. package/transpiled/directives/octets/Context.js.map +1 -0
  207. package/transpiled/directives/octets/Delete.d.ts +10 -0
  208. package/transpiled/directives/octets/Delete.js +47 -0
  209. package/transpiled/directives/octets/Delete.js.map +1 -0
  210. package/transpiled/directives/octets/Family.d.ts +12 -0
  211. package/transpiled/directives/octets/Family.js +49 -0
  212. package/transpiled/directives/octets/Family.js.map +1 -0
  213. package/transpiled/directives/octets/Fetch.d.ts +18 -0
  214. package/transpiled/directives/octets/Fetch.js +77 -0
  215. package/transpiled/directives/octets/Fetch.js.map +1 -0
  216. package/transpiled/directives/octets/List.d.ts +10 -0
  217. package/transpiled/directives/octets/List.js +47 -0
  218. package/transpiled/directives/octets/List.js.map +1 -0
  219. package/transpiled/directives/octets/Permute.d.ts +10 -0
  220. package/transpiled/directives/octets/Permute.js +51 -0
  221. package/transpiled/directives/octets/Permute.js.map +1 -0
  222. package/transpiled/directives/octets/Store.d.ts +33 -0
  223. package/transpiled/directives/octets/Store.js +124 -0
  224. package/transpiled/directives/octets/Store.js.map +1 -0
  225. package/transpiled/directives/octets/index.d.ts +2 -0
  226. package/transpiled/directives/octets/index.js +7 -0
  227. package/transpiled/directives/octets/index.js.map +1 -0
  228. package/transpiled/directives/octets/schemas.d.ts +6 -0
  229. package/transpiled/directives/octets/schemas.js +17 -0
  230. package/transpiled/directives/octets/schemas.js.map +1 -0
  231. package/transpiled/directives/octets/types.d.ts +9 -0
  232. package/transpiled/directives/octets/types.js +3 -0
  233. package/transpiled/directives/octets/types.js.map +1 -0
  234. package/transpiled/discovery.d.ts +1 -0
  235. package/transpiled/discovery.js +3 -0
  236. package/transpiled/discovery.js.map +1 -0
  237. package/transpiled/exceptions.d.ts +2 -0
  238. package/transpiled/exceptions.js +39 -0
  239. package/transpiled/exceptions.js.map +1 -0
  240. package/transpiled/index.d.ts +5 -0
  241. package/transpiled/index.js +12 -0
  242. package/transpiled/index.js.map +1 -0
  243. package/transpiled/manifest.d.ts +3 -0
  244. package/transpiled/manifest.js +61 -0
  245. package/transpiled/manifest.js.map +1 -0
  246. package/transpiled/root.d.ts +2 -0
  247. package/transpiled/root.js +39 -0
  248. package/transpiled/root.js.map +1 -0
  249. package/transpiled/schemas.d.ts +3 -0
  250. package/transpiled/schemas.js +14 -0
  251. package/transpiled/schemas.js.map +1 -0
  252. package/transpiled/tsconfig.tsbuildinfo +1 -0
@@ -13,9 +13,8 @@
13
13
 
14
14
  The Authorization is implemented as a set of [RTD Directives](tree.md#directives).
15
15
 
16
- Directives are executed in a predetermined order until one of them grants access to a resource. If
17
- none of the
18
- directives grants access, then the Authorization interrupts request processing and responds with an
16
+ Directives are executed in a predetermined order until one of them grants access to a resource.
17
+ If none of the directives grants access, then the Authorization interrupts request processing and responds with an
19
18
  authorization error.
20
19
 
21
20
  > The Authorization directive provider is named `authorization`,
@@ -0,0 +1,42 @@
1
+ # Caching
2
+
3
+ Directive family `cache` implements the
4
+ HTTP [Cache-Control](https://datatracker.ietf.org/doc/html/rfc2616#section-14.9).
5
+
6
+ ## `cache:control`
7
+
8
+ Sets the value of the `Cache-Control` header
9
+ for [successful responses](https://datatracker.ietf.org/doc/html/rfc2616#section-10.2)
10
+ to [safe HTTP methods](https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP).
11
+
12
+ ```yaml
13
+ /:
14
+ GET:
15
+ cache:control: max-age=60000
16
+ ```
17
+
18
+ ### Implicit modifications
19
+
20
+ In terms of security, the following implicit modifications are made to the `Cache-Control` header:
21
+
22
+ - If it contains the `public` directive without `no-cache` and the request is authenticated,
23
+ the `no-cache` directive is added.
24
+ This is done to prevent the storage of authentication tokens in shared caches.
25
+ - If it does not contain the `private` directive and the request is authenticated, the `private`
26
+ directive is added.
27
+ This is to prevent the storage of private data in shared caches.
28
+
29
+ ## `cache:exact`
30
+
31
+ Same as `cache:control` without implicit modifications.
32
+
33
+ ```yaml
34
+ /:
35
+ GET:
36
+ cache:exact: public, max-age=60000
37
+ ```
38
+
39
+ ## References
40
+
41
+ - HTTP 14.9.1 [What is cacheable](https://datatracker.ietf.org/doc/html/rfc2616#section-14.9.1)
42
+ - See also [features](/extensions/exposition/features/cache.feature)
@@ -114,9 +114,9 @@ created: 1698004822358
114
114
  optimize: null
115
115
  --cut
116
116
  error:
117
- step: resize
118
- code: TOO_SMALL
119
- message: Image is too small
117
+ step: resize
118
+ code: TOO_SMALL
119
+ message: Image is too small
120
120
  --cut--
121
121
  ```
122
122
 
@@ -124,7 +124,7 @@ Intermediate Nodes must not have Methods as they are unreachable.
124
124
 
125
125
  ## Directives
126
126
 
127
- RTD Directives are declared using RTD node or Method keys following the `{provider}:{directive}` pattern and can be used
127
+ RTD Directives are declared using RTD node or Method keys following the `{family}:{directive}` pattern and can be used
128
128
  to add or modify the behavior of request processing. Directive declarations are applied to the RTD node where they are
129
129
  declared and to all nested nodes.
130
130
 
@@ -151,10 +151,6 @@ When it is necessary to avoid directive nesting, a Route can be declared adjacen
151
151
  In this example, the Route `/posts/:user-id/:post-id/` has only the `authorization:role` directive
152
152
  applied.
153
153
 
154
- > Directives can be declared without the `{provider}:` prefix unless there are multiple directives
155
- > with the same name
156
- > across different providers.
157
-
158
154
  Another way to avoid nesting is to declare an _isolated_ Node as follows:
159
155
 
160
156
  ```yaml
@@ -0,0 +1,160 @@
1
+ Feature: Caching
2
+
3
+ Background:
4
+ Given the `identity.basic` database contains:
5
+ # developer:secret
6
+ # user:12345
7
+ | _id | username | password |
8
+ | b70a7dbca6b14a2eaac8a9eb4b2ff4db | developer | $2b$10$ZRSKkgZoGnrcTNA5w5eCcu3pxDzdTduhteVYXcp56AaNcilNkwJ.O |
9
+ Given the `identity.roles` database contains:
10
+ | _id | identity | role |
11
+ | 775a648d054e4ce1a65f8f17e5b51803 | b70a7dbca6b14a2eaac8a9eb4b2ff4db | developer |
12
+
13
+ Scenario: Caching successful response
14
+ Given the annotation:
15
+ """yaml
16
+ /:
17
+ anonymous: true
18
+ GET:
19
+ cache:control: max-age=60000
20
+ dev:stub: hello
21
+ """
22
+ When the following request is received:
23
+ """
24
+ GET / HTTP/1.1
25
+ accept: text/plain
26
+ """
27
+ Then the following reply is sent:
28
+ """
29
+ 200 OK
30
+ content-type: text/plain
31
+ cache-control: max-age=60000
32
+
33
+ hello
34
+ """
35
+
36
+ Scenario: Nested cache directives
37
+ Given the annotation:
38
+ """yaml
39
+ /:
40
+ cache:control: max-age=30000
41
+ GET:
42
+ anonymous: true
43
+ dev:stub: hello
44
+ /foo:
45
+ auth:role: developer
46
+ GET:
47
+ dev:stub: hello
48
+ /bar:
49
+ auth:role: developer
50
+ cache:control: max-age=60000, public
51
+ GET:
52
+ dev:stub: hello
53
+ """
54
+ When the following request is received:
55
+ """
56
+ GET / HTTP/1.1
57
+ accept: text/plain
58
+ """
59
+ Then the following reply is sent:
60
+ """
61
+ 200 OK
62
+ content-type: text/plain
63
+ cache-control: max-age=30000
64
+
65
+ hello
66
+ """
67
+ When the following request is received:
68
+ """
69
+ GET /foo/ HTTP/1.1
70
+ accept: text/plain
71
+ authorization: Basic ZGV2ZWxvcGVyOnNlY3JldA==
72
+ """
73
+ Then the following reply is sent:
74
+ """
75
+ 200 OK
76
+ content-type: text/plain
77
+ cache-control: private, max-age=30000
78
+
79
+ hello
80
+ """
81
+ When the following request is received:
82
+ """
83
+ GET /bar/ HTTP/1.1
84
+ accept: text/plain
85
+ authorization: Basic ZGV2ZWxvcGVyOnNlY3JldA==
86
+ """
87
+ Then the following reply is sent:
88
+ """
89
+ 200 OK
90
+ content-type: text/plain
91
+ cache-control: no-cache, max-age=60000, public
92
+
93
+ hello
94
+ """
95
+ And the reply does not contain:
96
+ """
97
+ cache-control: private, max-age=30000
98
+ """
99
+
100
+ Scenario: Cache-control is not added when request is unsafe
101
+ Given the annotation:
102
+ """yaml
103
+ /:
104
+ anonymous: true
105
+ cache:control: max-age=60000
106
+ POST:
107
+ dev:stub: hello
108
+ """
109
+ When the following request is received:
110
+ """
111
+ POST / HTTP/1.1
112
+ accept: application/yaml
113
+ """
114
+ Then the reply does not contain:
115
+ """
116
+ cache-control: max-age=60000
117
+ """
118
+
119
+ Scenario: Cache-control is added without implicit modifications
120
+ Given the annotation:
121
+ """yaml
122
+ /:
123
+ auth:role: developer
124
+ cache:exact: max-age=60000, public
125
+ GET:
126
+ dev:stub: hello
127
+ """
128
+ When the following request is received:
129
+ """
130
+ GET / HTTP/1.1
131
+ authorization: Basic ZGV2ZWxvcGVyOnNlY3JldA==
132
+ accept: text/plain
133
+
134
+ """
135
+ Then the following reply is sent:
136
+ """
137
+ 200 OK
138
+ content-type: text/plain
139
+ cache-control: max-age=60000, public
140
+
141
+ hello
142
+ """
143
+
144
+ Scenario: Response without caching
145
+ Given the annotation:
146
+ """yaml
147
+ /:
148
+ anonymous: true
149
+ GET:
150
+ dev:stub: hello
151
+ """
152
+ When the following request is received:
153
+ """
154
+ GET / HTTP/1.1
155
+ accept: text/plain
156
+ """
157
+ Then the reply does not contain:
158
+ """
159
+ cache-control:
160
+ """
@@ -4,7 +4,7 @@ import * as http from '@toa.io/http'
4
4
  import { trim } from '@toa.io/generic'
5
5
  import { buffer } from '@toa.io/streams'
6
6
  import { open } from '../../../storages/source/test/util'
7
- import { Parameters } from './parameters'
7
+ import { Parameters } from './Parameters'
8
8
  import { Gateway } from './Gateway'
9
9
 
10
10
  @binding([Gateway, Parameters])
@@ -85,7 +85,13 @@ export class HTTP {
85
85
  const { url, method, headers } = http.parse.request(head)
86
86
  const href = new URL(url, this.origin).href
87
87
  const body = open(filename)
88
- const request = { method, headers, body, duplex: 'half' } as unknown as RequestInit
88
+
89
+ const request = {
90
+ method,
91
+ headers,
92
+ body: body as unknown as ReadableStream,
93
+ duplex: 'half'
94
+ }
89
95
 
90
96
  try {
91
97
  const response = await fetch(href, request)
@@ -8,8 +8,7 @@ export class Parameters {
8
8
  }
9
9
  }
10
10
 
11
- setDefaultTimeout(10 * 1000)
12
-
11
+ setDefaultTimeout(30 * 1000)
13
12
  process.env.TOA_DEV = '1'
14
13
 
15
14
  // { octets: tmp:///exposition-octets }
@@ -1,5 +1,6 @@
1
1
  import { join } from 'node:path'
2
- import { directory } from '@toa.io/filesystem'
2
+ import { tmpdir } from 'node:os'
3
+ import { mkdtemp, copy } from 'fs-extra'
3
4
  import * as yaml from '@toa.io/yaml'
4
5
 
5
6
  export class Workspace {
@@ -10,7 +11,8 @@ export class Workspace {
10
11
  const method = descriptor.value
11
12
 
12
13
  descriptor.value = async function (this: Workspace, ...args: any[]): Promise<any> {
13
- if (this.root === devnull) this.root = await directory.temp()
14
+ if (this.root === devnull) this.root =
15
+ await mkdtemp(join(tmpdir(), Math.random().toString(36).slice(2)))
14
16
 
15
17
  return method.apply(this, args)
16
18
  }
@@ -23,7 +25,7 @@ export class Workspace {
23
25
  const source = join(__dirname, 'components', name)
24
26
  const target = join(this.root, name)
25
27
 
26
- await directory.copy(source, target)
28
+ await copy(source, target)
27
29
 
28
30
  if (patch !== undefined)
29
31
  await this.patchManifest(target, patch)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.exposition",
3
- "version": "0.23.0-dev.0",
3
+ "version": "0.24.0-alpha.0",
4
4
  "description": "Toa Exposition",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -17,11 +17,11 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@toa.io/core": "0.23.0-dev.0",
21
- "@toa.io/generic": "0.23.0-dev.0",
22
- "@toa.io/http": "0.23.0-dev.0",
23
- "@toa.io/schemas": "0.23.0-dev.0",
24
- "@toa.io/streams": "0.23.0-dev.0",
20
+ "@toa.io/core": "0.24.0-alpha.0",
21
+ "@toa.io/generic": "0.24.0-alpha.0",
22
+ "@toa.io/http": "0.24.0-alpha.0",
23
+ "@toa.io/schemas": "0.24.0-alpha.0",
24
+ "@toa.io/streams": "0.24.0-alpha.0",
25
25
  "bcryptjs": "2.4.3",
26
26
  "cors": "2.8.5",
27
27
  "error-value": "0.3.0",
@@ -38,17 +38,20 @@
38
38
  },
39
39
  "scripts": {
40
40
  "test": "jest",
41
- "transpile": "rm -rf transpiled && npx tsc && npm run transpile:basic && npm run transpile:tokens",
41
+ "transpile": "rm -rf transpiled && npx tsc && npm run transpile:basic && npm run transpile:tokens && npm run transpile:roles",
42
42
  "transpile:basic": "rm -rf ./components/identity.basic/operations && npx tsc -p ./components/identity.basic",
43
43
  "transpile:tokens": "rm -rf ./components/identity.tokens/operations && npx tsc -p ./components/identity.tokens",
44
+ "transpile:roles": "rm -rf ./components/identity.roles/operations && npx tsc -p ./components/identity.roles",
44
45
  "features": "npx cucumber-js"
45
46
  },
46
47
  "devDependencies": {
47
- "@toa.io/extensions.storages": "0.23.0-dev.0",
48
+ "@toa.io/extensions.storages": "0.24.0-alpha.0",
48
49
  "@types/bcryptjs": "2.4.3",
49
50
  "@types/cors": "2.8.13",
50
51
  "@types/express": "4.17.17",
51
- "@types/negotiator": "0.6.1"
52
+ "@types/fs-extra": "11.0.4",
53
+ "@types/negotiator": "0.6.1",
54
+ "fs-extra": "11.1.1"
52
55
  },
53
- "gitHead": "19d4af8ff3b8a1a8f191644f44113c88de4bb404"
56
+ "gitHead": "09a88bdf444bc5dba66dbf0005c382f3526f0296"
54
57
  }
package/readme.md CHANGED
@@ -182,4 +182,5 @@ exposition:
182
182
  - [Access authorization](documentation/access.md)
183
183
  - [BLOBs](documentation/octets.md)
184
184
  - [Components and resources](documentation/components.md)
185
+ - [Caching](documentation/cache.md)
185
186
  - [Features](features)
@@ -97,7 +97,9 @@ describe('result', () => {
97
97
  it('should send status code 200 if the result has a value', async () => {
98
98
  const req = createRequest()
99
99
 
100
- server.attach(async (): Promise<OutgoingMessage> => ({ headers: {}, body: generate() }))
100
+ server.attach(async (): Promise<OutgoingMessage> => ({
101
+ headers: new Headers(), body: generate()
102
+ }))
101
103
  await use(req)
102
104
 
103
105
  expect(res.status).toHaveBeenCalledWith(200)
@@ -106,7 +108,7 @@ describe('result', () => {
106
108
  it('should send status code 204 if the result has no value', async () => {
107
109
  const req = createRequest()
108
110
 
109
- server.attach(async (): Promise<OutgoingMessage> => ({ headers: {} }))
111
+ server.attach(async (): Promise<OutgoingMessage> => ({ headers: new Headers() }))
110
112
  await use(req)
111
113
 
112
114
  expect(res.status).toHaveBeenCalledWith(204)
@@ -118,7 +120,7 @@ describe('result', () => {
118
120
  const buf = Buffer.from(json)
119
121
  const req = createRequest({ headers: { accept: 'application/json' } })
120
122
 
121
- server.attach(async (): Promise<OutgoingMessage> => ({ headers: {}, body }))
123
+ server.attach(async (): Promise<OutgoingMessage> => ({ headers: new Headers(), body }))
122
124
  await use(req)
123
125
 
124
126
  expect(res.end).toHaveBeenCalledWith(buf)
@@ -1,3 +1,5 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
1
3
  import express from 'express'
2
4
  import cors from 'cors'
3
5
  import { Connector } from '@toa.io/core'
@@ -22,8 +24,10 @@ export class Server extends Connector {
22
24
  this.debug = debug
23
25
  }
24
26
 
25
- public static create (options: Partial<Properties> = {}): Server {
26
- const properties: Properties = Object.assign({}, defaults(), options)
27
+ public static create (options?: Partial<Properties>): Server {
28
+ const properties: Properties = options === undefined
29
+ ? DEFAULTS
30
+ : { ...DEFAULTS, ...options }
27
31
 
28
32
  const app = express()
29
33
 
@@ -35,7 +39,7 @@ export class Server extends Connector {
35
39
  }
36
40
 
37
41
  public attach (process: Processing): void {
38
- this.app.use((request: any, response: Response): void => {
42
+ this.app.use((request: any, response: Response) => {
39
43
  this.extend(request)
40
44
  .then(process)
41
45
  .then(this.success(request, response))
@@ -84,9 +88,8 @@ export class Server extends Connector {
84
88
  else if (message.body === undefined) status = 204
85
89
  else status = 200
86
90
 
87
- response
88
- .status(status)
89
- .set(message.headers)
91
+ response.status(status)
92
+ message.headers?.forEach((value, key) => response.set(key, value))
90
93
 
91
94
  if (message.body !== undefined && message.body !== null)
92
95
  write(request, response, message)
@@ -96,7 +99,10 @@ export class Server extends Connector {
96
99
  }
97
100
 
98
101
  private fail (request: IncomingMessage, response: Response) {
99
- return (exception: Error) => {
102
+ return async (exception: Error) => {
103
+ if (!request.complete)
104
+ await adam(request)
105
+
100
106
  const status = exception instanceof Exception
101
107
  ? exception.status
102
108
  : 500
@@ -113,10 +119,6 @@ export class Server extends Connector {
113
119
  write(request, response, { body })
114
120
  } else
115
121
  response.end()
116
-
117
- // stop accepting request
118
- if (!request.complete)
119
- request.destroy()
120
122
  }
121
123
  }
122
124
  }
@@ -135,16 +137,26 @@ function negotiate (request: Request): Format | null {
135
137
  return mediaType === undefined ? null : formats[mediaType]
136
138
  }
137
139
 
140
+ // https://github.com/whatwg/fetch/issues/1254
141
+ async function adam (request: Request): Promise<void> {
142
+ const completed = promex()
143
+ const devnull = fs.createWriteStream(os.devNull)
144
+
145
+ request
146
+ .on('end', completed.callback)
147
+ .pipe(devnull)
148
+
149
+ return await completed
150
+ }
151
+
152
+ const DEFAULTS = {
153
+ methods: new Set<string>(['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']),
154
+ debug: false
155
+ }
156
+
138
157
  interface Properties {
139
158
  methods: Set<string>
140
159
  debug: boolean
141
160
  }
142
161
 
143
- function defaults (): Properties {
144
- return {
145
- methods: new Set<string>(['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']),
146
- debug: false
147
- }
148
- }
149
-
150
162
  export type Processing = (input: IncomingMessage) => Promise<OutgoingMessage>
@@ -1,4 +1,4 @@
1
- import { type IncomingHttpHeaders, type OutgoingHttpHeaders } from 'node:http'
1
+ import { type IncomingHttpHeaders } from 'node:http'
2
2
  import { Readable } from 'node:stream'
3
3
  import { type Request, type Response } from 'express'
4
4
  import { buffer } from '@toa.io/streams'
@@ -53,7 +53,7 @@ function send (message: OutgoingMessage, request: IncomingMessage, response: Res
53
53
 
54
54
  function stream
55
55
  (message: OutgoingMessage, request: IncomingMessage, response: Response): void {
56
- const encoded = message.headers !== undefined && 'content-type' in message.headers
56
+ const encoded = message.headers !== undefined && message.headers.has('content-type')
57
57
 
58
58
  if (encoded)
59
59
  pipe(message, response)
@@ -67,7 +67,7 @@ function stream
67
67
  }
68
68
 
69
69
  function pipe (message: OutgoingMessage, response: Response): void {
70
- response.set(message.headers)
70
+ message.headers?.forEach((value, key) => response.set(key, value))
71
71
  message.body.pipe(response)
72
72
  }
73
73
 
@@ -101,7 +101,7 @@ export interface IncomingMessage extends Request {
101
101
 
102
102
  export interface OutgoingMessage {
103
103
  status?: number
104
- headers?: OutgoingHttpHeaders
104
+ headers?: Headers
105
105
  body?: any
106
106
  }
107
107
 
@@ -5,7 +5,8 @@ import {
5
5
  type Route,
6
6
  type Method,
7
7
  type Mapping,
8
- type Directive, type Range
8
+ type Directive,
9
+ type Range
9
10
  } from './types'
10
11
 
11
12
  export function parse (input: object, shortcuts?: Shortcuts): Node {
package/source/Tenant.ts CHANGED
@@ -30,6 +30,11 @@ export class Tenant extends Connector {
30
30
  `'${this.branch.namespace}.${this.branch.component}' has started.`)
31
31
  }
32
32
 
33
+ public override async dispose (): Promise<void> {
34
+ console.info('Exposition Tenant for ' +
35
+ `'${this.branch.namespace}.${this.branch.component}' has been stopped.`)
36
+ }
37
+
33
38
  private async expose (): Promise<void> {
34
39
  await this.broadcast.transmit('expose', this.branch)
35
40
  }
@@ -90,9 +90,9 @@ class Authorization implements Family<Directive, Extension> {
90
90
  const authorization = `Token ${token}`
91
91
 
92
92
  if (response.headers === undefined)
93
- response.headers = {}
93
+ response.headers = new Headers()
94
94
 
95
- response.headers.authorization = authorization
95
+ response.headers.set('authorization', authorization)
96
96
  }
97
97
 
98
98
  private async resolve (authorization: string | undefined): Promise<Identity | null> {
@@ -0,0 +1,59 @@
1
+ import { match } from 'matchacho'
2
+ import type { AuthenticatedRequest, Directive } from './types'
3
+
4
+ export class Control implements Directive {
5
+ protected readonly value: string
6
+ private cache: string | null = null
7
+
8
+ public constructor (value: string) {
9
+ this.value = value
10
+ }
11
+
12
+ public set (request: AuthenticatedRequest, headers: Headers): void {
13
+ if (!['GET', 'HEAD', 'OPTIONS'].includes(request.method))
14
+ return
15
+
16
+ this.cache ??= this.resolve(request)
17
+
18
+ headers.set('cache-control', this.cache)
19
+ }
20
+
21
+ protected resolve (request: AuthenticatedRequest): string {
22
+ if (request.identity === null)
23
+ return this.value
24
+
25
+ const directives = this.mask()
26
+
27
+ if ((directives & (PUBLIC | NO_CACHE)) === PUBLIC)
28
+ return 'no-cache, ' + this.value
29
+
30
+ if ((directives & (PUBLIC | PRIVATE)) === 0)
31
+ return 'private, ' + this.value
32
+
33
+ return this.value
34
+ }
35
+
36
+ private mask (): number {
37
+ const directives = this.value.match(DIRECTIVES_RX)
38
+
39
+ if (directives === null)
40
+ return 0
41
+
42
+ let mask = 0
43
+
44
+ for (const directive of directives)
45
+ mask |= match<number>(directive,
46
+ 'private', PRIVATE,
47
+ 'public', PUBLIC,
48
+ 'no-cache', NO_CACHE,
49
+ 0)
50
+
51
+ return mask
52
+ }
53
+ }
54
+
55
+ const DIRECTIVES_RX = /\b(private|public|no-cache)\b/ig
56
+
57
+ const PUBLIC = 1
58
+ const PRIVATE = 2
59
+ const NO_CACHE = 4
@@ -0,0 +1,7 @@
1
+ import { Control } from './Control'
2
+
3
+ export class Exact extends Control {
4
+ protected override resolve (): string {
5
+ return this.value
6
+ }
7
+ }
@@ -0,0 +1,36 @@
1
+ import { type Input, type Output, type Family } from '../../Directive'
2
+ import { Control } from './Control'
3
+ import { type Directive } from './types'
4
+ import { Exact } from './Exact'
5
+ import type * as http from '../../HTTP'
6
+
7
+ class Cache implements Family<Directive> {
8
+ public readonly name: string = 'cache'
9
+ public readonly mandatory: boolean = false
10
+
11
+ public create (name: string, value: any): Directive {
12
+ const Class = constructors[name]
13
+
14
+ if (Class === undefined)
15
+ throw new Error(`Directive '${name}' is not provided by the '${this.name}' family.`)
16
+
17
+ return new Class(value)
18
+ }
19
+
20
+ public preflight (): Output {
21
+ return null
22
+ }
23
+
24
+ public async settle
25
+ (directives: Directive[], request: Input, response: http.OutgoingMessage): Promise<void> {
26
+ response.headers ??= new Headers()
27
+ directives[0]?.set(request, response.headers)
28
+ }
29
+ }
30
+
31
+ const constructors: Record<string, new (value: any) => Directive> = {
32
+ control: Control,
33
+ exact: Exact
34
+ }
35
+
36
+ export = new Cache()
@@ -0,0 +1,3 @@
1
+ import Family from './Family'
2
+
3
+ export = Family