@toa.io/extensions.exposition 1.0.0-alpha.288 → 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 (120) hide show
  1. package/CHANGELOG.md +11 -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/tsconfig.tsbuildinfo +1 -1
  10. package/components/identity.roles/tsconfig.tsbuildinfo +1 -1
  11. package/components/identity.tokens/tsconfig.tsbuildinfo +1 -1
  12. package/documentation/access.md +3 -2
  13. package/documentation/discovery.md +8 -8
  14. package/documentation/help.md +16 -3
  15. package/documentation/introspection.md +10 -2
  16. package/documentation/octets.md +20 -0
  17. package/features/discovery.feature +4 -2
  18. package/features/discovery.ui.feature +19 -3
  19. package/features/help.feature +46 -0
  20. package/features/octets.feature +59 -0
  21. package/features/steps/Components.ts +4 -2
  22. package/features/steps/Database.ts +7 -1
  23. package/features/steps/Gateway.ts +23 -21
  24. package/features/steps/Parameters.ts +25 -16
  25. package/features/steps/Realtime.ts +3 -3
  26. package/package.json +5 -5
  27. package/source/Directive.ts +0 -11
  28. package/source/Discovery/trunk.ts +13 -3
  29. package/source/Factory.ts +3 -86
  30. package/source/HTTP/Probe.ts +0 -11
  31. package/source/HTTP/Server.ts +2 -3
  32. package/source/HTTP/messages.ts +4 -2
  33. package/source/Introspection.ts +28 -0
  34. package/source/const.ts +18 -0
  35. package/source/deployment.ts +2 -2
  36. package/source/directives/auth/Anonymous.ts +10 -0
  37. package/source/directives/help/Family.ts +5 -1
  38. package/source/directives/help/Help.ts +21 -0
  39. package/source/directives/octets/Directive.ts +7 -0
  40. package/source/directives/octets/Octets.ts +8 -0
  41. package/source/directives/octets/Put.ts +10 -0
  42. package/source/manifest.ts +1 -1
  43. package/source/root.ts +2 -2
  44. package/source/service.ts +94 -0
  45. package/source/shortcuts.ts +14 -0
  46. package/transpiled/Directive.d.ts +0 -1
  47. package/transpiled/Directive.js +0 -11
  48. package/transpiled/Directive.js.map +1 -1
  49. package/transpiled/Discovery/trunk.d.ts +5 -2
  50. package/transpiled/Discovery/trunk.js +12 -4
  51. package/transpiled/Discovery/trunk.js.map +1 -1
  52. package/transpiled/Factory.js +3 -61
  53. package/transpiled/Factory.js.map +1 -1
  54. package/transpiled/HTTP/Probe.d.ts +0 -10
  55. package/transpiled/HTTP/Probe.js +0 -10
  56. package/transpiled/HTTP/Probe.js.map +1 -1
  57. package/transpiled/HTTP/Server.d.ts +0 -1
  58. package/transpiled/HTTP/Server.js +2 -2
  59. package/transpiled/HTTP/Server.js.map +1 -1
  60. package/transpiled/HTTP/messages.js +4 -2
  61. package/transpiled/HTTP/messages.js.map +1 -1
  62. package/transpiled/Introspection.d.ts +21 -0
  63. package/transpiled/Introspection.js +2 -0
  64. package/transpiled/Introspection.js.map +1 -1
  65. package/transpiled/const.d.ts +14 -0
  66. package/transpiled/const.js +14 -0
  67. package/transpiled/const.js.map +1 -1
  68. package/transpiled/deployment.js +2 -2
  69. package/transpiled/deployment.js.map +1 -1
  70. package/transpiled/directives/auth/Anonymous.d.ts +7 -0
  71. package/transpiled/directives/auth/Anonymous.js +8 -0
  72. package/transpiled/directives/auth/Anonymous.js.map +1 -1
  73. package/transpiled/directives/help/Family.d.ts +1 -1
  74. package/transpiled/directives/help/Family.js +4 -0
  75. package/transpiled/directives/help/Family.js.map +1 -1
  76. package/transpiled/directives/help/Help.d.ts +6 -0
  77. package/transpiled/directives/help/Help.js +14 -0
  78. package/transpiled/directives/help/Help.js.map +1 -1
  79. package/transpiled/directives/octets/Directive.d.ts +6 -0
  80. package/transpiled/directives/octets/Directive.js.map +1 -1
  81. package/transpiled/directives/octets/Octets.d.ts +3 -0
  82. package/transpiled/directives/octets/Octets.js +6 -0
  83. package/transpiled/directives/octets/Octets.js.map +1 -1
  84. package/transpiled/directives/octets/Put.d.ts +3 -0
  85. package/transpiled/directives/octets/Put.js +8 -0
  86. package/transpiled/directives/octets/Put.js.map +1 -1
  87. package/transpiled/manifest.js +1 -1
  88. package/transpiled/root.js +2 -2
  89. package/transpiled/root.js.map +1 -1
  90. package/transpiled/service.d.ts +7 -0
  91. package/transpiled/service.js +74 -0
  92. package/transpiled/service.js.map +1 -0
  93. package/transpiled/shortcuts.d.ts +3 -0
  94. package/transpiled/shortcuts.js +13 -0
  95. package/transpiled/shortcuts.js.map +1 -0
  96. package/tsconfig.tsbuildinfo +1 -1
  97. package/ui/dist/_app/immutable/assets/0.Bl7vLmIK.css +2 -0
  98. package/ui/dist/_app/immutable/chunks/{CWJaG9mK.js → CDMRPWCx.js} +2 -2
  99. package/ui/dist/_app/immutable/chunks/DJTKIUs8.js +1 -0
  100. package/ui/dist/_app/immutable/chunks/{yDQhitF9.js → z-DVUbQC.js} +1 -1
  101. package/ui/dist/_app/immutable/entry/{app.WZSMBcGf.js → app.5S2Jtv46.js} +2 -2
  102. package/ui/dist/_app/immutable/entry/start.Bqi6IBmH.js +1 -0
  103. package/ui/dist/_app/immutable/nodes/{0.Dl2DqINo.js → 0.D4LwNilH.js} +1 -1
  104. package/ui/dist/_app/immutable/nodes/{1.CSSnGhVD.js → 1.CSRya9So.js} +1 -1
  105. package/ui/dist/_app/immutable/nodes/{2.DmolQyNG.js → 2.BtiGmR6I.js} +4 -4
  106. package/ui/dist/_app/version.json +1 -1
  107. package/ui/dist/index.html +11 -11
  108. package/components/identity.bans/migrations/0001-system-properties.yaml +0 -14
  109. package/components/identity.basic/migrations/0001-system-properties.yaml +0 -14
  110. package/components/identity.clients/migrations/0001-system-properties.yaml +0 -14
  111. package/components/identity.federation/migrations/0001-system-properties.yaml +0 -14
  112. package/components/identity.grants/migrations/0001-system-properties.yaml +0 -14
  113. package/components/identity.keys/migrations/0001-system-properties.yaml +0 -14
  114. package/components/identity.otp/migrations/0001-system-properties.yaml +0 -14
  115. package/components/identity.passkeys/migrations/0001-system-properties.yaml +0 -14
  116. package/components/identity.roles/migrations/0001-system-properties.yaml +0 -14
  117. package/components/identity.tokens/migrations/0001-system-properties.yaml +0 -14
  118. package/ui/dist/_app/immutable/assets/0.BcIGtTfw.css +0 -2
  119. package/ui/dist/_app/immutable/chunks/BgTrzN6u.js +0 -1
  120. package/ui/dist/_app/immutable/entry/start._LkoC6Lz.js +0 -1
