@toa.io/extensions.exposition 1.0.0-alpha.287 → 1.0.0-alpha.289

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 (277) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/components/identity.basic/tsconfig.tsbuildinfo +1 -1
  3. package/components/identity.clients/tsconfig.tsbuildinfo +1 -1
  4. package/components/identity.credentials/tsconfig.tsbuildinfo +1 -1
  5. package/components/identity.federation/tsconfig.tsbuildinfo +1 -1
  6. package/components/identity.grants/tsconfig.tsbuildinfo +1 -1
  7. package/components/identity.keys/tsconfig.tsbuildinfo +1 -1
  8. package/components/identity.otp/tsconfig.tsbuildinfo +1 -1
  9. package/components/identity.passkeys/manifest.toa.yaml +2 -1
  10. package/components/identity.passkeys/source/errors.test.ts +34 -0
  11. package/components/identity.passkeys/source/webauthn.test.ts +26 -0
  12. package/components/identity.passkeys/tsconfig.tsbuildinfo +1 -1
  13. package/components/identity.passkeys/types/toa.d.ts +1 -1
  14. package/components/identity.roles/tsconfig.tsbuildinfo +1 -1
  15. package/components/identity.tokens/tsconfig.tsbuildinfo +1 -1
  16. package/documentation/access.md +23 -4
  17. package/documentation/discovery.md +81 -0
  18. package/documentation/help.md +122 -0
  19. package/documentation/introspection.md +42 -9
  20. package/documentation/mcp.md +13 -16
  21. package/documentation/octets.md +20 -0
  22. package/features/cors.feature +31 -1
  23. package/features/dev.feature +2 -0
  24. package/features/discovery.feature +213 -0
  25. package/features/discovery.ui.feature +213 -0
  26. package/features/help.feature +512 -0
  27. package/features/introspection.feature +94 -8
  28. package/features/map.feature +1 -0
  29. package/features/mcp.feature +74 -12
  30. package/features/methods.feature +2 -1
  31. package/features/oauth.grants.feature +3 -1
  32. package/features/octets.download.feature +1 -0
  33. package/features/octets.feature +59 -0
  34. package/features/octets.meta.feature +1 -0
  35. package/features/site/_app/immutable/asset.js +1 -0
  36. package/features/site/favicon.ico +0 -0
  37. package/features/site/index.html +10 -0
  38. package/features/steps/Components.ts +4 -2
  39. package/features/steps/Database.ts +7 -1
  40. package/features/steps/Gateway.ts +23 -21
  41. package/features/steps/Parameters.ts +37 -16
  42. package/features/steps/Realtime.ts +3 -3
  43. package/package.json +7 -5
  44. package/readme.md +3 -0
  45. package/source/Directive.ts +4 -11
  46. package/source/Discovery/Explorer.ts +34 -0
  47. package/source/Discovery/Site.test.ts +147 -0
  48. package/source/Discovery/Site.ts +202 -0
  49. package/source/Discovery/index.ts +3 -0
  50. package/source/Discovery/tree.test.ts +175 -0
  51. package/source/Discovery/tree.ts +77 -0
  52. package/source/Discovery/trunk.ts +33 -0
  53. package/source/Endpoint.ts +4 -0
  54. package/source/Factory.ts +3 -86
  55. package/source/Gateway.ts +34 -4
  56. package/source/HTTP/Context.ts +7 -0
  57. package/source/HTTP/Probe.ts +0 -11
  58. package/source/HTTP/Server.ts +2 -3
  59. package/source/HTTP/messages.ts +4 -2
  60. package/source/Introspection.ts +58 -7
  61. package/source/MCP/schema.ts +16 -5
  62. package/source/MCP/tools.ts +14 -32
  63. package/source/Mapping.ts +5 -0
  64. package/source/Query.ts +17 -2
  65. package/source/RTD/Directives.ts +10 -0
  66. package/source/RTD/Endpoint.ts +8 -1
  67. package/source/RTD/Node.ts +20 -6
  68. package/source/RTD/Tree.ts +19 -9
  69. package/source/RTD/factory.ts +37 -1
  70. package/source/RTD/segment.ts +21 -0
  71. package/source/const.ts +26 -0
  72. package/source/deployment.ts +2 -2
  73. package/source/directives/auth/Anonymous.test.ts +31 -5
  74. package/source/directives/auth/Anonymous.ts +15 -3
  75. package/source/directives/auth/Anyone.ts +6 -0
  76. package/source/directives/auth/Assert.ts +5 -0
  77. package/source/directives/auth/Delegate.ts +5 -2
  78. package/source/directives/auth/Federation.ts +11 -0
  79. package/source/directives/auth/Id.ts +15 -0
  80. package/source/directives/auth/Input.ts +5 -0
  81. package/source/directives/auth/Role.ts +17 -0
  82. package/source/directives/cors/CORS.test.ts +75 -0
  83. package/source/directives/cors/CORS.ts +10 -3
  84. package/source/directives/help/Family.ts +122 -0
  85. package/source/directives/help/Help.test.ts +227 -0
  86. package/source/directives/help/Help.ts +47 -0
  87. package/source/directives/help/Parameters.ts +47 -0
  88. package/source/directives/help/described.ts +57 -0
  89. package/source/directives/help/index.ts +45 -0
  90. package/source/directives/index.ts +8 -1
  91. package/source/directives/map/Headers.ts +6 -6
  92. package/source/directives/mcp/MCP.ts +12 -21
  93. package/source/directives/mcp/Tool.test.ts +22 -68
  94. package/source/directives/mcp/Tool.ts +14 -41
  95. package/source/directives/octets/Directive.ts +7 -0
  96. package/source/directives/octets/Octets.ts +8 -0
  97. package/source/directives/octets/Put.ts +10 -0
  98. package/source/manifest.ts +1 -1
  99. package/source/root.ts +2 -2
  100. package/source/service.ts +94 -0
  101. package/source/shortcuts.ts +14 -0
  102. package/transpiled/Directive.d.ts +1 -1
  103. package/transpiled/Directive.js +3 -11
  104. package/transpiled/Directive.js.map +1 -1
  105. package/transpiled/Discovery/Explorer.d.ts +11 -0
  106. package/transpiled/Discovery/Explorer.js +29 -0
  107. package/transpiled/Discovery/Explorer.js.map +1 -0
  108. package/transpiled/Discovery/Site.d.ts +43 -0
  109. package/transpiled/Discovery/Site.js +171 -0
  110. package/transpiled/Discovery/Site.js.map +1 -0
  111. package/transpiled/Discovery/index.d.ts +3 -0
  112. package/transpiled/Discovery/index.js +4 -0
  113. package/transpiled/Discovery/index.js.map +1 -0
  114. package/transpiled/Discovery/tree.d.ts +26 -0
  115. package/transpiled/Discovery/tree.js +50 -0
  116. package/transpiled/Discovery/tree.js.map +1 -0
  117. package/transpiled/Discovery/trunk.d.ts +13 -0
  118. package/transpiled/Discovery/trunk.js +29 -0
  119. package/transpiled/Discovery/trunk.js.map +1 -0
  120. package/transpiled/Endpoint.d.ts +2 -1
  121. package/transpiled/Endpoint.js +3 -0
  122. package/transpiled/Endpoint.js.map +1 -1
  123. package/transpiled/Factory.js +3 -61
  124. package/transpiled/Factory.js.map +1 -1
  125. package/transpiled/Gateway.d.ts +4 -0
  126. package/transpiled/Gateway.js +29 -4
  127. package/transpiled/Gateway.js.map +1 -1
  128. package/transpiled/HTTP/Context.d.ts +6 -0
  129. package/transpiled/HTTP/Context.js +6 -0
  130. package/transpiled/HTTP/Context.js.map +1 -1
  131. package/transpiled/HTTP/Probe.d.ts +0 -10
  132. package/transpiled/HTTP/Probe.js +0 -10
  133. package/transpiled/HTTP/Probe.js.map +1 -1
  134. package/transpiled/HTTP/Server.d.ts +0 -1
  135. package/transpiled/HTTP/Server.js +2 -2
  136. package/transpiled/HTTP/Server.js.map +1 -1
  137. package/transpiled/HTTP/messages.js +4 -2
  138. package/transpiled/HTTP/messages.js.map +1 -1
  139. package/transpiled/Introspection.d.ts +38 -6
  140. package/transpiled/Introspection.js +18 -1
  141. package/transpiled/Introspection.js.map +1 -1
  142. package/transpiled/MCP/schema.d.ts +2 -2
  143. package/transpiled/MCP/schema.js +11 -5
  144. package/transpiled/MCP/schema.js.map +1 -1
  145. package/transpiled/MCP/tools.d.ts +1 -1
  146. package/transpiled/MCP/tools.js +12 -28
  147. package/transpiled/MCP/tools.js.map +1 -1
  148. package/transpiled/Mapping.d.ts +2 -0
  149. package/transpiled/Mapping.js +4 -0
  150. package/transpiled/Mapping.js.map +1 -1
  151. package/transpiled/Query.d.ts +12 -0
  152. package/transpiled/Query.js +16 -2
  153. package/transpiled/Query.js.map +1 -1
  154. package/transpiled/RTD/Directives.d.ts +8 -0
  155. package/transpiled/RTD/Endpoint.d.ts +7 -1
  156. package/transpiled/RTD/Node.d.ts +11 -2
  157. package/transpiled/RTD/Node.js +12 -2
  158. package/transpiled/RTD/Node.js.map +1 -1
  159. package/transpiled/RTD/Tree.js +15 -8
  160. package/transpiled/RTD/Tree.js.map +1 -1
  161. package/transpiled/RTD/factory.js +27 -1
  162. package/transpiled/RTD/factory.js.map +1 -1
  163. package/transpiled/RTD/segment.d.ts +7 -0
  164. package/transpiled/RTD/segment.js +17 -0
  165. package/transpiled/RTD/segment.js.map +1 -1
  166. package/transpiled/const.d.ts +21 -0
  167. package/transpiled/const.js +21 -0
  168. package/transpiled/const.js.map +1 -1
  169. package/transpiled/deployment.js +2 -2
  170. package/transpiled/deployment.js.map +1 -1
  171. package/transpiled/directives/auth/Anonymous.d.ts +11 -2
  172. package/transpiled/directives/auth/Anonymous.js +13 -3
  173. package/transpiled/directives/auth/Anonymous.js.map +1 -1
  174. package/transpiled/directives/auth/Anyone.d.ts +3 -0
  175. package/transpiled/directives/auth/Anyone.js +4 -0
  176. package/transpiled/directives/auth/Anyone.js.map +1 -1
  177. package/transpiled/directives/auth/Assert.d.ts +2 -0
  178. package/transpiled/directives/auth/Assert.js +4 -0
  179. package/transpiled/directives/auth/Assert.js.map +1 -1
  180. package/transpiled/directives/auth/Delegate.d.ts +4 -1
  181. package/transpiled/directives/auth/Delegate.js +5 -2
  182. package/transpiled/directives/auth/Delegate.js.map +1 -1
  183. package/transpiled/directives/auth/Federation.d.ts +5 -0
  184. package/transpiled/directives/auth/Federation.js +8 -0
  185. package/transpiled/directives/auth/Federation.js.map +1 -1
  186. package/transpiled/directives/auth/Id.d.ts +9 -0
  187. package/transpiled/directives/auth/Id.js +12 -0
  188. package/transpiled/directives/auth/Id.js.map +1 -1
  189. package/transpiled/directives/auth/Input.d.ts +2 -0
  190. package/transpiled/directives/auth/Input.js +4 -0
  191. package/transpiled/directives/auth/Input.js.map +1 -1
  192. package/transpiled/directives/auth/Role.d.ts +7 -0
  193. package/transpiled/directives/auth/Role.js +11 -0
  194. package/transpiled/directives/auth/Role.js.map +1 -1
  195. package/transpiled/directives/cors/CORS.js +9 -2
  196. package/transpiled/directives/cors/CORS.js.map +1 -1
  197. package/transpiled/directives/help/Family.d.ts +28 -0
  198. package/transpiled/directives/help/Family.js +83 -0
  199. package/transpiled/directives/help/Family.js.map +1 -0
  200. package/transpiled/directives/help/Help.d.ts +22 -0
  201. package/transpiled/directives/help/Help.js +36 -0
  202. package/transpiled/directives/help/Help.js.map +1 -0
  203. package/transpiled/directives/help/Parameters.d.ts +18 -0
  204. package/transpiled/directives/help/Parameters.js +34 -0
  205. package/transpiled/directives/help/Parameters.js.map +1 -0
  206. package/transpiled/directives/help/described.d.ts +16 -0
  207. package/transpiled/directives/help/described.js +23 -0
  208. package/transpiled/directives/help/described.js.map +1 -0
  209. package/transpiled/directives/help/index.d.ts +22 -0
  210. package/transpiled/directives/help/index.js +34 -0
  211. package/transpiled/directives/help/index.js.map +1 -0
  212. package/transpiled/directives/index.d.ts +4 -0
  213. package/transpiled/directives/index.js +8 -1
  214. package/transpiled/directives/index.js.map +1 -1
  215. package/transpiled/directives/map/Headers.d.ts +5 -0
  216. package/transpiled/directives/map/Headers.js +7 -5
  217. package/transpiled/directives/map/Headers.js.map +1 -1
  218. package/transpiled/directives/mcp/MCP.d.ts +8 -5
  219. package/transpiled/directives/mcp/MCP.js +9 -13
  220. package/transpiled/directives/mcp/MCP.js.map +1 -1
  221. package/transpiled/directives/mcp/Tool.d.ts +6 -12
  222. package/transpiled/directives/mcp/Tool.js +10 -23
  223. package/transpiled/directives/mcp/Tool.js.map +1 -1
  224. package/transpiled/directives/octets/Directive.d.ts +6 -0
  225. package/transpiled/directives/octets/Directive.js.map +1 -1
  226. package/transpiled/directives/octets/Octets.d.ts +3 -0
  227. package/transpiled/directives/octets/Octets.js +6 -0
  228. package/transpiled/directives/octets/Octets.js.map +1 -1
  229. package/transpiled/directives/octets/Put.d.ts +3 -0
  230. package/transpiled/directives/octets/Put.js +8 -0
  231. package/transpiled/directives/octets/Put.js.map +1 -1
  232. package/transpiled/manifest.js +1 -1
  233. package/transpiled/root.js +2 -2
  234. package/transpiled/root.js.map +1 -1
  235. package/transpiled/service.d.ts +7 -0
  236. package/transpiled/service.js +74 -0
  237. package/transpiled/service.js.map +1 -0
  238. package/transpiled/shortcuts.d.ts +3 -0
  239. package/transpiled/shortcuts.js +13 -0
  240. package/transpiled/shortcuts.js.map +1 -0
  241. package/tsconfig.tsbuildinfo +1 -1
  242. package/ui/dist/_app/env.js +1 -0
  243. package/ui/dist/_app/immutable/assets/0.Bl7vLmIK.css +2 -0
  244. package/ui/dist/_app/immutable/assets/2.BC0-Jhqu.css +1 -0
  245. package/ui/dist/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
  246. package/ui/dist/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
  247. package/ui/dist/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
  248. package/ui/dist/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
  249. package/ui/dist/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
  250. package/ui/dist/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
  251. package/ui/dist/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
  252. package/ui/dist/_app/immutable/chunks/Bjy-W4x2.js +81 -0
  253. package/ui/dist/_app/immutable/chunks/CDMRPWCx.js +6 -0
  254. package/ui/dist/_app/immutable/chunks/DJTKIUs8.js +1 -0
  255. package/ui/dist/_app/immutable/chunks/DjIYL2zC.js +3 -0
  256. package/ui/dist/_app/immutable/chunks/xihTtKlq.js +1 -0
  257. package/ui/dist/_app/immutable/chunks/z-DVUbQC.js +1 -0
  258. package/ui/dist/_app/immutable/entry/app.5S2Jtv46.js +2 -0
  259. package/ui/dist/_app/immutable/entry/start.Bqi6IBmH.js +1 -0
  260. package/ui/dist/_app/immutable/nodes/0.D4LwNilH.js +1 -0
  261. package/ui/dist/_app/immutable/nodes/1.CSRya9So.js +1 -0
  262. package/ui/dist/_app/immutable/nodes/2.BtiGmR6I.js +14 -0
  263. package/ui/dist/_app/version.json +1 -0
  264. package/ui/dist/apple-touch-icon.png +0 -0
  265. package/ui/dist/favicon-96x96.png +0 -0
  266. package/ui/dist/favicon.ico +0 -0
  267. package/ui/dist/index.html +66 -0
  268. package/components/identity.bans/migrations/0001-system-properties.yaml +0 -14
  269. package/components/identity.basic/migrations/0001-system-properties.yaml +0 -14
  270. package/components/identity.clients/migrations/0001-system-properties.yaml +0 -14
  271. package/components/identity.federation/migrations/0001-system-properties.yaml +0 -14
  272. package/components/identity.grants/migrations/0001-system-properties.yaml +0 -14
  273. package/components/identity.keys/migrations/0001-system-properties.yaml +0 -14
  274. package/components/identity.otp/migrations/0001-system-properties.yaml +0 -14
  275. package/components/identity.passkeys/migrations/0001-system-properties.yaml +0 -14
  276. package/components/identity.roles/migrations/0001-system-properties.yaml +0 -14
  277. package/components/identity.tokens/migrations/0001-system-properties.yaml +0 -14
