@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
@@ -0,0 +1,77 @@
1
+ import { describing } from '../Introspection.js'
2
+ import { template } from '../RPC/names.js'
3
+ import { variables } from '../RTD/segment.js'
4
+ import { guarded, resource, type Described } from '../directives/help/index.js'
5
+ import type * as http from '../HTTP/index.js'
6
+ import type { Tree } from '../RTD/index.js'
7
+ import type { Introspection } from '../Introspection.js'
8
+
9
+ /**
10
+ * Every route in the tree this caller may reach, keyed by the template it answers at, and
11
+ * each described exactly as `OPTIONS` on that path describes it — so a key can be taken
12
+ * from the answer and sent back as a request.
13
+ *
14
+ * A route a name cannot spell is here, unlike at `/.rpc` and `/.mcp`: HTTP addresses a
15
+ * route by its path, so leaving one out would make this disagree with the router. A
16
+ * resource no method of which this caller may reach is not here at all — `OPTIONS` answers
17
+ * `403` to one, and an empty entry would still say it exists.
18
+ *
19
+ * Sorted, because a branch is merged whenever its tenant answers and two replicas would
20
+ * otherwise order the same tree differently. Verbs keep the order they were declared in,
21
+ * which is the order `OPTIONS` answers them in.
22
+ */
23
+ export async function describe(tree: Tree, request: http.Context): Promise<Discovered> {
24
+ const context = describing(request)
25
+ const routes = new Map<string, Mounted>()
26
+
27
+ /*
28
+ * Sequentially: there is no I/O to overlap — an endpoint's own description is read once
29
+ * and memoized — so awaiting the whole tree at once would only allocate every clone of
30
+ * it at the same moment.
31
+ */
32
+ for (const { segments, verb, method } of tree.walk()) {
33
+ const introspection = await method.explain(context, variables(segments))
34
+
35
+ if (introspection === null) continue
36
+
37
+ const route = template(segments)
38
+ let mounted = routes.get(route)
39
+
40
+ if (mounted === undefined) {
41
+ mounted = { described: resource(method.directives), methods: {} }
42
+ routes.set(route, mounted)
43
+ }
44
+
45
+ // two declarations can walk to one template — `/a/b` and `/a: { /b: }` — and the walk
46
+ // is in the order `match` tries them, so the first is the one a request would reach
47
+ mounted.methods[verb] ??= introspection
48
+ }
49
+
50
+ const described: Record<string, Resource> = {}
51
+
52
+ for (const route of Array.from(routes.keys()).sort()) {
53
+ const { described: stated, methods } = routes.get(route)!
54
+
55
+ // what the resource is, beside the methods it serves; a verb is upper case and cannot
56
+ // collide with either key
57
+ described[route] = { ...stated, ...guarded(methods), ...methods }
58
+ }
59
+
60
+ return { routes: described }
61
+ }
62
+
63
+ /** What the tree answers. An object, so that what is said of the whole of it has somewhere to go. */
64
+ export interface Discovered {
65
+ routes: Record<string, Resource>
66
+ }
67
+
68
+ /** What one resource is, and what it serves; a verb is upper case, and nothing else here is. */
69
+ export interface Resource extends Described {
70
+ [verb: string]: unknown
71
+ }
72
+
73
+ /** One route template as the walk found it, before the two are answered as one. */
74
+ interface Mounted {
75
+ described: Described | null
76
+ methods: Record<string, Introspection>
77
+ }
@@ -0,0 +1,33 @@
1
+ import Negotiator from 'negotiator'
2
+ import { types } from '../HTTP/formats/index.js'
3
+ import { DISCOVERY } from '../const.js'
4
+ import type { Context, OutgoingMessage } from '../HTTP/index.js'
5
+
6
+ /** What a browser asks for, and what nothing else here answers with. */
7
+ const HTML = 'text/html'
8
+
9
+ /**
10
+ * Where a browser that asked for the trunk is sent. An application serves what it declares,
11
+ * and `/` is usually not one of those — a person who typed the address into a browser is
12
+ * looking for something to look at, and the page is the only thing here that is one.
13
+ *
14
+ * A client that asked for something in particular gets what it asked for: only an `accept`
15
+ * that prefers a page, or one that names no preference at all, is sent here. The second is
16
+ * what an unfurler sends — several ask for anything, and a link to an application would
17
+ * otherwise show nothing — and what `curl` sends, which the page serves no worse than the
18
+ * `405` it used to get.
19
+ */
20
+ export function looking(context: Context): OutgoingMessage | null {
21
+ if (context.request.method !== 'GET' || context.procedural) return null
22
+
23
+ const accept = context.request.headers.accept
24
+
25
+ if (accept !== undefined && accept.trim() !== ANYTHING) {
26
+ if (new Negotiator(context.request).mediaType([...types, HTML]) !== HTML) return null
27
+ }
28
+
29
+ return { status: 302, headers: new Headers({ location: DISCOVERY + '/' }) }
30
+ }
31
+
32
+ /** An `accept` that names nothing, which is the same as naming none. */
33
+ const ANYTHING = '*/*'
@@ -52,6 +52,10 @@ export class Endpoint implements RTD.Endpoint {
52
52
  return message
53
53
  }
