@toa.io/extensions.exposition 1.0.0-alpha.271 → 1.0.0-alpha.273

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 (151) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/components/{exposition.stash → exposition.atom}/manifest.toa.yaml +4 -4
  3. package/components/{exposition.stash → exposition.atom}/operations/meter.js +1 -1
  4. package/components/exposition.octets/manifest.toa.yaml +28 -10
  5. package/components/exposition.octets/operations/put.js +20 -5
  6. package/components/identity.bans/manifest.toa.yaml +26 -7
  7. package/components/identity.bans/operations/tsconfig.tsbuildinfo +1 -1
  8. package/components/identity.basic/manifest.toa.yaml +106 -30
  9. package/components/identity.basic/operations/authenticate.js +6 -3
  10. package/components/identity.basic/operations/authenticate.js.map +1 -1
  11. package/components/identity.basic/operations/incept.js +3 -2
  12. package/components/identity.basic/operations/incept.js.map +1 -1
  13. package/components/identity.basic/operations/transit.js +16 -5
  14. package/components/identity.basic/operations/transit.js.map +1 -1
  15. package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
  16. package/components/identity.basic/source/authenticate.ts +7 -3
  17. package/components/identity.basic/source/incept.ts +3 -2
  18. package/components/identity.basic/source/transit.ts +19 -5
  19. package/components/identity.credentials/manifest.toa.yaml +34 -13
  20. package/components/identity.credentials/operations/tsconfig.tsbuildinfo +1 -1
  21. package/components/identity.federation/manifest.toa.yaml +120 -28
  22. package/components/identity.federation/operations/authenticate.js +3 -2
  23. package/components/identity.federation/operations/authenticate.js.map +1 -1
  24. package/components/identity.federation/operations/create.js +4 -2
  25. package/components/identity.federation/operations/create.js.map +1 -1
  26. package/components/identity.federation/operations/lib/errors.d.ts +70 -11
  27. package/components/identity.federation/operations/lib/errors.js +40 -11
  28. package/components/identity.federation/operations/lib/errors.js.map +1 -1
  29. package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
  30. package/components/identity.federation/source/authenticate.ts +3 -2
  31. package/components/identity.federation/source/create.ts +4 -2
  32. package/components/identity.federation/source/lib/errors.ts +49 -12
  33. package/components/identity.keys/manifest.toa.yaml +14 -6
  34. package/components/identity.keys/operations/tsconfig.tsbuildinfo +1 -1
  35. package/components/identity.otp/manifest.toa.yaml +44 -15
  36. package/components/identity.otp/operations/authenticate.js +9 -4
  37. package/components/identity.otp/operations/authenticate.js.map +1 -1
  38. package/components/identity.otp/operations/tsconfig.tsbuildinfo +1 -1
  39. package/components/identity.otp/source/authenticate.ts +11 -4
  40. package/components/identity.passkeys/manifest.toa.yaml +38 -8
  41. package/components/identity.passkeys/operations/authenticate.js +3 -2
  42. package/components/identity.passkeys/operations/authenticate.js.map +1 -1
  43. package/components/identity.passkeys/operations/create.js +6 -3
  44. package/components/identity.passkeys/operations/create.js.map +1 -1
  45. package/components/identity.passkeys/operations/tsconfig.tsbuildinfo +1 -1
  46. package/components/identity.passkeys/operations/use.js +6 -3
  47. package/components/identity.passkeys/operations/use.js.map +1 -1
  48. package/components/identity.passkeys/source/authenticate.ts +3 -2
  49. package/components/identity.passkeys/source/create.ts +7 -3
  50. package/components/identity.passkeys/source/use.ts +7 -3
  51. package/components/identity.roles/manifest.toa.yaml +41 -10
  52. package/components/identity.roles/operations/grant.js +3 -2
  53. package/components/identity.roles/operations/grant.js.map +1 -1
  54. package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
  55. package/components/identity.roles/source/grant.ts +3 -2
  56. package/components/identity.tokens/manifest.toa.yaml +99 -39
  57. package/components/identity.tokens/operations/authenticate.js +6 -3
  58. package/components/identity.tokens/operations/authenticate.js.map +1 -1
  59. package/components/identity.tokens/operations/decrypt.js +9 -4
  60. package/components/identity.tokens/operations/decrypt.js.map +1 -1
  61. package/components/identity.tokens/operations/encrypt.js +3 -2
  62. package/components/identity.tokens/operations/encrypt.js.map +1 -1
  63. package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
  64. package/components/identity.tokens/source/authenticate.ts +7 -3
  65. package/components/identity.tokens/source/decrypt.ts +11 -4
  66. package/components/identity.tokens/source/encrypt.ts +3 -2
  67. package/documentation/identity.md +4 -1
  68. package/documentation/notes/throttling.md +1 -1
  69. package/features/cors.feature +1 -1
  70. package/features/dev.feature +1 -1
  71. package/features/identity.federation.feature +3 -2
  72. package/features/{identtiy.tokens.custom.feature → identity.tokens.custom.feature} +39 -0
  73. package/features/introspection.feature +27 -34
  74. package/features/map.feature +1 -1
  75. package/features/octets.download.feature +1 -1
  76. package/features/octets.meta.feature +1 -1
  77. package/features/passkeys.feature +8 -8
  78. package/features/query.feature +27 -0
  79. package/features/require.feature +2 -2
  80. package/features/response.feature +3 -3
  81. package/features/steps/Components.ts +12 -4
  82. package/features/steps/Database.ts +9 -0
  83. package/features/steps/Gateway.ts +13 -13
  84. package/features/steps/HTTP.ts +3 -5
  85. package/features/steps/Parameters.ts +2 -3
  86. package/features/steps/Realtime.ts +4 -4
  87. package/features/steps/Workspace.ts +8 -3
  88. package/features/steps/components/audit/manifest.toa.yaml +12 -5
  89. package/features/steps/components/echo/manifest.toa.yaml +33 -8
  90. package/features/steps/components/notify/manifest.toa.yaml +12 -5
  91. package/features/steps/components/octets.tester/manifest.toa.yaml +43 -14
  92. package/features/steps/components/orders/manifest.toa.yaml +22 -5
  93. package/features/steps/components/pots/manifest.toa.yaml +71 -17
  94. package/features/steps/components/pricing/manifest.toa.yaml +8 -2
  95. package/features/steps/components/sequences/manifest.toa.yaml +3 -1
  96. package/features/steps/components/users/manifest.toa.yaml +18 -3
  97. package/features/steps/components/users.properties/manifest.toa.yaml +12 -2
  98. package/package.json +6 -7
  99. package/schemas/annotation.cos.yaml +37 -17
  100. package/schemas/directive.cos.yaml +7 -3
  101. package/schemas/io/throttle.cos.yaml +47 -8
  102. package/schemas/method.cos.yaml +21 -9
  103. package/schemas/node.cos.yaml +22 -7
  104. package/schemas/octets/delete.cos.yaml +15 -2
  105. package/schemas/octets/get.cos.yaml +5 -2
  106. package/schemas/octets/put.cos.yaml +6 -3
  107. package/schemas/octets/workflow.cos.yaml +0 -1
  108. package/schemas/query.cos.yaml +31 -14
  109. package/schemas/querystring.cos.yaml +19 -7
  110. package/schemas/range.cos.yaml +10 -2
  111. package/schemas/route.cos.yaml +6 -2
  112. package/source/Branch.test.ts +37 -0
  113. package/source/Branch.ts +37 -0
  114. package/source/Factory.ts +5 -5
  115. package/source/Gateway.ts +29 -14
  116. package/source/HTTP/formats/yaml.test.ts +4 -2
  117. package/source/RTD/Tree.ts +4 -1
  118. package/source/Tenant.ts +15 -4
  119. package/source/deployment.ts +3 -4
  120. package/source/directives/io/IO.ts +2 -2
  121. package/source/directives/io/lib/throttle/Sync.ts +4 -4
  122. package/source/directives/map/Headers.ts +8 -1
  123. package/source/directives/octets/Put.ts +6 -3
  124. package/source/root.ts +2 -2
  125. package/transpiled/Branch.d.ts +23 -0
  126. package/transpiled/Branch.js +16 -0
  127. package/transpiled/Branch.js.map +1 -1
  128. package/transpiled/Factory.js +4 -4
  129. package/transpiled/Factory.js.map +1 -1
  130. package/transpiled/Gateway.d.ts +7 -0
  131. package/transpiled/Gateway.js +26 -8
  132. package/transpiled/Gateway.js.map +1 -1
  133. package/transpiled/RTD/Tree.js +4 -1
  134. package/transpiled/RTD/Tree.js.map +1 -1
  135. package/transpiled/Tenant.d.ts +8 -2
  136. package/transpiled/Tenant.js +12 -2
  137. package/transpiled/Tenant.js.map +1 -1
  138. package/transpiled/deployment.js +3 -4
  139. package/transpiled/deployment.js.map +1 -1
  140. package/transpiled/directives/io/IO.d.ts +1 -1
  141. package/transpiled/directives/io/IO.js +2 -2
  142. package/transpiled/directives/io/IO.js.map +1 -1
  143. package/transpiled/directives/io/lib/throttle/Sync.d.ts +2 -2
  144. package/transpiled/directives/io/lib/throttle/Sync.js +4 -4
  145. package/transpiled/directives/io/lib/throttle/Sync.js.map +1 -1
  146. package/transpiled/directives/map/Headers.js +7 -1
  147. package/transpiled/directives/map/Headers.js.map +1 -1
  148. package/transpiled/directives/octets/Put.js.map +1 -1
  149. package/transpiled/root.js +1 -1
  150. package/transpiled/root.js.map +1 -1
  151. package/transpiled/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,3 @@