@@ -62,7 +62,12 @@ Feature: Introspection
62
62
  required:
63
63
  - title
64
64
  - volume
65
- output: {}
65
+ output:
66
+ type: object
67
+ properties:
68
+ title:
69
+ type: string
70
+ maxLength: 64
66
71
  errors:
67
72
  - NO_WAY
68
73
  - WONT_CREATE
@@ -79,7 +84,8 @@ Feature: Introspection
79
84
  /:
80
85
  io:output: true
81
86
  GET:
82
- mcp:tool: Every pot there is, newest first.
87
+ help:method:
88
+ description: Every pot there is, newest first.
83
89
  endpoint: enumerate
84
90
  POST: create
85
91
  """
@@ -214,6 +220,85 @@ Feature: Introspection
214
220
  POST:
215
221
  """
216
222
 
223
+ Scenario: A credential does not hide what is anonymous
224
+ A credential refuses an `anonymous` route because it would make the reply uncacheable,
225
+ and a description is not that reply — it is what the resource is, and it varies by who
226
+ asked in any case. Without this an identity would be told less than someone presenting
227
+ nothing at all.
228
+
229
+ Given the `pots` is running with the following manifest:
230
+ """yaml
231
+ exposition:
232
+ /:
233
+ isolated: true
234
+ anonymous: true
235
+ io:output: [id]
236
+ GET: enumerate
237
+ """
238
+ When the following request is received:
239
+ """
240
+ OPTIONS /pots/ HTTP/1.1
241
+ host: nex.toa.io
242
+ authorization: Basic ZGV2ZWxvcGVyOnNlY3JldA==
243
+ accept: application/yaml
244
+ """
245
+ Then the following reply is sent:
246
+ """
247
+ 200 OK
248
+ Allow: GET
249
+ """
250
+
251
+ Scenario: A method that takes an identity is not described to nobody
252
+ Which identity it is about cannot be told from a description, which has no route
253
+ variable to read. That there must be one can, and a caller with none is refused
254
+ whatever the value would have been — so they are not shown it.
255
+
256
+ Given the `identity.basic` database contains:
257
+ # developer:secret
258
+ | _id | authority | username | password |
259
+ | efe3a65ebbee47ed95a73edd911ea328 | nex | developer | $2b$10$ZRSKkgZoGnrcTNA5w5eCcu3pxDzdTduhteVYXcp56AaNcilNkwJ.O |
260
+ And the `identity.bans` database is empty
261
+ And the `pots` is running with the following manifest:
262
+ """yaml
263
+ exposition:
264
+ /:id:
265
+ isolated: true
266
+ io:output: [id]
267
+ GET:
268
+ anonymous: true
269
+ endpoint: observe
270
+ PATCH:
271
+ auth:id: id
272
+ endpoint: assign
273
+ """
274
+ When the following request is received:
275
+ """
276
+ OPTIONS /pots/:id/ HTTP/1.1
277
+ host: nex.toa.io
278
+ accept: application/yaml
279
+ """
280
+ Then the following reply is sent:
281
+ """
282
+ 200 OK
283
+ Allow: GET
284
+ """
285
+ And the reply does not contain:
286
+ """
287
+ PATCH:
288
+ """
289
+ When the following request is received:
290
+ """
291
+ OPTIONS /pots/:id/ HTTP/1.1
292
+ host: nex.toa.io
293
+ authorization: Basic ZGV2ZWxvcGVyOnNlY3JldA==
294
+ accept: application/yaml
295
+ """
296
+ Then the following reply is sent:
297
+ """
298
+ 200 OK
299
+ Allow: GET, PATCH
300
+ """
301
+
217
302
  Scenario: A resource the caller cannot reach at all