54
54
 
55
+ public selection(): Record<string, Schema> | null {
56
+ return this.mapping.selection()
57
+ }
58
+
55
59
  public async explain(parameters: RTD.Parameter[]): Promise<Introspection> {
56
60
  this.introspection ??= await this.introspect(parameters)
57
61
 
package/source/Factory.ts CHANGED
@@ -1,20 +1,6 @@
1
- import assert from 'node:assert'
2
1
  import { createHash } from 'node:crypto'
3
- import { console, traces, type LevelName, type TracesOptions } from 'openspan'
4
2
  import { Tenant } from './Tenant.js'
5
- import { Gateway } from './Gateway.js'
6
- import { Remotes } from './Remotes.js'
7
- import { Tree } from './RTD/index.js'
8
- import { EndpointsFactory } from './Endpoint.js'
9
- import { families, interceptors } from './directives/index.js'
10
- import { DirectivesFactory } from './Directive.js'
11
- import { Composition } from './Composition.js'
12
- import * as root from './root.js'
13
- import { ATOM_GROUP } from './const.js'
14
- import { Interception } from './Interception.js'
15
- import { Dispatcher } from './RPC/index.js'
16
- import { Server as Model } from './MCP/index.js'
17
- import * as http from './HTTP/index.js'
3
+ import { CHANNEL } from './const.js'
18
4
  import type { Branch } from './Branch.js'
19
5
  import type { syntax } from './RTD/index.js'
20
6
  import type { Broadcast } from './Gateway.js'
@@ -46,80 +32,11 @@ export class Factory implements extensions.Factory {
46
32
  }
47
33
 
48
34
  public async service(): Promise<Connector | null> {
49
- assert.ok(
50
- process.env.TOA_EXPOSITION_PROPERTIES,
51
- 'TOA_EXPOSITION_PROPERTIES is undefined'
52
- )
35
+ const { service } = await import('./service.js')
53
36
 
54
- configureLogs()
55
-
56
- const options = JSON.parse(process.env.TOA_EXPOSITION_PROPERTIES) as http.Options
57
- const broadcast: Broadcast = await this.host.broadcast(CHANNEL)
58
- const server = http.Server.create({ ...options })
59
- const remotes = new Remotes(this.host)
60
- const node = root.resolve()
61
- const methods = new EndpointsFactory(remotes)
62
- const directives = new DirectivesFactory(families, remotes, this.host, options)
63
- const interception = new Interception(interceptors, options)
64
- const tree = new Tree(node, methods, directives)
65
-
66
- const composition = new Composition(this.host)
67
- const dispatcher = options.rpc === undefined ? null : new Dispatcher(options.rpc)
68
- const mcp = options.mcp === undefined ? null : new Model(options.mcp, tree)
69
- const gateway = new Gateway(
70
- broadcast,
71
- tree,
72
- interception,
73
- directives,
74
- dispatcher,
75
- mcp
76
- )
77
-
78
- gateway.depends(remotes)
79
- gateway.depends(composition)
80
- // what the directives meter through; one atom per process, connected once
81
- gateway.depends(this.host.atom(ATOM_GROUP))
82
-
83
- server.attach(gateway.process.bind(gateway))
84
- server.depends(gateway)
85
-
86
- return server
37
+ return await service(this.host)
87
38
  }
88
39
  }