1
- import { Err } from 'error-value'
2
1
  import type { Maybe, Operation } from '@toa.io/types'
3
2
  import type { AuthenticateInput, AuthenticateOutput, Context } from './lib'
4
3
 
@@ -43,5 +42,10 @@ export class Computation implements Operation {
43
42
  }
44
43
  }
45
44
 
46
- const ERR_AUTHORITY = new Err('AUTHORITY_MISMATCH')
47
- const ERR_TOKEN_REVOKED = new Err('TOKEN_REVOKED')
45
+ const ERR_AUTHORITY = new (class AuthorityMismatchError extends Error {
46
+ public readonly code = 'AUTHORITY_MISMATCH'
47
+ })()
48
+
49
+ const ERR_TOKEN_REVOKED = new (class TokenRevokedError extends Error {
50
+ public readonly code = 'TOKEN_REVOKED'
51
+ })()
@@ -1,5 +1,4 @@
1
1
  import { V3 } from 'paseto'
2
- import { Err } from 'error-value'
3
2
  import { LRUCache } from 'lru-cache'
4
3
  import { jweKey } from './lib'
5
4
  import { load } from './lib/jose'
@@ -161,6 +160,14 @@ interface KeyEntry {
161
160
  value: Key | null
162
161
  }
163
162
 