218
303
  Given the `pots` is running with the following manifest:
219
304
  """yaml
@@ -274,8 +359,9 @@ Feature: Introspection
274
359
  """
275
360
 
276
361
  Scenario: A property a header carries
277
- `map:headers` fills the property from the request, so it is not the body's to send —
278
- and the caller is told where it does go.
362
+ `map:headers` fills the property from the request, so it is not the body's to send.
363
+ Which header it reads is not answered: what an application takes off a request is its
364
+ own, and a caller has nowhere to put one.
279
365
 
280
366
  Given the `echo` is running with the following manifest:
281
367
  """yaml
@@ -299,16 +385,16 @@ Feature: Introspection
299
385
  Allow: PATCH
300
386
 
301
387
  PATCH:
302
- headers:
303
- a:
304
- type: string
305
- header: x-first
306
388
  input:
307
389
  type: object
308
390
  properties:
309
391
  b:
310
392
  type: string
311
393
  """
394
+ And the reply does not contain:
395
+ """
396
+ x-first
397
+ """
312
398
 
313
399
  Scenario: A property a segment carries under another name
314
400
  `map:segments` names the property differently from the segment it reads, so the route
@@ -78,6 +78,7 @@ Feature: HTTP context mapping
78
78
  When the following request is received:
79
79
  """
80
80
  OPTIONS / HTTP/1.1
81
+ access-control-request-method: GET
81
82
  host: nex.toa.io
82
83
  origin: https://example.com
83
84
  """