@@ -1,7 +1,8 @@
1
1
  # Resource introspection
2
2
 
3
3
  Any resource can be introspected by sending an `OPTIONS` request to the resource's path.
4
- [Discovery](discovery.md) answers the same of every resource at once.
4
+ [Resource discovery](discovery.md) answers every resource at once, at `OPTIONS /.discovery`, and
5
+ serves a page that reads it.
5
6
 
6
7
  What it answers is what the route's directives leave of what the operation declared, for each
7
8
  method this identity may reach. A method they may not is not there, nor in `Allow`; a resource
@@ -13,13 +14,17 @@ described to whoever asks, as it is at `/.rpc` and `/.mcp`.
13
14
 
14
15
  What the resource itself is, from [`help:node`](help.md), is answered beside the methods rather
15
16
  than inside one. A verb is upper case, so neither key can be mistaken for the other. A resource
16
- carries `private`, `protected` and `system` where any of its methods does.
17
+ carries `private`, `protected` and `system` where any of its methods does. A method
18
+ [`help:method: null`](help.md) hides is in none of this, and is reached as it always was.
17
19
 
18
20
  Introspection properties:
19
21
 
20
22
  - `title` and `description` what the route states this method is, from
21
23
  [`help:method`](help.md). The operation states what it is too, and that is not this: it is