164
- const ERR_INVALID_TOKEN = new Err('INVALID_TOKEN')
165
- const ERR_INVALID_KEY = new Err('INVALID_KEY')
166
- const ERR_FORGED_KEY = new Err('FORGED_KEY')
163
+ const ERR_INVALID_TOKEN = new (class InvalidTokenError extends Error {
164
+ public readonly code = 'INVALID_TOKEN'
165
+ })()
166
+
167
+ const ERR_INVALID_KEY = new (class InvalidKeyError extends Error {
168
+ public readonly code = 'INVALID_KEY'
169
+ })()
170
+
171
+ const ERR_FORGED_KEY = new (class ForgedKeyError extends Error {
172
+ public readonly code = 'FORGED_KEY'
173
+ })()
@@ -1,4 +1,3 @@
1
- import { Err } from 'error-value'
2
1
  import { jweKey } from './lib'
3
2
  import { load } from './lib/jose'
4
3
  import type { Operation, Maybe } from '@toa.io/types'
@@ -52,4 +51,6 @@ function within (scope: string, roles: string[]): boolean {
52
51
  return roles.some((role) => role === scope || scope.startsWith(role + ':'))
53
52
  }
54
53
 
55
- const ERR_INACCESSIBLE_SCOPE = new Err('INACCESSIBLE_SCOPE')
54
+ const ERR_INACCESSIBLE_SCOPE = new (class InaccessibleScopeError extends Error {
55
+ public readonly code = 'INACCESSIBLE_SCOPE'
56
+ })()
@@ -181,7 +181,10 @@ name: users
181
181
 
182
182
  entity:
183
183
  schema:
184
- name: string
184
+ type: object
185
+ properties:
186
+ name:
187
+ type: string
185
188
 
186
189
  exposition:
187
190
  /:
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Superseded.** The design below was never implemented. `io:throttle` is now a
4
4
  > [GCRA](https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm) deciding in each process, with
5
- > its state periodically reconciled through Redis by way of the `stash` extension's `meter`. It
5
+ > its state periodically reconciled through Redis by way of the `atom` aspect's `meter`. It
6
6
  > answers the same forces the same way — decide locally, reconcile in the background, never fail the
7
7
  > request path — and the section below records what it does instead. The problem statement and the
8
8
  > forces have not changed.