@@ -1,11 +1,12 @@
1
1
  @security
2
2
  Feature: Model Context Protocol
3
3
 
4
- A tool is a procedure a method is published as. A method says so with `mcp:tool`, whose
5
- value is what the tool is, and a default denies: a tree holds everything an application
6
- serves and most of it is machinery a model has no business reading. What a tool takes and
7
- answers is what the method says of itself, and what a caller may do with it is what `auth`
8
- says.
4
+ A tool is a procedure a method is published as. A method says so with `mcp:tool`, and a
5
+ default denies: a tree holds everything an application serves and most of it is machinery
6
+ a model has no business reading. What the tool is called and what it is for is
7
+ `help:method`, which is what describes the method everywhere else as well. What a tool
8
+ takes and answers is what the method says of itself, and what a caller may do with it is
9
+ what `auth` says.
9
10
 
10
11
  What a tool is, is stated on the route and nowhere else. The operation states what it is
11
12
  too, and that is not this: one operation mounted on two routes is two tools. A `title`
@@ -30,20 +31,27 @@ Feature: Model Context Protocol
30
31
  /:
31
32
  io:output: [id, title, volume]
32
33
  GET:
33
- mcp:tool: All the pots, newest first.
34
+ mcp:tool: true
35
+ help:method:
36
+ description: All the pots, newest first.
34
37
  endpoint: enumerate
