@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.
- package/CHANGELOG.md +40 -0
- package/components/{exposition.stash → exposition.atom}/manifest.toa.yaml +4 -4
- package/components/{exposition.stash → exposition.atom}/operations/meter.js +1 -1
- package/components/exposition.octets/manifest.toa.yaml +28 -10
- package/components/exposition.octets/operations/put.js +20 -5
- package/components/identity.bans/manifest.toa.yaml +26 -7
- package/components/identity.bans/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.basic/manifest.toa.yaml +106 -30
- package/components/identity.basic/operations/authenticate.js +6 -3
- package/components/identity.basic/operations/authenticate.js.map +1 -1
- package/components/identity.basic/operations/incept.js +3 -2
- package/components/identity.basic/operations/incept.js.map +1 -1
- package/components/identity.basic/operations/transit.js +16 -5
- package/components/identity.basic/operations/transit.js.map +1 -1
- package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.basic/source/authenticate.ts +7 -3
- package/components/identity.basic/source/incept.ts +3 -2
- package/components/identity.basic/source/transit.ts +19 -5
- package/components/identity.credentials/manifest.toa.yaml +34 -13
- package/components/identity.credentials/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/manifest.toa.yaml +120 -28
- package/components/identity.federation/operations/authenticate.js +3 -2
- package/components/identity.federation/operations/authenticate.js.map +1 -1
- package/components/identity.federation/operations/create.js +4 -2
- package/components/identity.federation/operations/create.js.map +1 -1
- package/components/identity.federation/operations/lib/errors.d.ts +70 -11
- package/components/identity.federation/operations/lib/errors.js +40 -11
- package/components/identity.federation/operations/lib/errors.js.map +1 -1
- package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/source/authenticate.ts +3 -2
- package/components/identity.federation/source/create.ts +4 -2
- package/components/identity.federation/source/lib/errors.ts +49 -12
- package/components/identity.keys/manifest.toa.yaml +14 -6
- package/components/identity.keys/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.otp/manifest.toa.yaml +44 -15
- package/components/identity.otp/operations/authenticate.js +9 -4
- package/components/identity.otp/operations/authenticate.js.map +1 -1
- package/components/identity.otp/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.otp/source/authenticate.ts +11 -4
- package/components/identity.passkeys/manifest.toa.yaml +38 -8
- package/components/identity.passkeys/operations/authenticate.js +3 -2
- package/components/identity.passkeys/operations/authenticate.js.map +1 -1
- package/components/identity.passkeys/operations/create.js +6 -3
- package/components/identity.passkeys/operations/create.js.map +1 -1
- package/components/identity.passkeys/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/operations/use.js +6 -3
- package/components/identity.passkeys/operations/use.js.map +1 -1
- package/components/identity.passkeys/source/authenticate.ts +3 -2
- package/components/identity.passkeys/source/create.ts +7 -3
- package/components/identity.passkeys/source/use.ts +7 -3
- package/components/identity.roles/manifest.toa.yaml +41 -10
- package/components/identity.roles/operations/grant.js +3 -2
- package/components/identity.roles/operations/grant.js.map +1 -1
- package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.roles/source/grant.ts +3 -2
- package/components/identity.tokens/manifest.toa.yaml +99 -39
- package/components/identity.tokens/operations/authenticate.js +6 -3
- package/components/identity.tokens/operations/authenticate.js.map +1 -1
- package/components/identity.tokens/operations/decrypt.js +9 -4
- package/components/identity.tokens/operations/decrypt.js.map +1 -1
- package/components/identity.tokens/operations/encrypt.js +3 -2
- package/components/identity.tokens/operations/encrypt.js.map +1 -1
- package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/source/authenticate.ts +7 -3
- package/components/identity.tokens/source/decrypt.ts +11 -4
- package/components/identity.tokens/source/encrypt.ts +3 -2
- package/documentation/identity.md +4 -1
- package/documentation/notes/throttling.md +1 -1
- package/features/cors.feature +1 -1
- package/features/dev.feature +1 -1
- package/features/identity.federation.feature +3 -2
- package/features/{identtiy.tokens.custom.feature → identity.tokens.custom.feature} +39 -0
- package/features/introspection.feature +27 -34
- package/features/map.feature +1 -1
- package/features/octets.download.feature +1 -1
- package/features/octets.meta.feature +1 -1
- package/features/passkeys.feature +8 -8
- package/features/query.feature +27 -0
- package/features/require.feature +2 -2
- package/features/response.feature +3 -3
- package/features/steps/Components.ts +12 -4
- package/features/steps/Database.ts +9 -0
- package/features/steps/Gateway.ts +13 -13
- package/features/steps/HTTP.ts +3 -5
- package/features/steps/Parameters.ts +2 -3
- package/features/steps/Realtime.ts +4 -4
- package/features/steps/Workspace.ts +8 -3
- package/features/steps/components/audit/manifest.toa.yaml +12 -5
- package/features/steps/components/echo/manifest.toa.yaml +33 -8
- package/features/steps/components/notify/manifest.toa.yaml +12 -5
- package/features/steps/components/octets.tester/manifest.toa.yaml +43 -14
- package/features/steps/components/orders/manifest.toa.yaml +22 -5
- package/features/steps/components/pots/manifest.toa.yaml +71 -17
- package/features/steps/components/pricing/manifest.toa.yaml +8 -2
- package/features/steps/components/sequences/manifest.toa.yaml +3 -1
- package/features/steps/components/users/manifest.toa.yaml +18 -3
- package/features/steps/components/users.properties/manifest.toa.yaml +12 -2
- package/package.json +6 -7
- package/schemas/annotation.cos.yaml +37 -17
- package/schemas/directive.cos.yaml +7 -3
- package/schemas/io/throttle.cos.yaml +47 -8
- package/schemas/method.cos.yaml +21 -9
- package/schemas/node.cos.yaml +22 -7
- package/schemas/octets/delete.cos.yaml +15 -2
- package/schemas/octets/get.cos.yaml +5 -2
- package/schemas/octets/put.cos.yaml +6 -3
- package/schemas/octets/workflow.cos.yaml +0 -1
- package/schemas/query.cos.yaml +31 -14
- package/schemas/querystring.cos.yaml +19 -7
- package/schemas/range.cos.yaml +10 -2
- package/schemas/route.cos.yaml +6 -2
- package/source/Branch.test.ts +37 -0
- package/source/Branch.ts +37 -0
- package/source/Factory.ts +5 -5
- package/source/Gateway.ts +29 -14
- package/source/HTTP/formats/yaml.test.ts +4 -2
- package/source/RTD/Tree.ts +4 -1
- package/source/Tenant.ts +15 -4
- package/source/deployment.ts +3 -4
- package/source/directives/io/IO.ts +2 -2
- package/source/directives/io/lib/throttle/Sync.ts +4 -4
- package/source/directives/map/Headers.ts +8 -1
- package/source/directives/octets/Put.ts +6 -3
- package/source/root.ts +2 -2
- package/transpiled/Branch.d.ts +23 -0
- package/transpiled/Branch.js +16 -0
- package/transpiled/Branch.js.map +1 -1
- package/transpiled/Factory.js +4 -4
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Gateway.d.ts +7 -0
- package/transpiled/Gateway.js +26 -8
- package/transpiled/Gateway.js.map +1 -1
- package/transpiled/RTD/Tree.js +4 -1
- package/transpiled/RTD/Tree.js.map +1 -1
- package/transpiled/Tenant.d.ts +8 -2
- package/transpiled/Tenant.js +12 -2
- package/transpiled/Tenant.js.map +1 -1
- package/transpiled/deployment.js +3 -4
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/directives/io/IO.d.ts +1 -1
- package/transpiled/directives/io/IO.js +2 -2
- package/transpiled/directives/io/IO.js.map +1 -1
- package/transpiled/directives/io/lib/throttle/Sync.d.ts +2 -2
- package/transpiled/directives/io/lib/throttle/Sync.js +4 -4
- package/transpiled/directives/io/lib/throttle/Sync.js.map +1 -1
- package/transpiled/directives/map/Headers.js +7 -1
- package/transpiled/directives/map/Headers.js.map +1 -1
- package/transpiled/directives/octets/Put.js.map +1 -1
- package/transpiled/root.js +1 -1
- package/transpiled/root.js.map +1 -1
- 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
|
|
47
|
-
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
54
|
+
const ERR_INACCESSIBLE_SCOPE = new (class InaccessibleScopeError extends Error {
|
|
55
|
+
public readonly code = 'INACCESSIBLE_SCOPE'
|
|
56
|
+
})()
|
|
@@ -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 `
|
|
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.
|
package/features/cors.feature
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
|
|
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
|
package/features/dev.feature
CHANGED
|
@@ -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
|
|
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
|
-
#
|
|
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:
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
package/features/map.feature
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
"""
|
package/features/query.feature
CHANGED
|
@@ -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
|
package/features/require.feature
CHANGED
|
@@ -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 '
|
|
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 '
|
|
5
|
-
import {
|
|
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 ('/'
|
|
22
|
+
if (annotation['/'] !== undefined) {
|
|
23
23
|
const tree = syntax.parse(annotation['/'], shortcuts)
|
|
24
24
|
|
|
25
|
-
process.env.TOA_EXPOSITION =
|
|
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 =
|
|
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 =
|
|
62
|
-
process.env.TOA_EXPOSITION_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
|
|
74
|
+
const patch = parse(yaml) as object
|
|
75
75
|
const configuration = Object.assign({}, def, patch)
|
|
76
76
|
|
|
77
|
-
process.env[key] =
|
|
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 ??=
|
|
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] ??=
|
|
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 =
|
|
146
|
+
const DEFAULT_TREE = JSON.stringify({
|
|
147
147
|
routes: [],
|
|
148
148
|
methods: [],
|
|
149
149
|
directives: [
|
package/features/steps/HTTP.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
36
|
+
assert.ok(this.bytes !== null, 'Response body is not available')
|
|
38
37
|
|
|
39
|
-
const
|
|
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 ??=
|
|
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 =
|
|
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 {
|
|
7
|
-
import { parse } from '
|
|
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 =
|
|
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
|
|
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
|
|
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
|
}
|