@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.
- package/CHANGELOG.md +19 -0
- package/components/identity.basic/tsconfig.tsbuildinfo +1 -1
- package/components/identity.clients/tsconfig.tsbuildinfo +1 -1
- package/components/identity.credentials/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/tsconfig.tsbuildinfo +1 -1
- package/components/identity.grants/tsconfig.tsbuildinfo +1 -1
- package/components/identity.keys/tsconfig.tsbuildinfo +1 -1
- package/components/identity.otp/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/manifest.toa.yaml +2 -1
- package/components/identity.passkeys/source/errors.test.ts +34 -0
- package/components/identity.passkeys/source/webauthn.test.ts +26 -0
- package/components/identity.passkeys/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/types/toa.d.ts +1 -1
- package/components/identity.roles/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/tsconfig.tsbuildinfo +1 -1
- package/documentation/access.md +23 -4
- package/documentation/discovery.md +81 -0
- package/documentation/help.md +122 -0
- package/documentation/introspection.md +42 -9
- package/documentation/mcp.md +13 -16
- package/documentation/octets.md +20 -0
- package/features/cors.feature +31 -1
- package/features/dev.feature +2 -0
- package/features/discovery.feature +213 -0
- package/features/discovery.ui.feature +213 -0
- package/features/help.feature +512 -0
- package/features/introspection.feature +94 -8
- package/features/map.feature +1 -0
- package/features/mcp.feature +74 -12
- package/features/methods.feature +2 -1
- package/features/oauth.grants.feature +3 -1
- package/features/octets.download.feature +1 -0
- package/features/octets.feature +59 -0
- package/features/octets.meta.feature +1 -0
- package/features/site/_app/immutable/asset.js +1 -0
- package/features/site/favicon.ico +0 -0
- package/features/site/index.html +10 -0
- package/features/steps/Components.ts +4 -2
- package/features/steps/Database.ts +7 -1
- package/features/steps/Gateway.ts +23 -21
- package/features/steps/Parameters.ts +37 -16
- package/features/steps/Realtime.ts +3 -3
- package/package.json +7 -5
- package/readme.md +3 -0
- package/source/Directive.ts +4 -11
- package/source/Discovery/Explorer.ts +34 -0
- package/source/Discovery/Site.test.ts +147 -0
- package/source/Discovery/Site.ts +202 -0
- package/source/Discovery/index.ts +3 -0
- package/source/Discovery/tree.test.ts +175 -0
- package/source/Discovery/tree.ts +77 -0
- package/source/Discovery/trunk.ts +33 -0
- package/source/Endpoint.ts +4 -0
- package/source/Factory.ts +3 -86
- package/source/Gateway.ts +34 -4
- package/source/HTTP/Context.ts +7 -0
- package/source/HTTP/Probe.ts +0 -11
- package/source/HTTP/Server.ts +2 -3
- package/source/HTTP/messages.ts +4 -2
- package/source/Introspection.ts +58 -7
- package/source/MCP/schema.ts +16 -5
- package/source/MCP/tools.ts +14 -32
- package/source/Mapping.ts +5 -0
- package/source/Query.ts +17 -2
- package/source/RTD/Directives.ts +10 -0
- package/source/RTD/Endpoint.ts +8 -1
- package/source/RTD/Node.ts +20 -6
- package/source/RTD/Tree.ts +19 -9
- package/source/RTD/factory.ts +37 -1
- package/source/RTD/segment.ts +21 -0
- package/source/const.ts +26 -0
- package/source/deployment.ts +2 -2
- package/source/directives/auth/Anonymous.test.ts +31 -5
- package/source/directives/auth/Anonymous.ts +15 -3
- package/source/directives/auth/Anyone.ts +6 -0
- package/source/directives/auth/Assert.ts +5 -0
- package/source/directives/auth/Delegate.ts +5 -2
- package/source/directives/auth/Federation.ts +11 -0
- package/source/directives/auth/Id.ts +15 -0
- package/source/directives/auth/Input.ts +5 -0
- package/source/directives/auth/Role.ts +17 -0
- package/source/directives/cors/CORS.test.ts +75 -0
- package/source/directives/cors/CORS.ts +10 -3
- package/source/directives/help/Family.ts +122 -0
- package/source/directives/help/Help.test.ts +227 -0
- package/source/directives/help/Help.ts +47 -0
- package/source/directives/help/Parameters.ts +47 -0
- package/source/directives/help/described.ts +57 -0
- package/source/directives/help/index.ts +45 -0
- package/source/directives/index.ts +8 -1
- package/source/directives/map/Headers.ts +6 -6
- package/source/directives/mcp/MCP.ts +12 -21
- package/source/directives/mcp/Tool.test.ts +22 -68
- package/source/directives/mcp/Tool.ts +14 -41
- package/source/directives/octets/Directive.ts +7 -0
- package/source/directives/octets/Octets.ts +8 -0
- package/source/directives/octets/Put.ts +10 -0
- package/source/manifest.ts +1 -1
- package/source/root.ts +2 -2
- package/source/service.ts +94 -0
- package/source/shortcuts.ts +14 -0
- package/transpiled/Directive.d.ts +1 -1
- package/transpiled/Directive.js +3 -11
- package/transpiled/Directive.js.map +1 -1
- package/transpiled/Discovery/Explorer.d.ts +11 -0
- package/transpiled/Discovery/Explorer.js +29 -0
- package/transpiled/Discovery/Explorer.js.map +1 -0
- package/transpiled/Discovery/Site.d.ts +43 -0
- package/transpiled/Discovery/Site.js +171 -0
- package/transpiled/Discovery/Site.js.map +1 -0
- package/transpiled/Discovery/index.d.ts +3 -0
- package/transpiled/Discovery/index.js +4 -0
- package/transpiled/Discovery/index.js.map +1 -0
- package/transpiled/Discovery/tree.d.ts +26 -0
- package/transpiled/Discovery/tree.js +50 -0
- package/transpiled/Discovery/tree.js.map +1 -0
- package/transpiled/Discovery/trunk.d.ts +13 -0
- package/transpiled/Discovery/trunk.js +29 -0
- package/transpiled/Discovery/trunk.js.map +1 -0
- package/transpiled/Endpoint.d.ts +2 -1
- package/transpiled/Endpoint.js +3 -0
- package/transpiled/Endpoint.js.map +1 -1
- package/transpiled/Factory.js +3 -61
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Gateway.d.ts +4 -0
- package/transpiled/Gateway.js +29 -4
- package/transpiled/Gateway.js.map +1 -1
- package/transpiled/HTTP/Context.d.ts +6 -0
- package/transpiled/HTTP/Context.js +6 -0
- package/transpiled/HTTP/Context.js.map +1 -1
- package/transpiled/HTTP/Probe.d.ts +0 -10
- package/transpiled/HTTP/Probe.js +0 -10
- package/transpiled/HTTP/Probe.js.map +1 -1
- package/transpiled/HTTP/Server.d.ts +0 -1
- package/transpiled/HTTP/Server.js +2 -2
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/HTTP/messages.js +4 -2
- package/transpiled/HTTP/messages.js.map +1 -1
- package/transpiled/Introspection.d.ts +38 -6
- package/transpiled/Introspection.js +18 -1
- package/transpiled/Introspection.js.map +1 -1
- package/transpiled/MCP/schema.d.ts +2 -2
- package/transpiled/MCP/schema.js +11 -5
- package/transpiled/MCP/schema.js.map +1 -1
- package/transpiled/MCP/tools.d.ts +1 -1
- package/transpiled/MCP/tools.js +12 -28
- package/transpiled/MCP/tools.js.map +1 -1
- package/transpiled/Mapping.d.ts +2 -0
- package/transpiled/Mapping.js +4 -0
- package/transpiled/Mapping.js.map +1 -1
- package/transpiled/Query.d.ts +12 -0
- package/transpiled/Query.js +16 -2
- package/transpiled/Query.js.map +1 -1
- package/transpiled/RTD/Directives.d.ts +8 -0
- package/transpiled/RTD/Endpoint.d.ts +7 -1
- package/transpiled/RTD/Node.d.ts +11 -2
- package/transpiled/RTD/Node.js +12 -2
- package/transpiled/RTD/Node.js.map +1 -1
- package/transpiled/RTD/Tree.js +15 -8
- package/transpiled/RTD/Tree.js.map +1 -1
- package/transpiled/RTD/factory.js +27 -1
- package/transpiled/RTD/factory.js.map +1 -1
- package/transpiled/RTD/segment.d.ts +7 -0
- package/transpiled/RTD/segment.js +17 -0
- package/transpiled/RTD/segment.js.map +1 -1
- package/transpiled/const.d.ts +21 -0
- package/transpiled/const.js +21 -0
- package/transpiled/const.js.map +1 -1
- package/transpiled/deployment.js +2 -2
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/directives/auth/Anonymous.d.ts +11 -2
- package/transpiled/directives/auth/Anonymous.js +13 -3
- package/transpiled/directives/auth/Anonymous.js.map +1 -1
- package/transpiled/directives/auth/Anyone.d.ts +3 -0
- package/transpiled/directives/auth/Anyone.js +4 -0
- package/transpiled/directives/auth/Anyone.js.map +1 -1
- package/transpiled/directives/auth/Assert.d.ts +2 -0
- package/transpiled/directives/auth/Assert.js +4 -0
- package/transpiled/directives/auth/Assert.js.map +1 -1
- package/transpiled/directives/auth/Delegate.d.ts +4 -1
- package/transpiled/directives/auth/Delegate.js +5 -2
- package/transpiled/directives/auth/Delegate.js.map +1 -1
- package/transpiled/directives/auth/Federation.d.ts +5 -0
- package/transpiled/directives/auth/Federation.js +8 -0
- package/transpiled/directives/auth/Federation.js.map +1 -1
- package/transpiled/directives/auth/Id.d.ts +9 -0
- package/transpiled/directives/auth/Id.js +12 -0
- package/transpiled/directives/auth/Id.js.map +1 -1
- package/transpiled/directives/auth/Input.d.ts +2 -0
- package/transpiled/directives/auth/Input.js +4 -0
- package/transpiled/directives/auth/Input.js.map +1 -1
- package/transpiled/directives/auth/Role.d.ts +7 -0
- package/transpiled/directives/auth/Role.js +11 -0
- package/transpiled/directives/auth/Role.js.map +1 -1
- package/transpiled/directives/cors/CORS.js +9 -2
- package/transpiled/directives/cors/CORS.js.map +1 -1
- package/transpiled/directives/help/Family.d.ts +28 -0
- package/transpiled/directives/help/Family.js +83 -0
- package/transpiled/directives/help/Family.js.map +1 -0
- package/transpiled/directives/help/Help.d.ts +22 -0
- package/transpiled/directives/help/Help.js +36 -0
- package/transpiled/directives/help/Help.js.map +1 -0
- package/transpiled/directives/help/Parameters.d.ts +18 -0
- package/transpiled/directives/help/Parameters.js +34 -0
- package/transpiled/directives/help/Parameters.js.map +1 -0
- package/transpiled/directives/help/described.d.ts +16 -0
- package/transpiled/directives/help/described.js +23 -0
- package/transpiled/directives/help/described.js.map +1 -0
- package/transpiled/directives/help/index.d.ts +22 -0
- package/transpiled/directives/help/index.js +34 -0
- package/transpiled/directives/help/index.js.map +1 -0
- package/transpiled/directives/index.d.ts +4 -0
- package/transpiled/directives/index.js +8 -1
- package/transpiled/directives/index.js.map +1 -1
- package/transpiled/directives/map/Headers.d.ts +5 -0
- package/transpiled/directives/map/Headers.js +7 -5
- package/transpiled/directives/map/Headers.js.map +1 -1
- package/transpiled/directives/mcp/MCP.d.ts +8 -5
- package/transpiled/directives/mcp/MCP.js +9 -13
- package/transpiled/directives/mcp/MCP.js.map +1 -1
- package/transpiled/directives/mcp/Tool.d.ts +6 -12
- package/transpiled/directives/mcp/Tool.js +10 -23
- package/transpiled/directives/mcp/Tool.js.map +1 -1
- package/transpiled/directives/octets/Directive.d.ts +6 -0
- package/transpiled/directives/octets/Directive.js.map +1 -1
- package/transpiled/directives/octets/Octets.d.ts +3 -0
- package/transpiled/directives/octets/Octets.js +6 -0
- package/transpiled/directives/octets/Octets.js.map +1 -1
- package/transpiled/directives/octets/Put.d.ts +3 -0
- package/transpiled/directives/octets/Put.js +8 -0
- package/transpiled/directives/octets/Put.js.map +1 -1
- package/transpiled/manifest.js +1 -1
- package/transpiled/root.js +2 -2
- package/transpiled/root.js.map +1 -1
- package/transpiled/service.d.ts +7 -0
- package/transpiled/service.js +74 -0
- package/transpiled/service.js.map +1 -0
- package/transpiled/shortcuts.d.ts +3 -0
- package/transpiled/shortcuts.js +13 -0
- package/transpiled/shortcuts.js.map +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/ui/dist/_app/env.js +1 -0
- package/ui/dist/_app/immutable/assets/0.Bl7vLmIK.css +2 -0
- package/ui/dist/_app/immutable/assets/2.BC0-Jhqu.css +1 -0
- package/ui/dist/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
- package/ui/dist/_app/immutable/chunks/Bjy-W4x2.js +81 -0
- package/ui/dist/_app/immutable/chunks/CDMRPWCx.js +6 -0
- package/ui/dist/_app/immutable/chunks/DJTKIUs8.js +1 -0
- package/ui/dist/_app/immutable/chunks/DjIYL2zC.js +3 -0
- package/ui/dist/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/ui/dist/_app/immutable/chunks/z-DVUbQC.js +1 -0
- package/ui/dist/_app/immutable/entry/app.5S2Jtv46.js +2 -0
- package/ui/dist/_app/immutable/entry/start.Bqi6IBmH.js +1 -0
- package/ui/dist/_app/immutable/nodes/0.D4LwNilH.js +1 -0
- package/ui/dist/_app/immutable/nodes/1.CSRya9So.js +1 -0
- package/ui/dist/_app/immutable/nodes/2.BtiGmR6I.js +14 -0
- package/ui/dist/_app/version.json +1 -0
- package/ui/dist/apple-touch-icon.png +0 -0
- package/ui/dist/favicon-96x96.png +0 -0
- package/ui/dist/favicon.ico +0 -0
- package/ui/dist/index.html +66 -0
- package/components/identity.bans/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.basic/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.clients/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.federation/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.grants/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.keys/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.otp/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.passkeys/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.roles/migrations/0001-system-properties.yaml +0 -14
- 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
|
-
|
|
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
|
-
|
|
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
|
package/features/map.feature
CHANGED
package/features/mcp.feature
CHANGED
|
@@ -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`,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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:
|
|
34
|
+
mcp:tool: true
|
|
35
|
+
help:method:
|
|
36
|
+
description: All the pots, newest first.
|
|
34
37
|
endpoint: enumerate
|
|
35
38
|
POST:
|
|
36
|
-
mcp:tool:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
package/features/methods.feature
CHANGED
|
@@ -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
|
"""
|
package/features/octets.feature
CHANGED
|
@@ -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
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const hashed = true
|
|
Binary file
|
|
@@ -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
|
-
|
|
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
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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.
|
|
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
|
-
|
|
151
|
-
|
|
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
|
|
163
|
+
for (const key of this.written) environment.delete(key)
|
|
162
164
|
|
|
163
165
|
this.written = []
|
|
164
166
|
|
|
165
|
-
delete
|
|
166
|
-
delete
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
)
|