35
38
  POST:
36
- mcp:tool: Start a new pot brewing.
39
+ mcp:tool: true
40
+ help:method:
41
+ description: Start a new pot brewing.
37
42
  endpoint: create
38
43
  /:id:
39
44
  GET:
40
- mcp:tool: One pot, by its id.
45
+ mcp:tool: true
46
+ help:method:
47
+ description: One pot, by its id.
41
48
  endpoint: observe
42
49
  /large:
43
50
  GET:
44
51
  query:
45
52
  criteria: volume>=100
46
- mcp:tool:
53
+ mcp:tool: true
54
+ help:method:
47
55
  title: Large pots
48
56
  description: The pots that hold a hundred or more, newest first.
49
57
  endpoint: enumerate
@@ -53,7 +61,9 @@ Feature: Model Context Protocol
53
61
  isolated: true
54
62
  GET:
55
63
  auth:role: admin
56
- mcp:tool: Every pot there is, for whoever may see them all.
64
+ mcp:tool: true
65
+ help:method:
66
+ description: Every pot there is, for whoever may see them all.
57
67
  endpoint: enumerate
58
68
  """
59
69
  And the `pots` database contains:
@@ -256,6 +266,54 @@ Feature: Model Context Protocol
256
266
  code: -32601
257
267
  """
258
268
 
269
+ Scenario: A tool that takes an identity is not listed to nobody
270
+ `tools/list` answers what this caller may reach, and it is the description that decides
271
+ — the same one `OPTIONS` answers. A model is not shown a tool it would be refused.
272
+
273
+ Given the annotation:
274
+ """yaml
275
+ mcp:
276
+ name: Pots
277
+ anonymous: true
278
+ /:
279
+ anonymous: true
280
+ /pots/:id:
281
+ isolated: true
282
+ GET:
283
+ anonymous: true
284
+ mcp:tool: true
285
+ help:method: One pot, by its id.
286
+ dev:stub: []
287
+ PATCH:
288
+ auth:id: id
289
+ mcp:tool: true
290
+ help:method: Change your own pot.
291
+ dev:stub: []
292
+ """
293
+ When the following request is received:
294
+ """
295
+ POST /.mcp HTTP/1.1
296
+ host: nex.toa.io
297
+ accept: application/yaml
298
+ content-type: application/json
299
+ mcp-protocol-version: 2026-07-28
300
+ mcp-method: tools/list
301
+
302
+ {"jsonrpc": "2.0", "id": 3, "method": "tools/list",
303
+ "params": {"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
304
+ "io.modelcontextprotocol/clientCapabilities": {}}}}
305
+ """
306
+ Then the following reply is sent:
307
+ """
308
+ 200 OK
309
+
310
+ - name: pots._id.GET
311
+ """
312
+ And the reply does not contain:
313
+ """
314
+ pots._id.PATCH
315
+ """
316
+
259
317
  Scenario: A credential does not hide an anonymous tool
260
318
  A client here always presents one, and what refuses a credentialed request at an
261
319
  `anonymous` route is that the reply would not be cacheable — which a tool's is not.
@@ -563,7 +621,9 @@ Feature: Model Context Protocol
563
621
  /:
564
622
  io:output: true
565
623
  GET:
566
- mcp:tool: Refuses, with what it was given.
624
+ mcp:tool: true
625
+ help:method:
626
+ description: Refuses, with what it was given.
567
627
  endpoint: error