22
24
  written without knowledge of any route, and the same operation mounted twice is two methods
25
+ - `anonymous` reaching it takes nothing — [`auth:anonymous`](access.md#anonymous). Said rather
26
+ than left unsaid, because it is also what refuses a caller presenting a credential: a client
27
+ holding one has to know which methods not to present it to
23
28
  - `authenticated` reaching it takes being someone, whoever — [`auth:anyone`](access.md#anyone),
24
29
  `auth:delegate` and `auth:claims`
25
30
  - `private` reaching it is being the identity it is about — [`auth:id`](access.md#id)
@@ -33,6 +38,9 @@ Introspection properties:
33
38
  `omit`, `search` — is not among them: it is the same of every queryable resource, and so
34
39
  not something one says about itself. A [procedure](rpc.md) carries it, because there it is
35
40
  something the caller sends
41
+ - `octets` what sending a file takes, from [`octets:put`](octets.md): the media types it
42
+ `accept`s, the `limit` a larger body is refused by, and `stream` where storing it starts a
43
+ workflow and the reply arrives in parts rather than as one object
36
44
  - `input` input schema, restricted by `io:input`, without what the gateway fills itself
37
45
  - `output` output schema, restricted by `io:output`; absent where the reply is not sent at all.
38
46
  An operation that declares none is described by what its type answers: a transition, an
@@ -190,6 +190,26 @@ status: exception
190
190
  --cut--
191
191
  ```
192
192
 
193
+ ### What a client is told
194
+
195
+ `octets:put` says what sending a file takes, where everything else about a method is said —
196
+ [`OPTIONS`](introspection.md) and the [tree](discovery.md):
197
+
198
+ ```yaml
199
+ POST:
200
+ octets:
201
+ accept: image/jpeg,image/png
202
+ limit: 64MiB
203
+ stream: true
204
+ ```
205
+
206
+ `accept` is absent where the directive names no type, and anything may be sent. `limit` is
207
+ written as the declaration writes it, and is what a `413` reports. `stream` says a workflow runs
208
+ on what was stored, so the reply arrives as parts rather than as one object — the first part is
209
+ the entry.
210
+
211
+ Reading a file and deleting one say nothing: those requests are what they look like.
212
+
193
213
  ## `octets:get`
194
214
 
195
215
  Fetches the content of a stored BLOB corresponding to the request path, and returns it as the
@@ -133,7 +133,8 @@ Feature: Resource discovery
133
133
 
134
134
  routes:
135
135
  /open:
136
- GET: {}
136
+ GET:
137
+ anonymous: true
137
138
  """
138
139
  And the reply does not contain:
139
140
  """
@@ -207,5 +208,6 @@ Feature: Resource discovery
207
208
 
208
209
  routes:
209
210
  /pots:
210
- GET: {}
211
+ GET:
212
+ anonymous: true
211
213
  """
@@ -156,9 +156,9 @@ Feature: The discovery page
156
156
  location: /.discovery/
157
157
  """
158
158
 
159
- Scenario: A client that takes anything is not
160
- `accept` has to prefer a page over what the gateway answers with, which is what a
161
- browser sends and an API client does not.
159
+ Scenario: A client that asks for nothing in particular is sent there too
160
+ Several unfurlers ask for anything at all, and a link to an application would otherwise
161
+ show nothing.
162
162
 
163
163
  When the following request is received:
164
164
  """
@@ -166,6 +166,22 @@ Feature: The discovery page
166
166
  host: nex.toa.io
167
167
  accept: */*
168
168
  """
169
+ Then the following reply is sent:
170
+ """
171
+ 302 Moved Temporarily
172
+ location: /.discovery/
173
+ """
174
+
175
+ Scenario: A client that asked for something else gets what it asked for
176
+ An `accept` naming what the gateway answers with is a client that wants the resource,
177
+ not the page.
178
+
179
+ When the following request is received:
180
+ """
181
+ GET / HTTP/1.1
182
+ host: nex.toa.io
183
+ accept: application/json
184
+ """
169
185
  Then the following reply is sent:
170
186
  """
171
187
  405 Method Not Allowed
@@ -464,3 +464,49 @@ Feature: Help
464
464
  """
465
465
  404 Not Found
466
466
  """
467
+
468
+ Scenario: A method nobody is meant to find
469
+ `help:method: null` takes the method out of every answer and leaves it as callable as it
470
+ was — what a monitor reaches for, and what a person has no business going looking for.
471
+
472
+ Given the annotation:
473
+ """yaml
474
+ /:
475
+ anonymous: true
476
+ /hello:
477
+ GET:
478
+ dev:stub: hi
479
+ /agent:
480
+ GET:
481
+ help:method: null
482
+ dev:stub: hi
483
+ """
484
+ When the following request is received:
485
+ """
486
+ OPTIONS /.discovery HTTP/1.1
487
+ host: nex.toa.io
488
+ accept: application/yaml
489
+ """
490
+ Then the following reply is sent:
491
+ """
492
+ 200 OK
493
+
494
+ routes:
495
+ /hello:
496
+ GET:
497
+ anonymous: true
498
+ """
499
+ And the reply does not contain:
500
+ """
501
+ /hello/agent
502
+ """
503
+ # and it answers as it always did
504
+ When the following request is received:
505
+ """
506
+ GET /hello/agent/ HTTP/1.1
507
+ host: nex.toa.io
508
+ """
509
+ Then the following reply is sent:
510
+ """
511
+ 200 OK
512
+ """
@@ -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
+ """
@@ -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,29 @@ 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
+ )
42
48
 
43
49
  /*
44
50
  * The page the gateway serves at `/.discovery/`, pointed at a fixture: the scenarios are
@@ -144,10 +150,13 @@ const CLOUDINARY = {
144
150
  }
145
151
  }
146
152
 
147
- process.env.TOA_STORAGES = JSON.stringify({
148
- octets: {
149
- provider: 'tmp',
150
- directory: Math.random().toString(36).substring(2)
151
- },
152
- ...(environment === undefined ? {} : CLOUDINARY)
153
- })
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
+ )
@@ -4,7 +4,7 @@ import tsflow from 'cucumber-tsflow'
4
4
 
5
5
  import { Factory } from '@toa.io/extensions.realtime'
6
6
  import * as boot from '@toa.io/boot'
7
- import { match, timeout } from '@toa.io/generic'
7
+ import { environment, match, timeout } from '@toa.io/generic'
8
8
  import { load as parse } from 'js-yaml'
9
9
  import { Agent } from '@toa.io/agent'
10
10
  import { Parameters } from './Parameters.js'
@@ -38,7 +38,7 @@ export class Realtime {
38
38
  for (const [event, property] of Object.entries(annotation))
39
39
  routes.push({ event, properties: [property] })
40
40
 
41
- process.env.TOA_REALTIME = JSON.stringify(routes)
41
+ environment.set('TOA_REALTIME', JSON.stringify(routes))
42
42
 
43
43
  const factory = new Factory(boot.host())
44
44
 
@@ -93,7 +93,7 @@ export class Realtime {
93
93
  await this.instance.disconnect()
94
94
 
95
95
  this.instance = null
96
- process.env.TOA_REALTIME = undefined
96
+ environment.delete('TOA_REALTIME')
97
97
  }
98
98
 
99
99
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.exposition",
3
- "version": "1.0.0-alpha.288",
3
+ "version": "1.0.0-alpha.289",
4
4
  "type": "module",
5
5
  "description": "Toa Exposition",
6
6
  "author": "temich <tema.gurtovoy@gmail.com>",
@@ -20,9 +20,9 @@
20
20
  "dependencies": {
21
21
  "@node-rs/bcrypt": "1.10.8",
22
22
  "@simplewebauthn/server": "14.0.0",
23
- "@toa.io/core": "1.0.0-alpha.288",
24
- "@toa.io/generic": "1.0.0-alpha.286",
25
- "@toa.io/schemas": "1.0.0-alpha.287",
23
+ "@toa.io/core": "1.0.0-alpha.289",
24
+ "@toa.io/generic": "1.0.0-alpha.289",
25
+ "@toa.io/schemas": "1.0.0-alpha.289",
26
26
  "content-type": "3.0.0",
27
27
  "jose": "6.2.10",
28
28
  "js-yaml": "5.4.1",
@@ -60,5 +60,5 @@
60
60
  "@types/cors": "2.8.19",
61
61
  "@types/negotiator": "0.6.5"
62
62
  },
63
- "gitHead": "d3d696f0a4e287c3c297ac81028d600ad7574112"
63
+ "gitHead": "4fcb482beac5896305d8084cdbc1d5fcf372d108"
64
64
  }
@@ -246,14 +246,3 @@ interface Stage {
246
246
  depart: SpanOptions
247
247
  }
248
248
 
249
- export const shortcuts: RTD.syntax.Shortcuts = new Map([
250
- ['anonymous', 'auth:anonymous'],
251
- ['anyone', 'auth:anyone'],
252
- ['id', 'auth:id'],
253
- ['role', 'auth:role'],
254
- ['rule', 'auth:rule'],
255
- ['incept', 'auth:incept'],
256
- ['input', 'io:input'],
257
- ['output', 'io:output'],
258
- ['languages', 'map:languages']
259
- ])
@@ -11,13 +11,23 @@ const HTML = 'text/html'
11
11
  * and `/` is usually not one of those — a person who typed the address into a browser is
12
12
  * looking for something to look at, and the page is the only thing here that is one.
13
13
  *
14
- * A client that takes anything is not one: `accept` has to prefer a page over what the
15
- * gateway answers with, which is what a browser sends and an API client does not.
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.
16
19
  */
17
20
  export function looking(context: Context): OutgoingMessage | null {
18
21
  if (context.request.method !== 'GET' || context.procedural) return null
19
22
 
20
- if (new Negotiator(context.request).mediaType([...types, HTML]) !== HTML) return null
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
+ }
21
28
 
22
29
  return { status: 302, headers: new Headers({ location: DISCOVERY + '/' }) }
23
30
  }
31
+
32
+ /** An `accept` that names nothing, which is the same as naming none. */
33
+ const ANYTHING = '*/*'