@@ -20,7 +20,7 @@ Feature: CORS Support
20
20
  """
21
21
  204 No Content
22
22
  access-control-allow-credentials: true
23
- access-control-allow-headers: accept, authorization, content-type, if-match, if-none-match, origin
23
+ access-control-allow-headers: accept, authorization, content-type, if-match, if-none-match
24
24
  access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, LOCK, UNLOCK
25
25
  access-control-allow-origin: https://hello.world
26
26
  access-control-max-age: 3600
@@ -74,7 +74,7 @@ Feature: Dev
74
74
  Then the following reply is sent:
75
75
  """
76
76
  204 No Content
77
- access-control-allow-headers: accept, authorization, content-type, if-match, if-none-match, faulty
77
+ access-control-allow-headers: accept, authorization, content-type, faulty, if-match, if-none-match
78
78
  """
79
79
  When the following request is received:
80
80
  """
@@ -195,13 +195,14 @@ Feature: Identity Federation
195
195
 
196
196
  Then after 0.2 seconds
197
197
 
198
- # check the role
198
+ # the role is checked over the id token: a Token carries the roles held when it
199
+ # was issued, and this one was issued before the grant
199
200
  When the following request is received:
200
201
  """
201
202
  GET /identity/ HTTP/1.1
202
203
  host: nex.toa.io
203
204
  accept: application/yaml
204
- authorization: Token ${{ root.token }}
205
+ authorization: Bearer ${{ root.id_token }}
205
206
  """
206
207
  Then the following reply is sent:
207
208
  """
@@ -76,6 +76,45 @@ Feature: Custom tokens
76
76
  200 OK
77
77
  """
78
78
 
79
+ Scenario: Issuing a token for another identity
80
+ Given the `identity.basic` database contains:
81
+ | _id | authority | username | password |
82
+ | efe3a65ebbee47ed95a73edd911ea328 | nex | developer | $2b$10$ZRSKkgZoGnrcTNA5w5eCcu3pxDzdTduhteVYXcp56AaNcilNkwJ.O |
83
+ | e8e4f9c2a68d419b861403d71fabc915 | nex | user | $2b$10$Frszmrmsz9iwSXzBbRRMKeDVKsNxozkrLNSsN.SnVC.KPxLtQr/bK |
84
+ And the `identity.roles` database contains:
85
+ | _id | identity | role |
86
+ | 9c4702490ff84f2a9e1b1da2ab64bdd4 | efe3a65ebbee47ed95a73edd911ea328 | system:identity:tokens |
87
+ When the following request is received:
88
+ """
89
+ POST /identity/tokens/e8e4f9c2a68d419b861403d71fabc915/ HTTP/1.1
90
+ host: nex.toa.io
91
+ authorization: Basic ZGV2ZWxvcGVyOnNlY3JldA==
92
+ content-type: application/yaml
93
+ accept: application/yaml
94
+
95
+ label: On behalf
96
+ lifetime: 600
97
+ """
98
+ Then the following reply is sent:
99
+ """
100
+ 201 Created
101
+
102
+ token: ${{ token }}
103
+ """
104
+ When the following request is received:
105
+ """
106
+ GET /identity/ HTTP/1.1
107
+ host: nex.toa.io
108
+ authorization: Token ${{ token }}
109
+ accept: application/yaml
110
+ """
111
+ Then the following reply is sent:
112
+ """
113
+ 200 OK
114
+
115
+ id: e8e4f9c2a68d419b861403d71fabc915
116
+ """
117
+
79
118
  Scenario: Token with restricted scopes
80
119
  When the following request is received:
81
120
  """
@@ -21,55 +21,48 @@ Feature: Introspection
21
21
  Allow: GET, POST
22
22
 
23
23
  GET:
24
- type: array
25
- items:
26
- type: object
24
+ output:
25
+ type: array
26
+ items:
27
+ properties:
28
+ id:
29
+ type: string
30
+ pattern: ^[a-fA-F0-9]{32}$
31
+ title:
32
+ type: string
33
+ maxLength: 64
34
+ volume:
35
+ type: number
36
+ exclusiveMinimum: 0
37
+ maximum: 1000
38
+ temperature:
39
+ type: number
40
+ exclusiveMinimum: 0
41
+ maximum: 300
42
+ type: object
43
+ required:
44
+ - id
45
+ - title
46
+ - volume
47
+ POST:
48
+ input:
27
49
  properties:
28
- id:
29
- type: string
30
- pattern: ^[a-fA-F0-9]{32}$
31
- title:
32
- type: string
33
- maxLength: 64
34
- volume:
35
- type: number
36
- exclusiveMinimum: 0
37
- maximum: 1000
38
50
  temperature:
39
51
  type: number
40
52
  exclusiveMinimum: 0
41
53
  maximum: 300
42
- additionalProperties: false
43
- required:
44
- - id
45
- - title
46
- - volume
47
- POST:
48
- input:
49
- type: object
50
- properties:
51
54
  title:
52
55
  type: string
53
56
  maxLength: 64
54
- temperature:
55
- type: number
56
- exclusiveMinimum: 0
57
- maximum: 300
58
57
  volume:
59
58
  type: number
60
59
  exclusiveMinimum: 0
61
60
  maximum: 1000
62
- additionalProperties: false
61
+ type: object
63
62
  required:
64
63
  - title
65
64
  - volume
66
- output:
67
- type: object
68
- properties:
69
- id:
70
- type: string
71
- pattern: ^[a-fA-F0-9]{32}$
72
- additionalProperties: false
65
+ output: {}
73
66
  errors:
74
67
  - NO_WAY
75
68
  - WONT_CREATE
@@ -84,7 +84,7 @@ Feature: HTTP context mapping
84
84
  Then the following reply is sent:
85
85
  """