568
628
  """
569
629
  When the following request is received:
@@ -605,7 +665,9 @@ Feature: Model Context Protocol
605
665
  /:
606
666
  io:output: true
607
667
  GET:
608
- mcp:tool: Answers with what it was given.
668
+ mcp:tool: true
669
+ help:method:
670
+ description: Answers with what it was given.
609
671
  endpoint: echo
610
672
  """
611
673
  When the following request is received:
@@ -37,11 +37,12 @@ Feature: Supported methods
37
37
  When the following request is received:
38
38
  """
39
39
  OPTIONS / HTTP/1.1
40
+ access-control-request-method: GET
40
41
  host: nex.toa.io
41
42
  origin: https://hello.world
42
43
  """
43
44
  Then the following reply is sent:
44
45
  """
45
46
  204 No Content
46
- access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, LOCK, UNLOCK
47
+ access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, LOCK, UNLOCK, OPTIONS
47
48
  """
@@ -26,7 +26,9 @@ Feature: Authorization code flow
26
26
  /pots:
27
27
  anyone: true
28
28
  GET:
29
- mcp:tool: Every pot there is.
29
+ mcp:tool: true
30
+ help:method:
31
+ description: Every pot there is.
30
32
  dev:stub: Kettles and teapots.
31
33
  """
32
34
  When the following request is received:
@@ -183,6 +183,7 @@ Feature: Download external resources
183
183
  When the following request is received:
184
184
  """
185
185
  OPTIONS / HTTP/1.1
186
+ access-control-request-method: GET
186
187
  host: nex.toa.io
187
188
  origin: https://hello.world
188
189
  """
@@ -245,3 +245,62 @@ Feature: Octets directive family
245
245
 
246
246
  Trailing slash is redundant
247
247
  """
248
+
249
+ Scenario: What sending a file takes
250
+ A file is not a value, so no schema states what may be sent — `octets:put` does, and a
251
+ client reads it where it reads everything else about the method.
252
+
253
+ When the following request is received:
254
+ """
255
+ OPTIONS /media/jpeg-or-png/ HTTP/1.1
256
+ host: nex.toa.io
257
+ accept: application/yaml
258
+ """
259
+ Then the following reply is sent:
260
+ """
261
+ 200 OK
262
+ Allow: POST
263
+
264
+ POST:
265
+ octets:
266
+ accept: image/jpeg,image/png
267
+ limit: 64MiB
268
+ """
269
+
270
+ Scenario: A limit is what a refusal reports
271
+ What a larger body is refused by is said the way the refusal says it.
272
+
273
+ When the following request is received:
274
+ """
275
+ OPTIONS /limit-1kb/ HTTP/1.1
276
+ host: nex.toa.io
277
+ accept: application/yaml
278
+ """
279
+ Then the following reply is sent:
280
+ """
281
+ 200 OK
282
+
283
+ POST:
284
+ octets:
285
+ limit: 1kb
286
+ """
287
+
288
+ Scenario: Reading a file takes nothing to say
289
+ Only sending one says anything a schema does not; `octets:get` and `octets:delete` are
290
+ the request they look like.
291
+
292
+ When the following request is received:
293
+ """
294
+ OPTIONS /media/images/whatever/ HTTP/1.1
295
+ host: nex.toa.io
296
+ accept: application/yaml
297
+ """
298
+ Then the following reply is sent:
299
+ """
300
+ 200 OK
301
+ Allow: GET
302
+ """
303
+ And the reply does not contain:
304
+ """
305
+ octets:
306
+ """
@@ -105,6 +105,7 @@ Feature: Octets metadata
105
105
  When the following request is received:
106
106
  """
107
107
  OPTIONS / HTTP/1.1
108
+ access-control-request-method: GET
108
109
  host: nex.toa.io
109
110
  origin: https://example.com
110
111
  """
@@ -0,0 +1 @@
1
+ export const hashed = true
Binary file
@@ -0,0 +1,10 @@
1
+ <!doctype html>
2
+ <!-- stands in for what `ui` builds; the scenarios are about the server, not the page -->
3
+ <html lang="en">
4
+ <head>
5
+ <title>fixture</title>
6
+ </head>
7
+ <body>
8
+ the page
9
+ </body>
10
+ </html>
@@ -5,6 +5,8 @@ import tsflow from 'cucumber-tsflow'
5
5
 
6
6
  import * as boot from '@toa.io/boot'
7
7
  import { type Connector } from '@toa.io/core'
8
+ import { environment } from '@toa.io/generic'
9
+
8
10
  import { load as parse } from 'js-yaml'
9
11
  import { Gateway } from './Gateway.js'
10
12
  import { Workspace } from './Workspace.js'