89
40
 
90
- const CHANNEL = 'exposition'
91
- const LOGS_PREFIX = 'TOA_TELEMETRY_LOGS'
92
- const TRACES_ENV = 'TOA_TELEMETRY_TRACES'
93
-
94
- function configureLogs(): void {
95
- const globEnv = process.env[LOGS_PREFIX]
96
- const level: LevelName = process.env.TOA_DEV === '1' ? 'trace' : 'info'
97
- const options =
98
- globEnv === undefined ? { level } : (JSON.parse(globEnv) as { level?: LevelName })
99
-
100
- console.configure({ level: options.level ?? level })
101
-
102
- const tracesEnv = process.env[TRACES_ENV]
103
-
104
- traces(
105
- tracesEnv === undefined ? development() : (JSON.parse(tracesEnv) as TracesOptions)
106
- )
107
- }
108
-
109
- /**
110
- * Tracing is off unless it is configured. The console exporter is a local development
111
- * mechanism, so it is turned on for `toa dev` and for a boot trace the CLI has already
112
- * asked for (`runtime/boot/src/span.js`), and nowhere else — a deployment that wants
113
- * traces annotates `telemetry.traces.exporters`.
114
- *
115
- * The gateway boots without the telemetry extension, hence the copy of
116
- * `extensions/telemetry/source/extension.ts`.
117
- */
118
- function development(): TracesOptions {
119
- const local = process.env.TOA_DEV === '1' || process.env.TOA_BOOT_TRACE === '1'
120
-
121
- return local ? { exporters: { console: {} } } : {}
122
- }
123
-
124
41
  // eslint-disable-next-line @typescript-eslint/consistent-type-imports
125
42
  export type Host = extensions.Host
package/source/Gateway.ts CHANGED
@@ -4,9 +4,11 @@ import { console } from 'openspan'
4
4
  import { Connector } from '@toa.io/core'
5
5
  import type { bindings } from '@toa.io/core/types'
6
6
  import * as http from './HTTP/index.js'
7
- import { MCP, RPC } from './const.js'
7
+ import { DISCOVERY, MCP, RPC } from './const.js'
8
8
  import { rethrow } from './exceptions.js'
9
9
  import { decide } from './Branch.js'
10
+ import { describing } from './Introspection.js'
11
+ import { Explorer, looking } from './Discovery/index.js'
10
12
  import type { Interception } from './Interception.js'
11
13
  import type { Dispatcher } from './RPC/index.js'
12
14
  import type { Server } from './MCP/index.js'
