@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.
- package/package.json +9 -8
- package/readme.md +113 -0
- package/source/.deployment/index.js +5 -0
- package/source/.deployment/uris.js +35 -0
- package/source/.test/constants.js +3 -0
- package/source/.test/deployment.fixtures.js +20 -0
- package/source/.test/factory.fixtures.js +13 -0
- package/source/constants.js +3 -0
- package/source/deployment.js +28 -0
- package/source/deployment.test.js +158 -0
- package/source/env.js +50 -0
- package/source/factory.js +46 -0
- package/source/factory.test.js +140 -0
- package/{src → source}/index.js +2 -0
- package/source/manifest.js +25 -0
- package/source/manifest.test.js +57 -0
- package/source/protocols/amqp/.test/aspect.fixtures.js +15 -0
- package/source/protocols/amqp/.test/mock.comq.js +13 -0
- package/source/protocols/amqp/aspect.js +77 -0
- package/source/protocols/amqp/aspect.test.js +119 -0
- package/source/protocols/amqp/deployment.js +63 -0
- package/source/protocols/amqp/id.js +3 -0
- package/source/protocols/amqp/index.js +11 -0
- package/source/protocols/amqp/protocols.js +3 -0
- package/source/protocols/http/.aspect/permissions.js +65 -0
- package/{test → source/protocols/http/.test}/aspect.fixtures.js +6 -6
- package/source/protocols/http/aspect.js +129 -0
- package/source/protocols/http/aspect.test.js +239 -0
- package/source/protocols/http/id.js +3 -0
- package/source/protocols/http/index.js +9 -0
- package/source/protocols/http/protocols.js +3 -0
- package/source/protocols/index.js +6 -0
- package/source/schemas/annotations.cos.yaml +1 -0
- package/source/schemas/index.js +8 -0
- package/source/schemas/manifest.cos.yaml +1 -0
- package/types/amqp.d.ts +9 -0
- package/types/deployment.d.ts +7 -0
- package/types/http.d.ts +28 -0
- package/src/.manifest/index.js +0 -7
- package/src/.manifest/normalize.js +0 -17
- package/src/.manifest/schema.yaml +0 -13
- package/src/.manifest/validate.js +0 -13
- package/src/aspect.js +0 -95
- package/src/factory.js +0 -14
- package/src/manifest.js +0 -13
- package/test/aspect.test.js +0 -144
- package/test/factory.fixtures.js +0 -5
- package/test/factory.test.js +0 -22
- package/test/manifest.fixtures.js +0 -11
- package/test/manifest.test.js +0 -58
- package/types/aspect.ts +0 -19
- package/types/declaration.d.ts +0 -11
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
const { PROTOCOLS } = require('./.test/constants')
|
|
5
|
+
|
|
6
|
+
const { manifest } = require('../')
|
|
7
|
+
|
|
8
|
+
it('should be', async () => {
|
|
9
|
+
expect(manifest).toBeInstanceOf(Function)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('should return manifest', async () => {
|
|
13
|
+
const input = { [generate()]: 'http://' + generate() }
|
|
14
|
+
const output = manifest(input)
|
|
15
|
+
|
|
16
|
+
expect(output).toStrictEqual(input)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should fail if not Record<string, string>', async () => {
|
|
20
|
+
const input = /** @type {toa.origins.Manifest} */ {
|
|
21
|
+
foo: {
|
|
22
|
+
bar: 'dev://null'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
expect(() => manifest(input)).toThrow()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should pass if valid', async () => {
|
|
30
|
+
const input = { foo: 'amqp://' + generate() }
|
|
31
|
+
|
|
32
|
+
expect(() => manifest(input)).not.toThrow()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should fail if not uri', async () => {
|
|
36
|
+
const input = { [generate()]: generate() }
|
|
37
|
+
|
|
38
|
+
expect(() => manifest(input)).toThrow('must match format')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should throw if protocol is not supported', async () => {
|
|
42
|
+
const input = { foo: 'wat://' + generate() }
|
|
43
|
+
|
|
44
|
+
expect(() => manifest(input)).toThrow('is not supported')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should convert null to {}', async () => {
|
|
48
|
+
const output = manifest(null)
|
|
49
|
+
|
|
50
|
+
expect(output).toStrictEqual({})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it.each(PROTOCOLS)('should support %s protocol', async (protocol) => {
|
|
54
|
+
const input = { foo: protocol + '//' + generate() }
|
|
55
|
+
|
|
56
|
+
expect(() => manifest(input)).not.toThrow()
|
|
57
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
const { random } = require('@toa.io/generic')
|
|
5
|
+
|
|
6
|
+
const manifest = {}
|
|
7
|
+
const originCount = random(5) + 2
|
|
8
|
+
|
|
9
|
+
for (let j = 0; j < originCount; j++) {
|
|
10
|
+
const origin = generate()
|
|
11
|
+
|
|
12
|
+
manifest[origin] = 'amqp://' + generate()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
exports.manifest = manifest
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
|
|
5
|
+
const connect = jest.fn(async () => ({
|
|
6
|
+
emit: jest.fn(async () => undefined),
|
|
7
|
+
request: jest.fn(async () => generate()),
|
|
8
|
+
reply: jest.fn(async () => undefined),
|
|
9
|
+
consume: jest.fn(async () => undefined),
|
|
10
|
+
close: jest.fn(async () => undefined)
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
exports.connect = connect
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { connect } = require('comq')
|
|
4
|
+
const { Connector } = require('@toa.io/core')
|
|
5
|
+
const { shards } = require('@toa.io/generic')
|
|
6
|
+
|
|
7
|
+
const { id } = require('./id')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @implements {toa.origins.amqp.Aspect}
|
|
11
|
+
*/
|
|
12
|
+
class Aspect extends Connector {
|
|
13
|
+
name = id
|
|
14
|
+
/** @type {toa.origins.Manifest} */
|
|
15
|
+
#manifest
|
|
16
|
+
|
|
17
|
+
/** @type {Record<string, Partial<comq.IO>>} */
|
|
18
|
+
#origins = {}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {toa.origins.Manifest} manifest
|
|
22
|
+
*/
|
|
23
|
+
constructor (manifest) {
|
|
24
|
+
super()
|
|
25
|
+
|
|
26
|
+
this.#manifest = manifest
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async open () {
|
|
30
|
+
const promises = Object.entries(this.#manifest).map(this.#open)
|
|
31
|
+
|
|
32
|
+
await Promise.all(promises)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async close () {
|
|
36
|
+
const promises = Object.values(this.#origins).map(this.#close)
|
|
37
|
+
|
|
38
|
+
await Promise.all(promises)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async invoke (origin, method, ...args) {
|
|
42
|
+
return this.#origins[origin][method](...args)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#open = async ([origin, reference]) => {
|
|
46
|
+
const references = shards(reference)
|
|
47
|
+
const io = await connect(...references)
|
|
48
|
+
|
|
49
|
+
this.#origins[origin] = restrict(io)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#close = async (io) => {
|
|
53
|
+
await io.close()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {comq.IO} io
|
|
59
|
+
* @return {Partial<comq.IO>}
|
|
60
|
+
*/
|
|
61
|
+
function restrict (io) {
|
|
62
|
+
// noinspection JSUnresolvedReference
|
|
63
|
+
return {
|
|
64
|
+
request: (...args) => io.request(...args),
|
|
65
|
+
emit: (...args) => io.emit(...args),
|
|
66
|
+
close: () => io.close()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {toa.origins.Manifest} manifest
|
|
72
|
+
*/
|
|
73
|
+
function create (manifest) {
|
|
74
|
+
return new Aspect(manifest)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
exports.create = create
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
const { Connector } = require('@toa.io/core')
|
|
5
|
+
const { sample } = require('@toa.io/generic')
|
|
6
|
+
|
|
7
|
+
const comq = require('./.test/mock.comq')
|
|
8
|
+
const mock = { comq }
|
|
9
|
+
|
|
10
|
+
jest.mock('comq', () => mock.comq)
|
|
11
|
+
|
|
12
|
+
const fixtures = require('./.test/aspect.fixtures')
|
|
13
|
+
const { create } = require('./aspect')
|
|
14
|
+
|
|
15
|
+
it('should be', async () => {
|
|
16
|
+
expect(create).toBeInstanceOf(Function)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
/** @type {toa.origins.amqp.Aspect} */
|
|
20
|
+
let aspect
|
|
21
|
+
|
|
22
|
+
/** @type {toa.origins.Manifest} */
|
|
23
|
+
let manifest
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks()
|
|
27
|
+
|
|
28
|
+
manifest = fixtures.manifest
|
|
29
|
+
aspect = create(manifest)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should be instance of Connector', async () => {
|
|
33
|
+
expect(aspect).toBeInstanceOf(Connector)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should expose name', async () => {
|
|
37
|
+
expect(aspect.name).toStrictEqual('amqp')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should connect', async () => {
|
|
41
|
+
await aspect.open()
|
|
42
|
+
|
|
43
|
+
for (const reference of Object.values(manifest)) {
|
|
44
|
+
expect(comq.connect).toHaveBeenCalledWith(reference)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should connect to shards', async () => {
|
|
49
|
+
jest.clearAllMocks()
|
|
50
|
+
|
|
51
|
+
manifest = {
|
|
52
|
+
test: 'amqps://host{0-2}.domain.com'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
aspect = create(manifest)
|
|
56
|
+
|
|
57
|
+
const shards = []
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < 3; i++) shards.push(`amqps://host${i}.domain.com`)
|
|
60
|
+
|
|
61
|
+
await aspect.open()
|
|
62
|
+
|
|
63
|
+
expect(comq.connect).toHaveBeenCalledWith(...shards)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should disconnect', async () => {
|
|
67
|
+
await aspect.open()
|
|
68
|
+
await aspect.close()
|
|
69
|
+
|
|
70
|
+
for (const reference of Object.values(manifest)) {
|
|
71
|
+
const index = comq.connect.mock.calls.findIndex((call) => call[0] === reference)
|
|
72
|
+
const io = await comq.connect.mock.results[index].value
|
|
73
|
+
|
|
74
|
+
expect(io.close).toHaveBeenCalled()
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('invoke', () => {
|
|
79
|
+
/** @type {jest.MockedObject<comq.IO>} */
|
|
80
|
+
let io
|
|
81
|
+
|
|
82
|
+
/** @type {string} */
|
|
83
|
+
let origin
|
|
84
|
+
|
|
85
|
+
let args
|
|
86
|
+
|
|
87
|
+
beforeEach(async () => {
|
|
88
|
+
await aspect.open()
|
|
89
|
+
|
|
90
|
+
const origins = Object.keys(manifest)
|
|
91
|
+
|
|
92
|
+
origin = sample(origins)
|
|
93
|
+
|
|
94
|
+
const reference = manifest[origin]
|
|
95
|
+
const index = comq.connect.mock.calls.findIndex((call) => call[0] === reference)
|
|
96
|
+
|
|
97
|
+
io = await comq.connect.mock.results[index].value
|
|
98
|
+
args = [generate(), generate(), generate()]
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should be', async () => {
|
|
102
|
+
expect(aspect.invoke).toBeInstanceOf(Function)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it.each(['emit', 'request'])('should %s', async (method) => {
|
|
106
|
+
await aspect.invoke(origin, method, ...args)
|
|
107
|
+
|
|
108
|
+
expect(io[method]).toHaveBeenCalledWith(...args)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it.each(['reply', 'consume', generate()])('should not expose %s',
|
|
112
|
+
async (method) => {
|
|
113
|
+
await expect(aspect.invoke(origin, method)).rejects.toThrow()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should throw if unknown origin', async () => {
|
|
117
|
+
await expect(aspect.invoke(generate(), 'emit')).rejects.toThrow()
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { letters: { up } } = require('@toa.io/generic')
|
|
4
|
+
const protocols = require('./protocols')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {toa.norm.context.dependencies.Instance[]} instances
|
|
8
|
+
* @returns {toa.deployment.dependency.Variables}
|
|
9
|
+
*/
|
|
10
|
+
function deployment (instances) {
|
|
11
|
+
/** @type {toa.deployment.dependency.Variables} */
|
|
12
|
+
const variables = {}
|
|
13
|
+
|
|
14
|
+
for (const { locator, manifest } of instances) {
|
|
15
|
+
const secrets = []
|
|
16
|
+
|
|
17
|
+
for (const [origin, reference] of Object.entries(manifest)) {
|
|
18
|
+
const url = new URL(reference)
|
|
19
|
+
|
|
20
|
+
if (protocols.includes(url.protocol)) {
|
|
21
|
+
const originSecrets = createSecrets(locator, origin)
|
|
22
|
+
|
|
23
|
+
secrets.push(...originSecrets)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
variables[locator.label] = secrets
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return variables
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {toa.core.Locator} locator
|
|
35
|
+
* @param {string} origin
|
|
36
|
+
* @return {toa.deployment.dependency.Variable[]}
|
|
37
|
+
*/
|
|
38
|
+
function createSecrets (locator, origin) {
|
|
39
|
+
const properties = ['username', 'password']
|
|
40
|
+
|
|
41
|
+
return properties.map((property) => createSecret(locator, origin, property))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {toa.core.Locator} locator
|
|
46
|
+
* @param {string} origin
|
|
47
|
+
* @param {string} property
|
|
48
|
+
* @return {toa.deployment.dependency.Variable}
|
|
49
|
+
*/
|
|
50
|
+
function createSecret (locator, origin, property) {
|
|
51
|
+
const variable = `TOA_ORIGINS_${locator.uppercase}_${up(origin)}_${up(property)}`
|
|
52
|
+
const secret = `toa-origins-${locator.label}-${origin}`
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
name: variable,
|
|
56
|
+
secret: {
|
|
57
|
+
name: secret,
|
|
58
|
+
key: property
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
exports.deployment = deployment
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const protocols = require('./protocols')
|
|
4
|
+
const { id } = require('./id')
|
|
5
|
+
const { create } = require('./aspect')
|
|
6
|
+
const { deployment } = require('./deployment')
|
|
7
|
+
|
|
8
|
+
exports.protocols = protocols
|
|
9
|
+
exports.id = id
|
|
10
|
+
exports.create = create
|
|
11
|
+
exports.deployment = deployment
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @implements {toa.origins.http.Permissions}
|
|
5
|
+
*/
|
|
6
|
+
class Permissions {
|
|
7
|
+
#default = process.env.TOA_DEV === '1'
|
|
8
|
+
|
|
9
|
+
/** @type {RegExp[]} */
|
|
10
|
+
#allowances = []
|
|
11
|
+
|
|
12
|
+
/** @type {RegExp[]} */
|
|
13
|
+
#denials = []
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {toa.origins.http.Properties} properties
|
|
17
|
+
*/
|
|
18
|
+
constructor (properties) {
|
|
19
|
+
if (properties !== undefined) this.#parse(properties)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test (url) {
|
|
23
|
+
const denial = this.#denials.findIndex((regexp) => regexp.test(url))
|
|
24
|
+
|
|
25
|
+
if (denial !== -1) return false
|
|
26
|
+
|
|
27
|
+
const allowance = this.#allowances.findIndex((regexp) => regexp.test(url))
|
|
28
|
+
|
|
29
|
+
if (allowance !== -1) return true
|
|
30
|
+
|
|
31
|
+
return this.#default
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#parse (properties) {
|
|
35
|
+
if ('null' in properties) {
|
|
36
|
+
const always = /** @type {RegExp} */ { test: () => true }
|
|
37
|
+
|
|
38
|
+
this.#addRule(always, properties.null)
|
|
39
|
+
delete properties.null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const [key, rule] of Object.entries(properties)) {
|
|
43
|
+
const match = key.match(EXPRESSION)
|
|
44
|
+
|
|
45
|
+
if (match === null) throw new Error(`'${key}' is not a regular expression`)
|
|
46
|
+
|
|
47
|
+
const regex = new RegExp(match.groups.expression)
|
|
48
|
+
|
|
49
|
+
this.#addRule(regex, rule)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {RegExp} regex
|
|
55
|
+
* @param {boolean} rule
|
|
56
|
+
*/
|
|
57
|
+
#addRule (regex, rule) {
|
|
58
|
+
if (rule === true) this.#allowances.push(regex)
|
|
59
|
+
else this.#denials.push(regex)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const EXPRESSION = /^\/(?<expression>.+)\/$/
|
|
64
|
+
|
|
65
|
+
exports.Permissions = Permissions
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const { generate } = require('randomstring')
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
/** @type {toa.origins.Manifest} */
|
|
6
|
+
const manifest = {
|
|
7
|
+
foo: 'https://' + generate().toLowerCase(),
|
|
8
|
+
amazon: 'https://*.*.amazon.com:*',
|
|
9
|
+
deep: 'http://www.domain.com/some/path/'
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const responses = []
|
|
@@ -30,5 +30,5 @@ fetch.reset = () => {
|
|
|
30
30
|
responses.length = 0
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
exports.
|
|
33
|
+
exports.manifest = manifest
|
|
34
34
|
exports.mock = { fetch }
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fetch = require('node-fetch')
|
|
4
|
+
|
|
5
|
+
const { Connector } = require('@toa.io/core')
|
|
6
|
+
const { retry } = require('@toa.io/generic')
|
|
7
|
+
|
|
8
|
+
const { Permissions } = require('./.aspect/permissions')
|
|
9
|
+
const { id } = require('./id')
|
|
10
|
+
const protocols = require('./protocols')
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @implements {toa.origins.http.Aspect}
|
|
14
|
+
*/
|
|
15
|
+
class Aspect extends Connector {
|
|
16
|
+
/** @readonly */
|
|
17
|
+
name = id
|
|
18
|
+
|
|
19
|
+
/** @type {toa.origins.Manifest} */
|
|
20
|
+
#origins
|
|
21
|
+
|
|
22
|
+
/** @type {toa.origins.http.Permissions} */
|
|
23
|
+
#permissions
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {toa.origins.Manifest} manifest
|
|
27
|
+
* @param {toa.origins.http.Permissions} permissions
|
|
28
|
+
*/
|
|
29
|
+
constructor (manifest, permissions) {
|
|
30
|
+
super()
|
|
31
|
+
|
|
32
|
+
this.#origins = manifest
|
|
33
|
+
this.#permissions = permissions
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async invoke (name, path, request, options) {
|
|
37
|
+
let origin = this.#origins[name]
|
|
38
|
+
|
|
39
|
+
if (origin === undefined) {
|
|
40
|
+
if (isAbsoluteURL(/** @type {string} */ name)) {
|
|
41
|
+
return this.#invokeURL(
|
|
42
|
+
/** @type {string} */ name,
|
|
43
|
+
/** @type {import('node-fetch').RequestInit} */ path
|
|
44
|
+
)
|
|
45
|
+
} else throw new Error(`Origin '${name}' is not defined`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// absolute urls are forbidden when using origins
|
|
49
|
+
if (typeof path === 'string' && isAbsoluteURL(path)) throw new Error(`Absolute URLs are forbidden (${path})`)
|
|
50
|
+
|
|
51
|
+
if (options?.substitutions !== undefined) origin = substitute(origin, options.substitutions)
|
|
52
|
+
|
|
53
|
+
const url = path === undefined ? new URL(origin) : new URL(path, origin)
|
|
54
|
+
|
|
55
|
+
return this.#request(url.href, request, options?.retry)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} url
|
|
60
|
+
* @param {import('node-fetch').RequestInit} request
|
|
61
|
+
* @return {Promise<void>}
|
|
62
|
+
*/
|
|
63
|
+
async #invokeURL (url, request) {
|
|
64
|
+
if (this.#permissions.test(url) === false) throw new Error(`URL '${url}' is not allowed`)
|
|
65
|
+
|
|
66
|
+
return this.#request(url, request)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} url
|
|
71
|
+
* @param {import('node-fetch').RequestInit} request
|
|
72
|
+
* @param {toa.generic.retry.Options} [options]
|
|
73
|
+
* @return {Promise<import('node-fetch').Response>}
|
|
74
|
+
*/
|
|
75
|
+
async #request (url, request, options) {
|
|
76
|
+
const call = () => fetch(url, request)
|
|
77
|
+
|
|
78
|
+
if (options === undefined) return call()
|
|
79
|
+
else return this.#retry(call, options)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {Function} call
|
|
84
|
+
* @param {toa.generic.retry.Options} options
|
|
85
|
+
* @return {any}
|
|
86
|
+
*/
|
|
87
|
+
#retry (call, options) {
|
|
88
|
+
return retry(async (retry) => {
|
|
89
|
+
const response = await call()
|
|
90
|
+
|
|
91
|
+
if (Math.floor(response.status / 100) !== 2) return retry()
|
|
92
|
+
|
|
93
|
+
return response
|
|
94
|
+
}, options)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {string} origin
|
|
100
|
+
* @param {string[]} substitutions
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
function substitute (origin, substitutions) {
|
|
104
|
+
const replace = () => substitutions.shift()
|
|
105
|
+
|
|
106
|
+
return origin.replace(PLACEHOLDER, replace)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} path
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
function isAbsoluteURL (path) {
|
|
114
|
+
return protocols.findIndex((protocol) => path.indexOf(protocol) === 0) !== -1
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const PLACEHOLDER = /\*/g
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {toa.origins.Manifest} manifest
|
|
121
|
+
* @param {toa.origins.http.Properties} [properties]
|
|
122
|
+
*/
|
|
123
|
+
function create (manifest, properties) {
|
|
124
|
+
const permissions = new Permissions(properties)
|
|
125
|
+
|
|
126
|
+
return new Aspect(manifest, permissions)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
exports.create = create
|