@@ -68,7 +70,7 @@ export class Components {
68
70
  /** What the deployment would tell the values service, as the scenario needs it. */
69
71
  @given('the configuration values are deployed:')
70
72
  public deployValues(yaml: string): void {
71
- process.env.TOA_CONFIGURATION_VALUES = JSON.stringify(parse(yaml))
73
+ environment.set('TOA_CONFIGURATION_VALUES', JSON.stringify(parse(yaml)))
72
74
  }
73
75
 
74
76
  @given('the `{word}` is stopped')
@@ -87,7 +89,7 @@ export class Components {
87
89
 
88
90
  await Promise.all(promises)
89
91
 
90
- delete process.env.TOA_CONFIGURATION_VALUES
92
+ environment.delete('TOA_CONFIGURATION_VALUES')
91
93
  }
92
94
 
93
95
  private async runComponent(name: string, manifest?: object): Promise<void> {
@@ -45,6 +45,10 @@ export class Database {
45
45
  document.CREATED ??= Date.now()
46
46
  document.UPDATED ??= document.CREATED
47
47
 
48
+ // a table states a timestamp as the entity carries it; the storage holds it as a date
49
+ for (const name of TIMESTAMPS)
50
+ if (typeof document[name] === 'number') document[name] = new Date(document[name])
51
+
48
52
  documents.push(document)
49
53
  }
50
54
 
@@ -96,5 +100,7 @@ export class Database {
96
100
  }
97
101
  }
98
102
 
103
+ const TIMESTAMPS = ['CREATED', 'UPDATED', 'DELETED']
104
+
99
105
  /** Every fixture table names an `_id`, which is what a row is replaced by. */
100
- type Document = Record<string, string | number | boolean | null> & { _id: string }
106
+ type Document = Record<string, string | number | boolean | Date | null> & { _id: string }
@@ -2,10 +2,12 @@ import tsflow from 'cucumber-tsflow'
2
2
 
3
3
  import * as boot from '@toa.io/boot'
4
4
  import { type Connector } from '@toa.io/core'
5
+ import { environment } from '@toa.io/generic'
6
+
5
7
  import { load as parse } from 'js-yaml'
6
8
  import { Factory } from '../../source/index.js'
7
9
  import * as syntax from '../../source/RTD/syntax/index.js'
8
- import { shortcuts } from '../../source/Directive.js'
10
+ import { shortcuts } from '../../source/shortcuts.js'
9
11
  import { manifests } from './map.js'
10
12
  import { PORT, PROBE } from './Parameters.js'
11
13
  import type * as http from '../../source/HTTP/index.js'
@@ -26,7 +28,7 @@ export class Gateway {
26
28
  if (annotation['/'] !== undefined) {
27
29
  const tree = syntax.parse(annotation['/'], shortcuts)
28
30
 
29
- process.env.TOA_EXPOSITION = JSON.stringify(tree)
31
+ environment.set('TOA_EXPOSITION', JSON.stringify(tree))
30
32
  }
31
33
 
32
34
  const { debug, authorities, bouncer, ip, oauth, rpc, mcp } = annotation
@@ -46,7 +48,7 @@ export class Gateway {
46
48
 
47
49
  if (mcp !== undefined) properties.mcp = mcp
48
50
 
49
- process.env.TOA_EXPOSITION_PROPERTIES = JSON.stringify(properties)
51
+ environment.set('TOA_EXPOSITION_PROPERTIES', JSON.stringify(properties))
50
52
  }
51
53
 
52
54
  /**
@@ -75,8 +77,8 @@ export class Gateway {
75
77
  tree.routes.push(...node.routes)
76
78
  }
77
79
 
78
- process.env.TOA_EXPOSITION = JSON.stringify(tree)
79
- process.env.TOA_EXPOSITION_PROPERTIES = JSON.stringify(DEFAULT_PROPERTIES)
80
+ environment.set('TOA_EXPOSITION', JSON.stringify(tree))
81
+ environment.set('TOA_EXPOSITION_PROPERTIES', JSON.stringify(DEFAULT_PROPERTIES))
80
82
  }
81
83
 
82
84
  @given('the `{word}` configuration:')
@@ -90,7 +92,7 @@ export class Gateway {
90
92
  // scenario-scoped, as the secrets it may refer to are: a configuration left behind
91
93
  // outlives the secrets it points at, and the next scenario cannot resolve them
92
94
  this.written.push(key)
93
- process.env[key] = JSON.stringify(configuration)
95
+ environment.set(key, JSON.stringify(configuration))
94
96
  }
95
97
 
96
98
  /** The secrets a scenario's configuration refers to. */
@@ -100,7 +102,7 @@ export class Gateway {
100
102
 
101
103
  for (const [name, value] of Object.entries(secrets)) {
102
104
  this.written.push(SECRET + name)
103
- process.env[SECRET + name] = value
105
+ environment.set(SECRET + name, value)
104
106
  }
105
107
  }
106
108
 
@@ -111,8 +113,11 @@ export class Gateway {
111
113
 
112
114
  @given('the Gateway is running')
113
115
  public async start(): Promise<void> {
114
- process.env.TOA_EXPOSITION ??= DEFAULT_TREE
115
- process.env.TOA_EXPOSITION_PROPERTIES ??= JSON.stringify(DEFAULT_PROPERTIES)
116
+ if (!environment.has('TOA_EXPOSITION'))
117
+ environment.set('TOA_EXPOSITION', DEFAULT_TREE)
118
+
119
+ if (!environment.has('TOA_EXPOSITION_PROPERTIES'))
120
+ environment.set('TOA_EXPOSITION_PROPERTIES', JSON.stringify(DEFAULT_PROPERTIES))
116
121
 
117
122
  this.writeConfiguration()
118
123
 
@@ -141,14 +146,11 @@ export class Gateway {
141
146
 
142
147
  /** What the running gateway was built from: a different value is a different deployment. */
143
148
  private static signature(): string {
144
- const configuration = Object.keys(process.env)
145
- .filter((key) => key.startsWith('TOA_CONFIGURATION_'))
146
- .sort()
147
- .map((key) => [key, process.env[key]])
149
+ const configuration = Object.entries(environment.entries('TOA_CONFIGURATION_')).sort()
148
150
 
149
151
  return JSON.stringify([
150
- process.env.TOA_EXPOSITION,
151
- process.env.TOA_EXPOSITION_PROPERTIES,
152
+ environment.get('TOA_EXPOSITION'),
153
+ environment.get('TOA_EXPOSITION_PROPERTIES'),
152
154
  process.env.__TESTING_EXPOSITION_BRANCH_TTL,
153
155
  configuration
154
156
  ])
@@ -158,12 +160,12 @@ export class Gateway {
158
160
  public async cleanup(): Promise<void> {
159
161
  delete process.env.__TESTING_EXPOSITION_BRANCH_TTL
160
162
 
161
- for (const key of this.written) delete process.env[key]
163
+ for (const key of this.written) environment.delete(key)
162
164
 
163
165
  this.written = []
164
166
 
165
- delete process.env.TOA_EXPOSITION
166
- delete process.env.TOA_EXPOSITION_PROPERTIES
167
+ environment.delete('TOA_EXPOSITION')
168
+ environment.delete('TOA_EXPOSITION_PROPERTIES')
167
169
  }
168
170
 
169
171
  @afterAll()
@@ -178,11 +180,11 @@ export class Gateway {
178
180
  const [name, namespace = 'default'] = id.split('.').reverse()
179
181
  const key = `TOA_CONFIGURATION_${namespace.toUpperCase()}_${name.toUpperCase()}`
180
182
 
181
- process.env[key] ??= JSON.stringify(configuration)
183
+ if (!environment.has(key)) environment.set(key, JSON.stringify(configuration))
182
184
  }
183
185
 
184
186
  for (const [name, value] of Object.entries(DEFAULT_SECRETS))
185
- process.env[SECRET + name] ??= value
187
+ if (!environment.has(SECRET + name)) environment.set(SECRET + name, value)
186
188
  }
187
189
  }
188
190
 
@@ -208,7 +210,7 @@ const DEFAULT_PROPERTIES: Partial<http.Options> = {
208
210
  port: PORT,
209
211
  probe: PROBE,
210
212
  // `npm run features:h2c` runs the whole suite over cleartext HTTP/2
211
- protocol: process.env.TOA_EXPOSITION_PROTOCOL === 'h2c' ? 'h2c' : 'h1'
213
+ protocol: environment.get('TOA_EXPOSITION_PROTOCOL') === 'h2c' ? 'h2c' : 'h1'
212
214
  }
213
215
 
214
216
  // the identity components boot inside the gateway, and without a variable each would wait
@@ -1,6 +1,7 @@
1
1
  import { join } from 'node:path'
2
2
  import * as dotenv from 'dotenv'
3
3
  import { setDefaultTimeout } from '@cucumber/cucumber'
4
+ import { environment as variables } from '@toa.io/generic'
4
5
 
5
6
  dotenv.config({ path: join(import.meta.dirname, '.env') })
6
7
 
@@ -21,24 +22,41 @@ export class Parameters {
21
22
 
22
23
  setDefaultTimeout(60 * 1000)
23
24
 
24
- process.env.TOA_DEV = '1'
25
+ variables.set('TOA_DEV', '1')
25
26
 
26
27
  // a reply is checked against what the operation declares, so the suite runs Toa under the
27
28
  // contract it asks applications to keep
28
- process.env.TOA_ENV ??= 'local'
29
+ if (!variables.has('TOA_ENV')) variables.set('TOA_ENV', 'local')
29
30
 
30
31
  // the gateway answers for itself, as it does in a deployment: telemetry's probe tracks the
31
32
  // nested composition, which connects before route discovery settles
32
- process.env.TOA_TELEMETRY_READY ??= JSON.stringify(false)
33
+ if (!variables.has('TOA_TELEMETRY_READY'))
34
+ variables.set('TOA_TELEMETRY_READY', JSON.stringify(false))
33
35
 
34
36
  // export traces to the local Tempo (`docker compose up tempo grafana`),
35
37
  // unavailability of the endpoint is harmless
36
- process.env.TOA_TELEMETRY_TRACES ??= JSON.stringify({
37
- exporters: {
38
- console: null,
39
- otlp: { endpoint: 'http://localhost:31061' }
40
- }
41
- })
38
+ if (!variables.has('TOA_TELEMETRY_TRACES'))
39
+ variables.set(
40
+ 'TOA_TELEMETRY_TRACES',
41
+ JSON.stringify({
42
+ exporters: {
43
+ console: null,
44
+ otlp: { endpoint: 'http://localhost:31061' }
45
+ }
46
+ })
47
+ )
48
+
49
+ /*
50
+ * The page the gateway serves at `/.discovery/`, pointed at a fixture: the scenarios are
51
+ * about the server, not the page, and none of them should need a UI build. Set here rather
52
+ * than in a step because the gateway is not rebuilt between scenarios that share its
53
+ * signature, so one flipping this would silently read whatever the last one left.
54
+ */
55
+ process.env.__TESTING_EXPOSITION_DISCOVERY_ROOT = join(
56
+ import.meta.dirname,
57
+ '..',
58
+ 'site'
59
+ )
42
60
 
43
61
  const environment = process.env.CLOUDINARY_ENVIRONMENT
44
62
 
@@ -132,10 +150,13 @@ const CLOUDINARY = {
132
150
  }
133
151
  }
134
152
 
135
- process.env.TOA_STORAGES = JSON.stringify({
136
- octets: {
137
- provider: 'tmp',
138
- directory: Math.random().toString(36).substring(2)
139
- },
140
- ...(environment === undefined ? {} : CLOUDINARY)
141
- })
153
+ variables.set(
154
+ 'TOA_STORAGES',
155
+ JSON.stringify({
156
+ octets: {
157
+ provider: 'tmp',
158
+ directory: Math.random().toString(36).substring(2)
159
+ },
160
+ ...(environment === undefined ? {} : CLOUDINARY)
161
+ })
162
+ )