@@ -32,6 +34,9 @@ export class Gateway extends Connector {
32
34
 
33
35
  /** And the same of MCP. */
34
36
  private readonly mcp: Server | null
37
+
38
+ /** The tree, for every route at once. Served always, and so not nullable. */
39
+ private readonly explorer: Explorer
35
40
  private readonly branches = new Map<string, Exposed>()
36
41
  private lastMerge = 0
37
42
  private widestGap = 0
@@ -56,6 +61,7 @@ export class Gateway extends Connector {
56
61
  this.directives = directives
57
62
  this.dispatcher = dispatcher
58
63
  this.mcp = mcp
64
+ this.explorer = new Explorer(tree)
59
65
 
60
66
  this.depends(broadcast)
61
67
  }
@@ -89,9 +95,28 @@ export class Gateway extends Connector {
89
95
  if (this.mcp !== null && context.url.pathname === MCP)
90
96
  return await this.mcp.process(context, route)
91
97
 
98
+ // the page under this prefix is an interceptor's, and has already answered
99
+ if (context.url.pathname === DISCOVERY || context.url.pathname === DISCOVERY + '/')
100
+ return await this.explorer.process(context)
101
+
102
+ // a browser that asked for the trunk of an API is looking for the page, and where the
103
+ // application answers there itself, that is what answers
104
+ if (context.url.pathname === '/' && !this.serves(context)) {
105
+ const page = looking(context)
106
+
107
+ if (page !== null) return page
108
+ }
109
+
92
110
  return await this.route(context)
93
111
  }
94
112
 
113
+ /** Whether the application serves the trunk with the verb the request was made with. */
114
+ private serves(context: http.Context): boolean {
115
+ const match = this.tree.match('/')
116
+
117
+ return match !== null && context.request.method in match.node.methods
118
+ }
119
+
95
120
  /**
96
121
  * One call: everything that needs a node. A request makes one of these, and a request
97
122
  * that carries several calls makes one per call.
@@ -200,14 +225,19 @@ export class Gateway extends Connector {
200
225
  node: Node,
201
226
  parameters: Parameter[]
202
227
  ): Promise<http.OutgoingMessage> {
203
- const body = await node.explain(context, parameters)
204
- const verbs = Object.keys(body)
228
+ const { described, methods } = await node.explain(describing(context), parameters)
229
+ const verbs = Object.keys(methods)
205
230
 
206
231
  // what a caller may not use is not a resource to them, and describing it would be the leak
207
232
  if (verbs.length === 0 && Object.keys(node.methods).length > 0)
208
233
  throw new http.Forbidden()
209
234
 
210
- return { body, headers: new Headers({ allow: verbs.join(', ') }) }
235
+ // what the resource is, beside the methods it serves; a verb is upper case and cannot
236
+ // collide with either key
237
+ return {
238
+ body: { ...described, ...methods },
239
+ headers: new Headers({ allow: verbs.join(', ') })
240
+ }
211
241
  }
212
242
 
213
243
  private async discover(): Promise<void> {
@@ -26,6 +26,13 @@ export class Context {
26
26
  */
27
27
  public readonly procedural: boolean = false
28
28
 
29
+ /**
30
+ * Whether this describes a resource rather than calling one. What builds a describing
31
+ * context says so, and a directive that answers differently to a description than to a
32
+ * call reads it here.
33
+ */
34
+ public readonly exploratory: boolean = false
35
+
29
36
  public readonly pipelines: Pipelines = {
30
37
  body: [],
31
38
  response: []
@@ -96,15 +96,4 @@ export class Probe {
96
96
  }
97
97
  }
98
98
 
99
- /**
100
- * Reserved for this probe. `8001` is the Telemetry readiness probe's, and `toa export` refuses a
101
- * port claimed twice — `toa mono` and a local run put every service in one process.
102
- */
103
- export const PROBE = 8004
104
99
  export const PATH = '/.ready'
105
-
106
- /**
107
- * The initial delay of the readiness probe. The server does not sleep for it: whoever
108
- * probes is the one that waits, and doing it here as well only delayed the process twice.
109
- */
110
- export const DELAY = 3 // seconds
@@ -10,7 +10,8 @@ import { Connector } from '@toa.io/core'
10
10
  import { type OutgoingMessage, write } from './messages.js'
11
11
  import { ClientError, Exception } from './exceptions.js'
12
12
  import { Context } from './Context.js'
13
- import { PROBE, Probe } from './Probe.js'
13
+ import { Probe } from './Probe.js'
14
+ import { PORT, PROBE } from '../const.js'
14
15
  import type { IncomingMessage, Protocol, ServerResponse } from './types.js'
15
16
  import type { Bouncer, MCP, OAuth, RPC } from '../Annotation.js'
16
17
 
@@ -331,8 +332,6 @@ function errorAttributes(
331
332
  return attributes
332
333
  }
333
334
 
334
- export const PORT = 8000
335
-
336
335
  export const DRAIN = 10 // seconds
337
336
 
338
337
  /** Megabytes a single HTTP/2 session may hold, over Node's default of 10. */
@@ -5,13 +5,15 @@ import * as contentType from 'content-type'
5
5
  import { console } from 'openspan'
6
6
  import { type Format, decoders } from './formats/index.js'
7
7
  import { BadRequest, NotAcceptable, UnsupportedMediaType } from './exceptions.js'
8
+ import { environment } from '@toa.io/generic'
8
9
  import type { Context } from './Context.js'
9
10
  import type { ServerResponse } from './types.js'
10
11
 
12
+ const context = environment.get('TOA_CONTEXT')
13
+ const env = environment.get('TOA_ENV')
11
14
  const server =
12
15
  `Exposition/${JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version}` +
13
- ((process.env.TOA_CONTEXT === undefined ? '' : ` ${process.env.TOA_CONTEXT}`) +
14
- (process.env.TOA_ENV === undefined ? '' : `/${process.env.TOA_ENV}`))
16
+ ((context === undefined ? '' : ` ${context}`) + (env === undefined ? '' : `/${env}`))
15
17
 
16
18
  /**
17
19
  * Applies what the request accumulated in `pipelines.response` — an `io:output` restriction,
@@ -1,4 +1,5 @@
1
1
  import type { Remote } from '@toa.io/core'
2
+ import type { Context } from './HTTP/index.js'
2
3
 
3
4
  export interface Introspection {
4
5
  /**
@@ -11,24 +12,68 @@ export interface Introspection {
11
12
 
12
13
  /** what a person is shown where a client lists this method, which a name is not */
13
14
  title?: string
15
+
16
+ /**
17
+ * whether reaching it takes nothing at all — `auth:anonymous`. Said rather than left
18
+ * unsaid, because it is also what refuses a caller presenting a credential: a client
19
+ * cannot know that from an answer that says nothing.
20
+ */
21
+ anonymous?: boolean
22
+
23
+ /** whether reaching it takes being someone, whoever — `auth:anyone` and its like */
24
+ authenticated?: boolean
25
+
26
+ /** whether reaching it is being the identity it is about — `auth:id` */
27
+ private?: boolean
28
+
29
+ /** whether reaching it takes a role — `auth:role` */
30
+ protected?: boolean
31
+
32
+ /** and whether that role is one of the `system` scope */
33
+ system?: boolean
34
+
35
+ /** whether the route publishes this method to a model — [`mcp:tool`](mcp.md) */
36
+ mcp?: boolean
14
37
  route?: Record<string, Schema>
15
38
  query?: Record<string, Schema>
16
39
 
17
- /** what a request header carries, which is therefore not the body's to send */
18
- headers?: Record<string, Sourced>
40
+ /** what the body is, where it is a file rather than a value — [`octets:put`](octets.md) */
41
+ octets?: Octets
19
42
  input?: Schema
20
43
  output?: Schema
21
44
  errors?: string[]
22
45
  }