86
86
  204 No Content
87
- access-control-allow-headers: accept, accept-language, authorization, content-type, foo, if-match, if-none-match, origin
87
+ access-control-allow-headers: accept, accept-language, authorization, content-type, foo, if-match, if-none-match
88
88
  """
89
89
 
90
90
  Scenario: Mapping the value of an arbitrary header
@@ -186,5 +186,5 @@ Feature: Download external resources
186
186
  Then the following reply is sent:
187
187
  """
188
188
  204 No Content
189
- access-control-allow-headers: accept, authorization, content-attributes, content-location, content-type, if-match, if-none-match, origin
189
+ access-control-allow-headers: accept, authorization, content-attributes, content-location, content-type, if-match, if-none-match
190
190
  """
@@ -111,6 +111,6 @@ Feature: Octets metadata
111
111
  Then the following reply is sent:
112
112
  """
113
113
  204 No Content
114
- access-control-allow-headers: accept, authorization, content-attributes, content-location, content-type, if-match, if-none-match, origin
114
+ access-control-allow-headers: accept, authorization, content-attributes, content-location, content-type, if-match, if-none-match
115
115
  access-control-allow-origin: https://example.com
116
116
  """
@@ -20,16 +20,16 @@ Feature: Web Authentication
20
20
  201 Created
21
21
 
22
22
  challenge: ${{ challenge }}
23
- identity: ${{ identity.id }}
24
23
  timeout: 60000
25
- authenticatorSelection:
26
- requireResidentKey: true
27
- residentKey: required
24
+ identity: ${{ identity.id }}
28
25
  pubKeyCredParams:
29
26
  - type: public-key
30
27
  alg: -7
31
28
  - type: public-key
32
29
  alg: -257
30
+ authenticatorSelection:
31
+ residentKey: required
32
+ requireResidentKey: true
33
33
  """
34
34
 
35
35
  Scenario: Create a challenge to create a passkey to new identity
@@ -53,14 +53,14 @@ Feature: Web Authentication
53
53
  authorization: Token ${{ token }}
54
54
 
55
55
  challenge: ${{ challenge }}
56
- identity: ${{ identity }}
57
56
  timeout: 60000
58
- authenticatorSelection:
59
- requireResidentKey: true
60
- residentKey: required
57
+ identity: ${{ identity }}
61
58
  pubKeyCredParams:
62
59
  - type: public-key
63
60
  alg: -7
64
61
  - type: public-key
65
62
  alg: -257
63
+ authenticatorSelection:
64
+ residentKey: required
65
+ requireResidentKey: true
66
66
  """
@@ -283,6 +283,33 @@ Feature: Queries
283
283
 
284
284
  Hello John
285
285
  """
286
+
287
+ # a declared parameter is taken out of the query string before what is left of it is
288
+ # answered for. A route that declares nothing but parameters is not queryable, so what
289
+ # is left has to be nothing at all
290
+ Scenario: Query parameters beside an undeclared one
291
+ Given the `echo` is running with the following manifest:
292
+ """yaml
293
+ exposition:
294
+ /:
295
+ GET:
296
+ io:output: true
297
+ query:
298
+ parameters: [name]
299
+ endpoint: compute
300
+ """
301
+ When the following request is received:
302
+ """
303
+ GET /echo/?name=John&foo=bar HTTP/1.1
304
+ host: nex.toa.io
305
+ accept: text/plain
306
+ """
307
+ Then the following reply is sent:
308
+ """
309
+ 400 Bad Request
310
+
311
+ Query parameter 'foo' is not allowed
312
+ """
286
313
  When the following request is received:
