@toa.io/extensions.origins 0.7.2-dev.0 → 0.8.0-dev.1

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 (52) hide show
  1. package/package.json +9 -8
  2. package/readme.md +113 -0
  3. package/source/.deployment/index.js +5 -0
  4. package/source/.deployment/uris.js +35 -0
  5. package/source/.test/constants.js +3 -0
  6. package/source/.test/deployment.fixtures.js +20 -0
  7. package/source/.test/factory.fixtures.js +13 -0
  8. package/source/constants.js +3 -0
  9. package/source/deployment.js +28 -0
  10. package/source/deployment.test.js +158 -0
  11. package/source/env.js +50 -0
  12. package/source/factory.js +46 -0
  13. package/source/factory.test.js +140 -0
  14. package/{src → source}/index.js +2 -0
  15. package/source/manifest.js +25 -0
  16. package/source/manifest.test.js +57 -0
  17. package/source/protocols/amqp/.test/aspect.fixtures.js +15 -0
  18. package/source/protocols/amqp/.test/mock.comq.js +13 -0
  19. package/source/protocols/amqp/aspect.js +77 -0
  20. package/source/protocols/amqp/aspect.test.js +119 -0
  21. package/source/protocols/amqp/deployment.js +63 -0
  22. package/source/protocols/amqp/id.js +3 -0
  23. package/source/protocols/amqp/index.js +11 -0
  24. package/source/protocols/amqp/protocols.js +3 -0
  25. package/source/protocols/http/.aspect/permissions.js +65 -0
  26. package/{test → source/protocols/http/.test}/aspect.fixtures.js +6 -6
  27. package/source/protocols/http/aspect.js +129 -0
  28. package/source/protocols/http/aspect.test.js +239 -0
  29. package/source/protocols/http/id.js +3 -0
  30. package/source/protocols/http/index.js +9 -0
  31. package/source/protocols/http/protocols.js +3 -0
  32. package/source/protocols/index.js +6 -0
  33. package/source/schemas/annotations.cos.yaml +1 -0
  34. package/source/schemas/index.js +8 -0
  35. package/source/schemas/manifest.cos.yaml +1 -0
  36. package/types/amqp.d.ts +9 -0
  37. package/types/deployment.d.ts +7 -0
  38. package/types/http.d.ts +28 -0
  39. package/src/.manifest/index.js +0 -7
  40. package/src/.manifest/normalize.js +0 -17
  41. package/src/.manifest/schema.yaml +0 -13
  42. package/src/.manifest/validate.js +0 -13
  43. package/src/aspect.js +0 -95
  44. package/src/factory.js +0 -14
  45. package/src/manifest.js +0 -13
  46. package/test/aspect.test.js +0 -144
  47. package/test/factory.fixtures.js +0 -5
  48. package/test/factory.test.js +0 -22
  49. package/test/manifest.fixtures.js +0 -11
  50. package/test/manifest.test.js +0 -58
  51. package/types/aspect.ts +0 -19
  52. package/types/declaration.d.ts +0 -11
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.origins",
3
- "version": "0.7.2-dev.0",
4
- "description": "Toa operations context HTTP client",
3
+ "version": "0.8.0-dev.1",
4
+ "description": "Toa Origins",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
7
- "main": "src/index.js",
7
+ "main": "source/index.js",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/toa-io/toa.git"
@@ -19,14 +19,15 @@
19
19
  "test": "echo \"Error: run tests from root\" && exit 1"
20
20
  },
21
21
  "dependencies": {
22
- "@toa.io/core": "0.7.2-dev.0",
23
- "@toa.io/generic": "0.7.0",
24
- "@toa.io/schema": "0.7.2-dev.0",
25
- "@toa.io/yaml": "0.7.2-dev.0",
22
+ "@toa.io/core": "0.8.0-dev.1",
23
+ "@toa.io/generic": "0.8.0-dev.1",
24
+ "@toa.io/schemas": "0.8.0-dev.1",
25
+ "@toa.io/yaml": "0.7.2-dev.2",
26
+ "comq": "0.6.0",
26
27
  "node-fetch": "2.6.7"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@types/node-fetch": "2.6.2"
30
31
  },