23
46
 
24
- /** A property the gateway reads from somewhere else, and the schema it was declared with. */
25
- export interface Sourced {
26
- header: string
27
- [keyword: string]: unknown
47
+ /** What sending a file to a resource takes, which is not something a schema states. */
48
+ export interface Octets {
49
+ /** what may be sent, in the syntax of an `accept` header; anything where unstated */
50
+ accept?: string
51
+
52
+ /** the largest body it takes, as it is written and as a refusal reports it */
53
+ limit: string
54
+
55
+ /**
56
+ * Whether the reply arrives as a stream of parts rather than as one object: a workflow
57
+ * runs on what was stored, and each step answers as it finishes. The first part is the
58
+ * entry itself, which is what a caller that wants nothing else reads and stops.
59
+ */
60
+ stream?: boolean
28
61
  }
29
62
 
30
63
  export type Schema = Awaited<ReturnType<Remote['explain']>>['input']
31
64
 
65
+ /**
66
+ * The same request, as a description of a resource rather than a call to one. A directive
67
+ * that answers differently to the two reads `exploratory` — `Anonymous` is the one that
68
+ * does, because the rule refusing a credential at an `anonymous` route is about a reply a
69
+ * cache would hold, and a description is not one.
70
+ */
71
+ export function describing(context: Context): Context {
72
+ return Object.create(context, {
73
+ exploratory: { value: true, enumerable: true }
74
+ }) as Context
75
+ }
76
+
32
77
  /**
33
78
  * The same, in the order the shape states — whatever order the families that filled it ran
34
79
  * in. What a resource says about itself is read, so it is written the way it is documented.
@@ -45,9 +90,15 @@ export function order(introspection: Introspection): Introspection {
45
90
  const KEYS = [
46
91
  'title',
47
92
  'description',
93
+ 'anonymous',
94
+ 'authenticated',
95
+ 'private',
96
+ 'protected',
97
+ 'system',
98
+ 'mcp',
48
99
  'route',
49
100
  'query',
50
- 'headers',
101
+ 'octets',
51
102
  'input',
52
103
  'output',
53
104
  'errors'
@@ -8,7 +8,12 @@ import type { Annotations } from './types.js'
8
8
  * A `map:headers` property is not here: a call carries no headers of its own, so there is
9
9
  * nowhere for a model to put one.
10
10
  */
11
- export function input(introspection: Introspection, variables: string[]): object {
11
+ // eslint-disable-next-line max-params
12
+ export function input(
13
+ introspection: Introspection,
14
+ variables: string[],
15
+ selection: Record<string, Schema> | null
16
+ ): object {
12
17
  const properties: Record<string, unknown> = {}
13
18
  const required: string[] = []
14
19
 
@@ -23,15 +28,21 @@ export function input(introspection: Introspection, variables: string[]): object
23
28
  required.push(variable)
24
29
  }
25
30
 
26
- // it is a querystring on the wire, which is nothing a model knows or needs to; what it
27
- // is to a caller is the part of a call that picks what the call is about
28
- if (introspection.query !== undefined)
31
+ /*
32
+ * It is a querystring on the wire, which is nothing a model knows or needs to; what it is
33
+ * to a caller is the part of a call that picks what the call is about. What selects
34
+ * records is stated here and not in the resource's own description, where it would be the
35
+ * same sentence on every queryable resource there is.
36
+ */
37
+ const query = { ...introspection.query, ...selection }
38
+
39
+ if (Object.keys(query).length > 0)
29
40
  properties.query = {
30
41
  type: 'object',
31
42
  description:
32
43
  'Which records the call works on: what to match, in what order, ' +
33
44
  'and how many at once.',
34
- properties: introspection.query
45
+ properties: query
35
46
  }
36
47
 
37
48
  const body = introspection.input as Shape | null | undefined
@@ -4,8 +4,9 @@ import { address, name, split } from '../RPC/names.js'
4
4
  import { METHOD_NOT_FOUND, failure, refusal, response } from './errors.js'
5
5
  import { annotations, input, output } from './schema.js'
6
6
  import { FAMILY, MCP, type Tool as Declaration } from '../directives/mcp/index.js'
7
- import type { Segment } from '../RTD/segment.js'
8
- import type { Parameter, Tree } from '../RTD/index.js'
7
+ import { describing } from '../Introspection.js'
8
+ import { variables } from '../RTD/segment.js'
9
+ import type { Tree } from '../RTD/index.js'
9
10
  import type { Params, Result, Tool } from './types.js'
10
11
 
11
12
  /**
@@ -20,28 +21,26 @@ import type { Params, Result, Tool } from './types.js'
20
21
  *
21
22
  * Sorted, because the revision asks for an order a client can cache on.
22
23
  */
23
- export async function list(tree: Tree, context: http.Context): Promise<Tool[]> {
24
+ export async function list(tree: Tree, request: http.Context): Promise<Tool[]> {
24
25
  const tools: Tool[] = []
25
26
 
26
27
  /*
27
- * Each method is described as the procedure it would be, not as the request asking. What
28
- * refuses a credentialed request at an `anonymous` route does not refuse the call a tool
29
- * makes, and a list that says otherwise disagrees with what `tools/call` then does.
28
+ * A description, not the request asking. What refuses a credentialed request at an
29
+ * `anonymous` route does not refuse the call a tool makes, and a list that says otherwise
30
+ * disagrees with what `tools/call` then does.
30
31
  */
31
- const describing: http.Context = Object.create(context, {
32
- procedural: { value: true, enumerable: true }
33
- })
32
+ const context = describing(request)
34
33
 
35
34
  for (const { segments, verb, method } of tree.walk()) {
36
- if (MCP.published(method.directives.declared<Declaration>(FAMILY)) === null) continue
35
+ if (!MCP.published(method.directives.declared<Declaration>(FAMILY))) continue
37
36
 
38
37
  const named = name(segments, verb)
39
38
 
40
39
  // a route a name cannot spell is a route nothing addresses, here or at `/.rpc`
41
40
  if (named === null) continue
42
41
 
43
- const variables = parameters(segments)
44
- const introspection = await method.explain(describing, variables)
42
+ const params = variables(segments)
43
+ const introspection = await method.explain(context, params)
45
44
 
46
45
  if (introspection === null) continue
47
46
 
@@ -56,7 +55,8 @@ export async function list(tree: Tree, context: http.Context): Promise<Tool[]> {
56
55
  ...(described === undefined ? {} : { description: described }),
57
56
  inputSchema: input(
58
57
  introspection,
59
- variables.map((variable) => variable.name)
58
+ params.map((param) => param.name),
59
+ method.endpoint?.selection() ?? null
60
60
  ),
61
61
  ...(schema === undefined ? {} : { outputSchema: schema }),
62
62
  ...(hints === undefined ? {} : { annotations: hints })
@@ -129,7 +129,7 @@ export interface Scope {
129
129
  function published(tree: Tree, named: string): boolean {
130
130
  for (const { segments, verb, method } of tree.walk())
131
131
  if (name(segments, verb) === named)
132
- return MCP.published(method.directives.declared<Declaration>(FAMILY)) !== null
132
+ return MCP.published(method.directives.declared<Declaration>(FAMILY))
133
133
 
134
134
  return false
135
135
  }
@@ -142,21 +142,3 @@ function result(body: unknown): Result {
142
142
  structuredContent: body
143
143
  }
144
144
  }
145
-
146
- /**
147
- * What a route template takes, by name. Describing has no values for them — a template is
148
- * not a path — and nothing that describes a method reads one.
149
- */
150
- function parameters(segments: Segment[]): Parameter[] {
151
- const params: Parameter[] = []
152
-
153
- for (const segment of segments) {
154
- if (segment.fragment !== null) continue
155
-
156
- if (segment.wildcard === true) params.push({ name: '**', value: '' })
157
- else if (segment.placeholder !== null)
158
- params.push({ name: segment.placeholder, value: '' })
159
- }
160
-
161
- return params
162
- }
package/source/Mapping.ts CHANGED
@@ -24,6 +24,11 @@ export abstract class Mapping {
24
24
  return this.query.explain(introspection)
25
25
  }
26
26
 
27
+ /** What picks the records a call is about; see `Query.selection`. */
28
+ public selection(): Record<string, Schema> | null {
29
+ return this.query.selection()
30
+ }
31
+
27
32
  protected assign(input: any, qs: QueryString): void {
28
33
  if (qs.parameters !== null) {
29
34
  if (typeof input !== 'object' || input === null)
package/source/Query.ts CHANGED
@@ -82,6 +82,11 @@ export class Query {
82
82
  * Only what it actually accepts — a criteria the declaration closes is refused, and so is
83
83
  * a search where none was asked for.
84
84
  */
85
+ /**
86
+ * The querystring parameters this resource declares, which are properties of its own
87
+ * taken out of the input. What selects records is not among them: that is the same of
88
+ * every queryable resource, and `selection` is where it is stated.
89
+ */
85
90
  public explain(introspection: Introspection): Record<string, Schema> | null {
86
91
  let query: Record<string, Schema> | null = null
87
92
 
@@ -96,9 +101,19 @@ export class Query {
96
101
  query[parameter] = schema
97
102
  }
98
103
 
99
- if (!this.queryable) return query
104
+ return query
105
+ }
106
+
107
+ /**
108
+ * What picks the records a call is about, which is the querystring the gateway reads
109
+ * rather than anything the resource declares — the same for every queryable resource,
110
+ * and so not what one says about itself. A procedure states it, because there it is
111
+ * something the caller sends.
112
+ */
113
+ public selection(): Record<string, Schema> | null {
114
+ if (!this.queryable) return null
100
115
 
101
- query ??= {}
116
+ const query: Record<string, Schema> = {}
102
117
 
103
118
  if (!this.closed)
104
119
  query.criteria = keyword(