287
314
  """
288
315
  GET /echo/?foo=bar HTTP/1.1
@@ -27,7 +27,7 @@ Feature: Request requirements
27
27
  Then the following reply is sent:
28
28
  """
29
29
  201 Created
30
- etag: ${{ etag }}
30
+ etag: "${{ etag }}"
31
31
 
32
32
  id: ${{ id }}
33
33
  """
@@ -53,7 +53,7 @@ Feature: Request requirements
53
53
  host: nex.toa.io
54
54
  content-type: application/yaml
55
55
  accept: text/plain
56
- if-match: ${{ etag }}
56
+ if-match: "${{ etag }}"
57
57
 
58
58
  title: Bye
59
59
  """
@@ -19,8 +19,8 @@ Feature: Response
19
19
  Then the following reply is sent:
20
20
  """
21
21
  200 OK
22
+ vary: origin, accept
22
23
  content-type: application/json
23
- vary: accept
24
24
 
25
25
  {"hello":"world"}
26
26
  """
@@ -33,8 +33,8 @@ Feature: Response
33
33
  Then the following reply is sent:
34
34
  """
35
35
  200 OK
36
+ vary: origin, accept
36
37
  content-type: application/yaml
37
- vary: accept
38
38
 
39
39
  hello: world
40
40
  """
@@ -48,8 +48,8 @@ Feature: Response
48
48
  Then the following reply is sent:
49
49
  """
50
50
  200 OK
51
+ vary: origin, accept
51
52
  content-type: application/json
52
- vary: accept
53
53
 
54
54
  {"hello":"world"}
55
55
  """
@@ -3,19 +3,22 @@ import { after, binding, given } from 'cucumber-tsflow'
3
3
  import * as boot from '@toa.io/boot'
4
4
  import { timeout } from '@toa.io/generic'
5
5
  import { type Connector } from '@toa.io/core'
6
- import { parse } from '@toa.io/yaml'
6
+ import { load as parse } from 'js-yaml'
7
+ import { Gateway } from './Gateway'
7
8
  import { Workspace } from './Workspace'
8
9
  import { components as map } from './map'
9
10
 
10
11
  const MAP = 'introspection'
11
12
 