31
- "gitHead": "e9a0781a797c3ec5e2dff2251fbc683ec8d08ecf"
32
+ "gitHead": "9bb2f372c556440ee3e2a42e10eb415220ba1cb0"
32
33
  }
package/readme.md ADDED
@@ -0,0 +1,113 @@
1
+ # Toa Origins
2
+
3
+ `origins` extension enables external communications over supported protocols (HTTP and AMQP).
4
+
5
+ ## TL;DR
6
+
7
+ ```yaml
8
+ # manifest.toa.yaml
9
+ name: dummy
10
+ namespace: dummies
11
+
12
+ origins:
13
+ docs: http://www.domain.com/docs/
14
+ amazon: amqps://amqp.amazon.com
15
+ ```
16
+
17
+ ```javascript
18
+ // Node.js bridge
19
+ async function transition (input, object, context) {
20
+ // direct Aspect invocation
21
+ await context.aspects.http('docs', './example', { method: 'GET' })
22
+
23
+ // shortcuts
24
+ await context.http.docs.example.get() // GET http://www.domain.com/docs/example
25
+ await context.amqp.amazon.emit('something_happened', { really: true })
26
+ }
27
+ ```
28
+
29
+ ```yaml
30
+ # context.toa.yaml
31
+ origins:
32
+ dummies.dummy:
33
+ amazon: amqps://amqp.azure.com
34
+ amazon@staging: amqp://amqp.stage
35
+ ```
36
+
37
+ ## Manifest
38
+
39
+ `origins` manifest is an object conforming declaring origin names as keys an origin URLs as values.
40
+
41
+ ## HTTP Aspect
42
+
43
+ Uses [node-fetch](https://github.com/node-fetch/node-fetch) and returns its result.
44
+
45
+ Aspect invocation function
46
+ signature: `async (origin: string, rel: string, reuest: fetch.Request): fetch.Response`
47
+
48
+ - `origin`: name of the origin in the manifest
49
+ - `rel`: relative reference to a resource
50
+ - `request`: `Request` form `node-fetch`
51
+
52
+ ### Absolute URLs
53
+
54
+ Requests to arbitrary URLs can be implemented with overloaded direct Aspect invocation.
55
+
56
+ `async (url: string, request: fetch.Request): fetch.Response`
57
+
58
+ By default, requests to arbitrary URLs are not allowed and must be explicitly permitted by setting
59
+ permissions in the Origins Annotation.
60
+
61
+ The Rules object is stored in the `.http` property of the corresponding component. Each key in the
62
+ Rules object is a regular expression that URLs will be tested against, and each value is a
63
+ permission — either `true` to allow the URL or `false` to deny it. In cases where a URL matches
64
+ multiple rules, denial takes priority.
65
+
66
+ > The `null` key is a special case that represents "any URL".
67
+
68
+ #### Example
69
+
70
+ ```yaml
71
+ # context.toa.yaml
72
+ origins:
73
+ dummies.dummy:
74
+ .http:
75
+ /^https?:\/\/api.domain.com/: true
76
+ /^http:\/\/sandbox.domain.com/@staging: true # staging environment
77
+ /.*hackers.*/: false # deny rule
78
+ ~: true # allow any URL
79
+ ```
80
+
81
+ ```javascript
82
+ // Node.js bridge
83
+ async function transition (input, object, context) {
84
+ await context.aspects.http('https://api.domain.com/example', { method: 'POST' })
85
+ }
86
+ ```
87
+
88
+ #### `null` origins
89
+
90
+ To enable the extension for a component that uses arbitrary URLs without any specific origins to
91
+ declare, the Origins manifest should be set to `null`.
92
+
93
+ ```yaml
94
+ # manifest.toa.yaml
95
+ origins: ~
96
+ ```
97
+
98
+ ## AMQP Aspect
99
+
100
+ Uses [ComQ](https://github.com/toa-io/comq), thus, provides interface of `comq.IO` restricted
101
+ to `emit` and `request` methods.
102
+
103
+ AMQP origins can have credential secrets deployed. Secret's name must
104
+ follow `toa-origins-{namespace}-{component}-{origin}` and it must have keys `username`
105
+ and `password`.
106
+
107
+ ### Example
108
+
109
+ ```shell
110
+ # deploy credentials to the current kubectl context
111
+ $ toa conceal toa-origins-dummies-dummiy-messages username developer
112
+ $ toa conceal toa-origins-dummies-dummiy-messages password secret
113
+ ```
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const { uris } = require('./uris')
4
+
5
+ exports.uris = uris
@@ -0,0 +1,35 @@
1
+ 'use strict'
2
+
3
+ const { PREFIX } = require('../constants')
4
+
5
+ /**
6
+ * @param {toa.norm.context.dependencies.Instance[]} instances
7
+ * @param {toa.origins.Annotations} annotations
8
+ * @returns {toa.deployment.dependency.Variables}
9
+ */
10
+ function uris (instances, annotations) {
11
+ const variables = {}
12
+
13
+ for (const [id, annotation] of Object.entries(annotations)) {
14
+ const component = instances.find((instance) => instance.locator.id === id)
15
+
16
+ if (component === undefined) throw new Error(`Origins annotations error: component '${id}' is not found`)
17
+
18
+ for (const origin of Object.keys(annotation)) {
19
+ if (!(origin in component.manifest)) {
20
+ throw new Error(`Origins annotations error: component '${id}' doesn't have '${origin}' origin`)
21
+ }
22
+ }
23
+
24
+ const name = PREFIX + component.locator.uppercase
25
+ const json = JSON.stringify(annotation)
26
+ const value = btoa(json)
27
+ const variable = { name, value }
28
+
29
+ variables[component.locator.label] = [variable]
30
+ }
31
+
32
+ return variables
33
+ }
34
+
35
+ exports.uris = uris
@@ -0,0 +1,3 @@
1
+ 'use strict'
2
+
3
+ exports.PROTOCOLS = ['http:', 'https:', 'amqp:', 'amqps:']
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+ const { Locator } = require('@toa.io/core')
5
+ const { random, sample } = require('@toa.io/generic')
6
+
7
+ const { PROTOCOLS } = require('./constants')
8
+
9
+ const component = () => ({
10
+ locator: new Locator(generate(), generate()),
11
+ manifest: { [generate()]: sample(PROTOCOLS) + '//' + generate() }
12
+ })
13
+
14
+ const components = () => {
15
+ const length = random(20) + 10
16
+
17
+ return Array.from({ length }, component)
18
+ }
19
+
20
+ exports.components = components
@@ -0,0 +1,13 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+
5
+ /** @type {Record<string, string>} */
6
+ const manifest = {
7
+ [generate()]: 'https://toa.io',
8
+ [generate()]: 'https://api.domain.com',
9
+ [generate()]: 'amqp://localhost',
10
+ [generate()]: 'amqps://localhost'
11
+ }
12
+
13
+ exports.manifest = manifest
@@ -0,0 +1,3 @@
1
+ 'use strict'
2
+
3
+ exports.PREFIX = 'TOA_ORIGINS_'
@@ -0,0 +1,28 @@
1
+ 'use strict'
2
+
3
+ const { merge } = require('@toa.io/generic')
4
+ const schemas = require('./schemas')
5
+ const protocols = require('./protocols')
6
+ const create = require('./.deployment')
7
+
8
+ /**
9
+ * @param {toa.norm.context.dependencies.Instance[]} instances
10
+ * @param {toa.origins.Annotations} annotations
11
+ * @returns {toa.deployment.dependency.Declaration}
12
+ */
13
+ function deployment (instances, annotations = {}) {
14
+ schemas.annotations.validate(annotations)
15
+
16
+ const uris = create.uris(instances, annotations)
17
+ const variables = { ...uris }
18
+
19
+ protocols.reduce((variables, provider) => {
20
+ const specifics = provider.deployment?.(instances)
21
+
22
+ return merge(variables, specifics)
23
+ }, variables)
24
+
25
+ return { variables }
26
+ }
27
+
28
+ exports.deployment = deployment
@@ -0,0 +1,158 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+ const { sample, letters: { up } } = require('@toa.io/generic')
5
+
6
+ const fixtures = require('./.test/deployment.fixtures')
7
+ const { deployment } = require('../')
8
+
9
+ it('should be', async () => {
10
+ expect(deployment).toBeInstanceOf(Function)
11
+ })
12
+
13
+ /** @type {toa.norm.context.dependencies.Instance[]} */
14
+ let components
15
+
16
+ /** @type {string} */
17
+ let origin
18
+
19
+ /** @type {toa.norm.context.dependencies.Instance} */
20
+ let component
21
+
22
+ beforeEach(() => {
23
+ components = fixtures.components()
24
+
25
+ component = sample(components)
26
+
27
+ const origins = Object.keys(component.manifest)
28
+
29
+ origin = sample(origins)
30
+ })
31
+
32
+ describe('validation', () => {
33
+ it('should throw on annotation component mismatch', async () => {
34
+ const id = generate()
35
+
36
+ const annotations = {
37
+ [id]: {
38
+ [origin]: 'dev://' + generate()
39
+ }
40
+ }
41
+
42
+ expect(() => deployment(components, annotations))
43
+ .toThrow(`Origins annotations error: component '${id}' is not found`)
44
+ })
45
+
46
+ it('should throw on annotation origin mismatch', async () => {
47
+ const id = component.locator.id
48
+ const origin = generate()
49
+
50
+ const annotations = {
51
+ [id]: {
52
+ [origin]: 'dev://' + generate()
53
+ }
54
+ }
55
+
56
+ expect(() => deployment(components, annotations))
57
+ .toThrow(`Origins annotations error: component '${id}' doesn't have '${origin}' origin`)
58
+ })
59
+
60
+ it('should throw if annotation is not valid', async () => {
61
+ const annotations = /** @type {toa.origins.Annotations} */ { [component.locator.id]: generate() }
62
+
63
+ expect(() => deployment(components, annotations)).toThrow('must be object')
64
+ })
65
+
66
+ it('should throw if annotation is URI is not valid', async () => {
67
+ const annotations = {
68
+ [component.locator.id]: {
69
+ [origin]: 'hello!'
70
+ }
71
+ }
72
+
73
+ expect(() => deployment(components, annotations)).toThrow('must match format')
74
+ })
75
+ })
76
+
77
+ it('should create variables', () => {
78
+ const value = 'dev://' + generate()
79
+
80
+ /** @type {toa.origins.Annotations} */
81
+ const annotations = {
82
+ [component.locator.id]: {
83
+ [origin]: value
84
+ }
85
+ }
86
+
87
+ const output = deployment(components, annotations)
88
+
89
+ expect(output.variables).not.toBeUndefined()
90
+
91
+ const variables = output.variables[component.locator.label]
92
+ const varName = 'TOA_ORIGINS_' + component.locator.uppercase
93
+ const variable = findVariable(variables, varName)
94
+
95
+ expect(variable).toBeDefined()
96
+
97
+ const json = JSON.stringify(annotations[component.locator.id])
98
+ const base64 = btoa(json)
99
+
100
+ expect(variable.value).toStrictEqual(base64)
101
+ })
102
+
103
+ describe('amqp', () => {
104
+ beforeEach(() => {
105
+ const amqpComponents = components.filter(
106
+ (component) => {
107
+ const origin = Object.keys(component.manifest)
108
+ const url = new URL(component.manifest[origin])
109
+
110
+ return url.protocol === 'amqp:' || url.protocol === 'amqps:'
111
+ }
112
+ )
113
+
114
+ component = sample(amqpComponents)
115
+
116
+ const origins = Object.keys(component.manifest)
117
+
118
+ origin = sample(origins)
119
+ })
120
+
121
+ it('should create credential secrets', () => {
122
+ /** @type {toa.origins.Annotations} */
123
+ const annotations = {
124
+ [component.locator.id]: {
125
+ [origin]: 'amqps://whatever'
126
+ }
127
+ }
128
+
129
+ const output = deployment(components, annotations)
130
+ const variables = output.variables[component.locator.label]
131
+
132
+ expect(variables).toBeDefined()
133
+
134
+ const envPrefix = `TOA_ORIGINS_${component.locator.uppercase}_${up(origin)}_`
135
+ const secretName = `toa-origins-${component.locator.label}-${origin}`
136
+
137
+ for (const property of ['username', 'password']) {
138
+ const variableName = envPrefix + up(property)
139
+ const variable = findVariable(variables, variableName)
140
+
141
+ expect(variable).toBeDefined()
142
+
143
+ expect(variable.secret).toStrictEqual({
144
+ name: secretName,
145
+ key: property
146
+ })
147
+ }
148
+ })
149
+ })
150
+
151
+ /**
152
+ * @param {toa.deployment.dependency.Variable[]} variables
153
+ * @param {string} name
154
+ * @returns {toa.deployment.dependency.Variable}
155
+ */
156
+ function findVariable (variables, name) {
157
+ return variables.find((variable) => variable.name === name)
158
+ }
package/source/env.js ADDED
@@ -0,0 +1,50 @@
1
+ 'use strict'
2
+
3
+ const { PREFIX } = require('./constants')
4
+ const { overwrite, remap, letters: { up } } = require('@toa.io/generic')
5
+
6
+ /**
7
+ * @param {toa.core.Locator} locator
8
+ * @param {toa.origins.Manifest} manifest
9
+ */
10
+ function apply (locator, manifest) {
11
+ const variable = PREFIX + locator.uppercase
12
+ const envValue = readEnv(variable)
13
+
14
+ overwrite(manifest, envValue)
15
+ addCredentials(manifest, variable)
16
+ }
17
+
18
+ function readEnv (variable) {
19
+ if (!(variable in process.env)) return
20
+
21
+ const base64 = process.env[variable]
22
+ const json = atob(base64)
23
+
24
+ return JSON.parse(json)
25
+ }
26
+
27
+ /**
28
+ * @param {toa.origins.Manifest} manifest
29
+ * @param {string} variable
30
+ */
31
+ function addCredentials (manifest, variable) {
32
+ const prefix = variable + '_'
33
+
34
+ remap(manifest, (reference, origin) => {
35
+ const originPrefix = prefix + up(origin)
36
+ const username = process.env[originPrefix + '_USERNAME']
37
+ const password = process.env[originPrefix + '_PASSWORD']
38
+
39
+ if (username === undefined && password === undefined) return
40
+
41
+ const url = new URL(reference)
42
+
43
+ url.username = username
44
+ url.password = password
45
+
46
+ manifest[origin] = url.href
47
+ })
48
+ }
49
+
50
+ exports.apply = apply
@@ -0,0 +1,46 @@
1
+ 'use strict'
2
+
3
+ const protocols = require('./protocols')
4
+ const env = require('./env')
5
+
6
+ class Factory {
7
+ /**
8
+ * @param {toa.core.Locator} locator
9
+ * @param {toa.origins.Manifest} manifest
10
+ * @return {toa.core.extensions.Aspect[]}
11
+ */
12
+ aspect (locator, manifest) {
13
+ env.apply(locator, manifest)
14
+
15
+ return protocols.map((protocol) => this.#createAspect(protocol, manifest))
16
+ }
17
+
18
+ /**
19
+ * @param {object} protocol
20
+ * @param {toa.origins.Manifest} manifest
21
+ * @return {toa.core.extensions.Aspect}
22
+ */
23
+ #createAspect (protocol, manifest) {
24
+ const protocolManifest = {}
25
+
26
+ let properties
27
+
28
+ // let properties
29
+
30
+ for (const [origin, reference] of Object.entries(manifest)) {
31
+ if (origin[0] === '.') {
32
+ if (origin.substring(1) === protocol.id) properties = reference
33
+
34
+ continue
35
+ }
36
+
37
+ const url = new URL(reference)
38
+
39
+ if (protocol.protocols.includes(url.protocol)) protocolManifest[origin] = reference
40
+ }
41
+
42
+ return protocol.create(protocolManifest, properties)
43
+ }
44
+ }
45
+
46
+ exports.Factory = Factory
@@ -0,0 +1,140 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+ const { Locator } = require('@toa.io/core')
5
+ const { sample, overwrite, letters: { up } } = require('@toa.io/generic')
6
+
7
+ jest.mock('./protocols/http/aspect')
8
+ jest.mock('./protocols/amqp/aspect')
9
+
10
+ const http = require('./protocols/http/aspect')
11
+ const amqp = require('./protocols/amqp/aspect')
12
+
13
+ const fixtures = require('./.test/factory.fixtures')
14
+ const { Factory } = require('../')
15
+
16
+ let factory
17
+
18
+ beforeEach(() => {
19
+ jest.clearAllMocks()
20
+
21
+ factory = new Factory()
22
+ })
23
+
24
+ it('should create aspects', () => {
25
+ factory.aspect(new Locator(generate(), generate()), fixtures.manifest)
26
+
27
+ const httpManifest = filterManifest(fixtures.manifest, 'http')
28
+ const amqpManifest = filterManifest(fixtures.manifest, 'amqp')
29
+
30
+ expect(http.create).toHaveBeenCalledWith(httpManifest, undefined)
31
+ expect(amqp.create).toHaveBeenCalledWith(amqpManifest, undefined)
32
+ })
33
+
34
+ describe('env', () => {
35
+ it('should overwrites URLs from environment', async () => {
36
+ const httpManifest = filterManifest(fixtures.manifest, 'http')
37
+ const key = sample(Object.keys(httpManifest))
38
+ const override = { [key]: 'http://' + generate() }
39
+ const json = JSON.stringify(override)
40
+ const base64 = btoa(json)
41
+ const locator = new Locator(generate(), generate())
42
+
43
+ process.env['TOA_ORIGINS_' + locator.uppercase] = base64
44
+
45
+ factory.aspect(locator, fixtures.manifest)
46
+
47
+ const expected = overwrite(httpManifest, override)
48
+
49
+ expect(http.create.mock.calls[0][0]).toStrictEqual(expected)
50
+ })
51
+
52
+ describe('amqp', () => {
53
+ /** @type {toa.origins.Manifest} */
54
+ let amqpManifest
55
+
56
+ beforeEach(() => {
57
+ amqpManifest = filterManifest(fixtures.manifest, 'amqp')
58
+ })
59
+
60
+ it('should add credentials from environment', async () => {
61
+ const key = sample(Object.keys(amqpManifest))
62
+ const locator = new Locator(generate(), generate())
63
+ const envPrefix = 'TOA_ORIGINS_' + locator.uppercase + '_' + up(key) + '_'
64
+ const username = generate()
65
+ const password = generate()
66
+
67
+ process.env[envPrefix + 'USERNAME'] = username
68
+ process.env[envPrefix + 'PASSWORD'] = password
69
+
70
+ factory.aspect(locator, amqpManifest)
71
+
72
+ const manifest = amqp.create.mock.calls[0][0]
73
+ const origin = manifest[key]
74
+ const url = new URL(origin)
75
+
76
+ expect(url.username).toStrictEqual(username)
77
+ expect(url.password).toStrictEqual(password)
78
+ })
79
+
80
+ it('should add credentials to URLs from environment', async () => {
81
+ const key = sample(Object.keys(amqpManifest))
82
+ const hostname = generate().toLowerCase()
83
+ const override = { [key]: 'amqp://' + hostname }
84
+ const json = JSON.stringify(override)
85
+ const base64 = btoa(json)
86
+ const locator = new Locator(generate(), generate())
87
+ const envPrefix = 'TOA_ORIGINS_' + locator.uppercase + '_' + up(key) + '_'
88
+ const username = generate()
89
+ const password = generate()
90
+
91
+ process.env['TOA_ORIGINS_' + locator.uppercase] = base64
92
+ process.env[envPrefix + 'USERNAME'] = username
93
+ process.env[envPrefix + 'PASSWORD'] = password
94
+
95
+ factory.aspect(locator, fixtures.manifest)
96
+
97
+ const manifest = amqp.create.mock.calls[0][0]
98
+ const origin = manifest[key]
99
+ const url = new URL(origin)
100
+
101
+ expect(url.hostname).toStrictEqual(hostname)
102
+ expect(url.username).toStrictEqual(username)
103
+ expect(url.password).toStrictEqual(password)
104
+ })
105
+ })
106
+
107
+ describe('http', () => {
108
+ it('should read properties', async () => {
109
+ const properties = {
110
+ '.http': {
111
+ [generate()]: generate()
112
+ }
113
+ }
114
+
115
+ const locator = new Locator(generate(), generate())
116
+ const json = JSON.stringify(properties)
117
+
118
+ process.env['TOA_ORIGINS_' + locator.uppercase] = btoa(json)
119
+
120
+ factory.aspect(locator, fixtures.manifest)
121
+
122
+ expect(http.create).toHaveBeenCalledWith(expect.anything(), properties['.http'])
123
+ })
124
+ })
125
+ })
126
+
127
+ /**
128
+ * @param {toa.origins.Manifest} manifest
129
+ * @param {string} protocol
130
+ * @return {toa.origins.Manifest}
131
+ */
132
+ function filterManifest (manifest, protocol) {
133
+ const result = {}
134
+
135
+ for (const [origin, reference] of Object.entries(manifest)) {
136
+ if (reference.slice(0, protocol.length) === protocol) result[origin] = reference
137
+ }
138
+
139
+ return result
140
+ }
@@ -1,7 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const { manifest } = require('./manifest')
4
+ const { deployment } = require('./deployment')
4
5
  const { Factory } = require('./factory')
5
6
 
6
7
  exports.manifest = manifest
8
+ exports.deployment = deployment
7
9
  exports.Factory = Factory
@@ -0,0 +1,25 @@
1
+ 'use strict'
2
+
3
+ const schemas = require('./schemas')
4
+ const protocols = require('./protocols')
5
+
6
+ /**
7
+ * @param {toa.origins.Manifest} manifest
8
+ * @returns {toa.origins.Manifest}
9
+ */
10
+ function manifest (manifest) {
11
+ if (manifest === null) return {}
12
+
13
+ schemas.manifest.validate(manifest)
14
+
15
+ for (const uri of Object.values(manifest)) {
16
+ const protocol = new URL(uri).protocol
17
+ const supported = protocols.find((provider) => provider.protocols.includes(protocol))
18
+
19
+ if (supported === undefined) throw new Error(`'${protocol}' protocol is not supported`)
20
+ }
21
+
22
+ return manifest
23
+ }
24
+
25
+ exports.manifest = manifest