12
- @binding([Workspace])
13
+ @binding([Workspace, Gateway])
13
14
  export class Components {
14
15
  private readonly workspace: Workspace
16
+ private readonly gateway: Gateway
15
17
  private compositions: Record<string, Connector> = {}
16
18
 
17
- public constructor (workspace: Workspace) {
19
+ public constructor (workspace: Workspace, gateway: Gateway) {
18
20
  this.workspace = workspace
21
+ this.gateway = gateway
19
22
  }
20
23
 
21
24
  @given('the `{word}` is running')
@@ -25,7 +28,7 @@ export class Components {
25
28
 
26
29
  @given('the `{word}` is running with the following manifest:')
27
30
  public async patchAndRun (name: string, yaml: string): Promise<void> {
28
- const manifest = parse(yaml)
31
+ const manifest = parse(yaml) as object
29
32
 
30
33
  await this.runComponent(name, manifest)
31
34
  }
@@ -59,6 +62,11 @@ export class Components {
59
62
  private async runComponent (name: string, manifest?: object): Promise<void> {
60
63
  assert.ok(!(name in this.compositions), `Composition '${name}' is already running`)
61
64
 
65
+ // the gateway first: a component announces itself when it opens, and that only
66
+ // reaches a gateway that is already listening. Started the other way round, the
67
+ // component waits for a knock, which the request that follows does not.
68
+ await this.gateway.start()
69
+
62
70
  const path = await this.workspace.addComponent(name, manifest)
63
71
 
64
72
  this.compositions[name] = await boot.composition([path])
@@ -32,6 +32,15 @@ export class Database {
32
32
  : str
33
33
  }
34
34
 
35
+ /*
36
+ * A record that has never been written carries no timestamps, and the entity stamps
37
+ * the ones it is missing as it is read — with the time it was read. The same row then
38
+ * enumerates differently on every request, which is a moving `etag` and a collection
39
+ * that is never unmodified.
40
+ */
41
+ document._created ??= Date.now()
42
+ document._updated ??= document._created
43
+
35
44
  documents.push(document)
36
45
  }
37
46
 
@@ -1,8 +1,8 @@
1
1
  import { after, afterAll, binding, given } from 'cucumber-tsflow'
2
2
  import * as boot from '@toa.io/boot'
3
3
  import { type Connector } from '@toa.io/core'
4
- import { parse } from '@toa.io/yaml'
5
- import { encode, timeout } from '@toa.io/generic'
4
+ import { load as parse } from 'js-yaml'
5
+ import { timeout } from '@toa.io/generic'
6
6
  import { Factory } from '../../source'
7
7
  import * as syntax from '../../source/RTD/syntax'
8
8
  import { shortcuts } from '../../source/Directive'
@@ -17,12 +17,12 @@ export class Gateway {
17
17
 
18
18
  @given('the annotation:')
19
19
  public async annotate (yaml: string): Promise<void> {
20
- const annotation = parse(yaml)
20
+ const annotation = parse(yaml) as Partial<http.Options> & { '/'?: object }
21
21
 
22
- if ('/' in annotation) {
22
+ if (annotation['/'] !== undefined) {
23
23
  const tree = syntax.parse(annotation['/'], shortcuts)
24
24
 
25
- process.env.TOA_EXPOSITION = encode(tree)
25
+ process.env.TOA_EXPOSITION = JSON.stringify(tree)
26
26
  }
27
27
 
28
28
  const { debug, authorities } = annotation
@@ -34,7 +34,7 @@ export class Gateway {
34
34
  if (authorities !== undefined)
35
35
  properties.authorities = authorities
36
36
 
37
- process.env.TOA_EXPOSITION_PROPERTIES = encode(properties)
37
+ process.env.TOA_EXPOSITION_PROPERTIES = JSON.stringify(properties)
38
38
 
39
39
  await Gateway.stop()
40
40
 
@@ -58,8 +58,8 @@ export class Gateway {
58
58
  tree.routes.push(...node.routes)
59
59
  }
60
60
 
61
- process.env.TOA_EXPOSITION = encode(tree)
62
- process.env.TOA_EXPOSITION_PROPERTIES = encode(DEFAULT_PROPERTIES)
61
+ process.env.TOA_EXPOSITION = JSON.stringify(tree)
62
+ process.env.TOA_EXPOSITION_PROPERTIES = JSON.stringify(DEFAULT_PROPERTIES)
63
63
 
64
64
  await Gateway.stop()
65
65
 
@@ -71,10 +71,10 @@ export class Gateway {
71
71
  const [name, namespace = 'default'] = id.split('.').reverse()
72
72
  const key = `TOA_CONFIGURATION_${namespace.toUpperCase()}_${name.toUpperCase()}`
73
73
  const def = DEFAULT_CONFIGURATION[id] ?? {}
74
- const patch: object = parse(yaml)
74
+ const patch = parse(yaml) as object
75
75
  const configuration = Object.assign({}, def, patch)
76
76
 
77
- process.env[key] = encode(configuration)
77
+ process.env[key] = JSON.stringify(configuration)
78
78
 
79
79
  await Gateway.stop()
80
80
 
@@ -96,7 +96,7 @@ export class Gateway {
96
96
  return
97
97
 
98
98
  process.env.TOA_EXPOSITION ??= DEFAULT_TREE
99
- process.env.TOA_EXPOSITION_PROPERTIES ??= encode(DEFAULT_PROPERTIES)
99
+ process.env.TOA_EXPOSITION_PROPERTIES ??= JSON.stringify(DEFAULT_PROPERTIES)
100
100
 
101
101
  this.writeConfiguration()
102
102
 
@@ -136,14 +136,14 @@ export class Gateway {
136
136
  const [name, namespace = 'default'] = id.split('.').reverse()
137
137
  const key = `TOA_CONFIGURATION_${namespace.toUpperCase()}_${name.toUpperCase()}`
138
138
 
139
- process.env[key] ??= encode(configuration)
139
+ process.env[key] ??= JSON.stringify(configuration)
140
140
  }
141
141
  }
142
142
  }
143
143
 
144
144
  const EXPOSITION = '@toa.io/extensions.exposition'
145
145
 
146
- const DEFAULT_TREE = encode({
146
+ const DEFAULT_TREE = JSON.stringify({
147
147
  routes: [],
148
148
  methods: [],
149
149
  directives: [
@@ -13,7 +13,6 @@ import type { Readable } from 'node:stream'
13
13
  @binding([Gateway, Parameters, Captures])
14
14
  export class HTTP extends http.Agent {
15
15
  private readonly gateway: Gateway
16
- private fetched: Response | null = null
17
16
 
18
17
  public constructor (gateway: Gateway, parameters: Parameters, captures: Captures) {
19
18
  super(parameters.origin, captures)
@@ -24,7 +23,7 @@ export class HTTP extends http.Agent {
24
23
  public override async request (input: string): Promise<any> {
25
24
  await this.gateway.start()
26
25
 
27
- this.fetched = await super.request(input)
26
+ await super.request(input)
28
27
  }
29
28
 
30
29
  @then('the following reply is sent:')
@@ -34,10 +33,9 @@ export class HTTP extends http.Agent {
34
33
 
35
34
  @then('response body contains {word}-encoded value:')
36
35
  public async bodyIs (format: string, yaml: string): Promise<void> {
37
- assert.ok(this.fetched !== null, 'Response is null')
36
+ assert.ok(this.bytes !== null, 'Response body is not available')
38
37
 
39
- const buf = await this.fetched.arrayBuffer()
40
- const value = encoders[format]?.(buf as unknown as Buffer)
38
+ const value = encoders[format]?.(this.bytes)
41
39
  const expected = YAML.load(yaml)
42
40
 
43
41
  assert.deepEqual(value, expected, 'Values are not equal')
@@ -1,7 +1,6 @@
1
1
  import { join } from 'node:path'
2
2
  import * as dotenv from 'dotenv'
3
3
  import { setDefaultTimeout } from '@cucumber/cucumber'
4
- import { encode } from '@toa.io/generic'
5
4
 
6
5
  dotenv.config({ path: join(__dirname, '.env') })
7
6
 
@@ -19,14 +18,14 @@ process.env.TOA_DEV = '1'
19
18
 
20
19
  // export traces to the local Tempo (`docker compose up tempo grafana`),
21
20
  // unavailability of the endpoint is harmless
22
- process.env.TOA_TELEMETRY_TRACES ??= encode({
21
+ process.env.TOA_TELEMETRY_TRACES ??= JSON.stringify({
23
22
  exporters: {
24
23
  console: null,
25
24
  otlp: { endpoint: 'http://localhost:4318' }
26
25
  }
27
26
  })
28
27
 
29
- process.env.TOA_STORAGES = encode({
28
+ process.env.TOA_STORAGES = JSON.stringify({
30
29
  octets: {
31
30
  provider: 'tmp',
32
31
  directory: Math.random().toString(36).substring(2)
@@ -3,8 +3,8 @@ import * as assert from 'node:assert'
3
3
  import { after, binding, given, afterAll, then } from 'cucumber-tsflow'
4
4
  import { Factory } from '@toa.io/extensions.realtime'
5
5
  import * as boot from '@toa.io/boot'
6
- import { encode, match } from '@toa.io/generic'
7
- import { parse } from '@toa.io/yaml'
6
+ import { match } from '@toa.io/generic'
7
+ import { load as parse } from 'js-yaml'
8
8
  import { Agent } from '@toa.io/agent'
9
9
  import { Parameters } from './Parameters'
10
10
  import { Gateway } from './Gateway'
@@ -29,13 +29,13 @@ export class Realtime {
29
29
  public async start (yaml: string): Promise<void> {
30
30
  await Realtime.stop()
31
31
 
32
- const annotation = parse(yaml)
32
+ const annotation = parse(yaml) as Record<string, string>
33
33
  const routes = []
34
34
 
35
35
  for (const [event, property] of Object.entries(annotation))
36
36
  routes.push({ event, properties: [property] })
37
37
 
38
- process.env.TOA_REALTIME = encode(routes)
38
+ process.env.TOA_REALTIME = JSON.stringify(routes)
39
39
 
40
40
  const factory = new Factory(boot)
41
41
 
@@ -1,7 +1,8 @@
1
1
  import { join } from 'node:path'
2
2
  import { tmpdir, devNull } from 'node:os'
3
- import { mkdtemp, cp } from 'node:fs/promises'
4
- import * as yaml from '@toa.io/yaml'
3
+ import { mkdtemp, cp, readFile, writeFile } from 'node:fs/promises'
4
+ import { load, dump } from 'js-yaml'
5
+ import { overwrite } from '@toa.io/generic'
5
6
 
6
7
  export class Workspace {
7
8
  private root: string = devNull
@@ -35,6 +36,10 @@ export class Workspace {
35
36
  private async patchManifest (target: string, patch: object): Promise<void> {
36
37
  const path = join(target, 'manifest.toa.yaml')
37
38
 
38
- await yaml.patch(path, patch)
39
+ const manifest = load(await readFile(path, 'utf8')) as object
40
+
41
+ overwrite(manifest, patch)
42
+
43
+ await writeFile(path, dump(manifest, { noRefs: true, lineWidth: -1 }), 'utf8')
39
44
  }
40
45
  }