@adobe/fetch
Advanced tools
Comparing version 0.2.4 to 0.3.0
@@ -19,3 +19,7 @@ /* | ||
}, | ||
globals: { | ||
fetch: "readonly", | ||
localStorage: "readonly", | ||
window: "readonly" | ||
}, | ||
env: { | ||
@@ -22,0 +26,0 @@ es6: true, |
188
index.js
@@ -13,186 +13,12 @@ /* | ||
const fetch = require('node-fetch'); | ||
const cache = require('./src/cache'); | ||
const uuid = require('uuid/v4'); | ||
const storage = require('./src/storage'); | ||
const auth = require('@adobe/jwt-auth'); | ||
const debug = require('debug')('@adobe/fetch'); | ||
const NO_CONFIG = 'Auth configuration missing.'; | ||
global.fetch = require('node-fetch'); | ||
const adobefetch = require('./src/adobefetch'); | ||
async function getToken(authOptions, tokenCache, forceNewToken) { | ||
const key = authOptions.clientId + '|' + authOptions.metaScopes.join(','); | ||
let token = await tokenCache.get(key); | ||
if (token && !forceNewToken) { | ||
return token; | ||
} else { | ||
try { | ||
token = await auth(authOptions); | ||
if (token) { | ||
return tokenCache.set(key, token); | ||
} else { | ||
throw 'Access token empty'; | ||
} | ||
} catch (err) { | ||
console.error('Error while getting a new access token.', err); | ||
throw err; | ||
} | ||
} | ||
} | ||
function capFirst(s) { | ||
return s[0].toUpperCase() + s.slice(1); | ||
} | ||
function generateRequestID() { | ||
return uuid().replace(/-/g, ''); | ||
} | ||
function normalizeHeaders(headers) { | ||
let normalized = {}; | ||
if (headers) { | ||
if (typeof headers.entries === 'function') { | ||
// This is a headers object, iterate with for..of. | ||
for (let pair of headers.entries()) { | ||
const [name, value] = pair; | ||
normalized[name.toLowerCase()] = value; | ||
} | ||
} else { | ||
// This is a normal JSON. Iterate with for.. in | ||
for (let name in headers) { | ||
normalized[name.toLowerCase()] = headers[name]; | ||
} | ||
} | ||
} | ||
return normalized; | ||
} | ||
function calculateHeaders(predefinedHeaders) { | ||
let headers = {}; | ||
for (let name in predefinedHeaders) { | ||
const value = predefinedHeaders[name]; | ||
if (typeof value === 'function') { | ||
headers[name] = value(); | ||
} else { | ||
headers[name] = value; | ||
} | ||
} | ||
return headers; | ||
} | ||
function getHeaders(token, options, predefinedHeaders) { | ||
let headers = calculateHeaders(predefinedHeaders); | ||
if (options && options.headers) { | ||
headers = Object.assign(headers, normalizeHeaders(options.headers)); | ||
} | ||
headers.authorization = `${capFirst(token.token_type)} ${token.access_token}`; | ||
return headers; | ||
} | ||
async function _fetch(url, opts, configOptions, tokenCache, forceNewToken) { | ||
const token = await getToken(configOptions.auth, tokenCache, forceNewToken); | ||
const fetchOpts = Object.assign({}, opts); | ||
fetchOpts.headers = getHeaders(token, opts, configOptions.headers); | ||
debug( | ||
`${fetchOpts.method || 'GET'} ${url} - x-request-id: ${ | ||
fetchOpts.headers['x-request-id'] | ||
}` | ||
); | ||
const res = await fetch(url, fetchOpts); | ||
if (!res.ok) { | ||
debug( | ||
`${fetchOpts.method || 'GET'} ${url} - status ${res.statusText} (${ | ||
res.status | ||
}). x-request-id: ${fetchOpts.headers['x-request-id']}` | ||
); | ||
if ((res.status === 401 || res.status === 403) && !forceNewToken) { | ||
debug(`${opts.method || 'GET'} ${url} - Will get new token.`); | ||
return await _fetch(url, opts, configOptions, tokenCache, true); | ||
} | ||
} | ||
return res; | ||
} | ||
/** | ||
* Fetch function | ||
* | ||
* @return Promise | ||
* @param url | ||
* @param options | ||
*/ | ||
function adobefetch(url, options, configOptions, tokenCache) { | ||
return _fetch(url, options, configOptions, tokenCache, false); | ||
} | ||
function verifyConfig(options) { | ||
let { | ||
clientId, | ||
technicalAccountId, | ||
orgId, | ||
clientSecret, | ||
privateKey, | ||
metaScopes, | ||
storage | ||
} = options; | ||
const errors = []; | ||
!clientId ? errors.push('clientId') : ''; | ||
!technicalAccountId ? errors.push('technicalAccountId') : ''; | ||
!orgId ? errors.push('orgId') : ''; | ||
!clientSecret ? errors.push('clientSecret') : ''; | ||
!privateKey ? errors.push('privateKey') : ''; | ||
!metaScopes || metaScopes.length === 0 ? errors.push('metaScopes') : ''; | ||
if (errors.length > 0) { | ||
throw `Required parameter(s) ${errors.join(', ')} are missing`; | ||
} | ||
if ( | ||
!( | ||
typeof privateKey === 'string' || | ||
privateKey instanceof Buffer || | ||
ArrayBuffer.isView(privateKey) | ||
) | ||
) { | ||
throw 'Required parameter privateKey is invalid'; | ||
} | ||
if (storage) { | ||
let { read, write } = storage; | ||
if (!read) { | ||
throw 'Storage read method missing!'; | ||
} else if (!write) { | ||
throw 'Storage write method missing!'; | ||
} | ||
} | ||
} | ||
function config(configOptions) { | ||
if (!configOptions.auth) { | ||
throw NO_CONFIG; | ||
} else { | ||
verifyConfig(configOptions.auth); | ||
} | ||
const tokenCache = cache.config(configOptions.auth); | ||
configOptions.headers = Object.assign( | ||
{ | ||
'x-api-key': configOptions.auth.clientId, | ||
'x-request-id': () => generateRequestID(), | ||
'x-gw-ims-org-id': configOptions.auth.orgId | ||
}, | ||
normalizeHeaders(configOptions.headers) | ||
); | ||
return (url, options = {}) => | ||
adobefetch(url, options, configOptions, tokenCache); | ||
} | ||
module.exports = { | ||
config: config, | ||
normalizeHeaders: normalizeHeaders, | ||
generateRequestID: generateRequestID | ||
config: adobefetch.getConfig(storage, auth), | ||
normalizeHeaders: adobefetch.normalizeHeaders, | ||
generateRequestID: adobefetch.generateRequestID, | ||
AUTH_MODES: adobefetch.AUTH_MODES | ||
}; |
{ | ||
"name": "@adobe/fetch", | ||
"version": "0.2.4", | ||
"version": "0.3.0", | ||
"description": "Call Adobe APIs", | ||
"main": "index.js", | ||
"main": "dist/server.js", | ||
"browser": "dist/client.js", | ||
"repository": { | ||
@@ -17,5 +18,5 @@ "type": "git", | ||
"scripts": { | ||
"lint": "eslint src test sample index.js", | ||
"run": "node index.js", | ||
"test": "jest" | ||
"lint": "eslint src test sample index*.js", | ||
"test": "jest", | ||
"build": "webpack" | ||
}, | ||
@@ -37,3 +38,3 @@ "keywords": [ | ||
"debug": "^4.1.1", | ||
"dotenv": "^8.1.0", | ||
"dotenv": "^8.2.0", | ||
"node-fetch": "^2.6.0", | ||
@@ -45,6 +46,9 @@ "node-persist": "^3.0.5", | ||
"eslint": "^6.6.0", | ||
"eslint-config-prettier": "^6.5.0", | ||
"eslint-config-prettier": "^6.7.0", | ||
"eslint-plugin-prettier": "^3.1.1", | ||
"jest": "^24.9.0", | ||
"prettier": "^1.18.2" | ||
"prettier": "^1.19.1", | ||
"webpack": "^4.41.2", | ||
"webpack-cli": "^3.3.10", | ||
"webpack-node-externals": "^1.7.2" | ||
}, | ||
@@ -51,0 +55,0 @@ "jest": { |
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) | ||
[![Version](https://img.shields.io/npm/v/@adobe/fetch.svg)](https://npmjs.org/package/@adobe/fetch) | ||
[![Downloads/week](https://img.shields.io/npm/dw/@adobe/fetch.svg)](https://npmjs.org/package/@adobe/fetch) | ||
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) | ||
[![Build Status](https://travis-ci.com/adobe/adobe-fetch.svg?branch=master)](https://travis-ci.com/adobe/adobe-fetch) | ||
[![codecov](https://codecov.io/gh/adobe/adobe-fetch/branch/master/graph/badge.svg)](https://codecov.io/gh/adobe/adobe-fetch) | ||
[![Greenkeeper badge](https://badges.greenkeeper.io/adobe/adobe-fetch.svg)](https://greenkeeper.io/) | ||
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/adobe/adobe-fetch.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/adobe/adobe-fetch/context:javascript) | ||
@@ -18,4 +18,6 @@ # adobe-fetch | ||
This package will handle JWT authentication, token caching and storage. | ||
Otherwise it works exactly as [fetch](https://github.com/bitinn/node-fetch) | ||
Otherwise it works exactly as [fetch](https://github.com/bitinn/node-fetch). | ||
This library now works in the browser as well, see information below. | ||
### Installation | ||
@@ -31,2 +33,3 @@ | ||
const AdobeFetch = require('@adobe/fetch'); | ||
const fs = require('fs'); | ||
@@ -46,3 +49,3 @@ | ||
const adobefetch = require('@adobe/fetch').config(config); | ||
const adobefetch = AdobeFetch.config(config); | ||
@@ -112,2 +115,47 @@ adobefetch("https://platform.adobe.io/some/adobe/api", { method: 'get'}) | ||
#### Alternative authentication methods | ||
To use this library with an alternative authentication flow such as OAuth, or execute the JWT authentication flow outside of adobe-fetch, it is possible to use the **Provided** mode and provide the access token directly to adobe-fetch via an asynchronious function: | ||
```javascript | ||
const AdobeFetch = require('@adobe/fetch'); | ||
const { AUTH_MODES } = AdobeFetch; | ||
const adobefetch = AdobeFetch).config({ | ||
auth: { | ||
mode: AUTH_MODES.Provided, | ||
clientId: 'asasdfasf', | ||
orgId: 'asdfasdfasdf@AdobeOrg', | ||
tokenProvider: async () => { ... Logic returning a valid access token object ... } | ||
} | ||
}); | ||
adobefetch("https://platform.adobe.io/some/adobe/api", { method: 'get'}) | ||
.then(response => response.json()) | ||
.then(json => console.log('Result: ',json)); | ||
``` | ||
When the **adobefetch** call above happens for the first time, it will call the tokenProvider function provided and wait for it to return the access token. Access token is then cached and persisted, if it expires or is rejected by the API, the tokenProvider function will be called again to acquire a new token. | ||
A valid token has the following structure: | ||
``` | ||
{ | ||
token_type: 'bearer', | ||
access_token: <<<TOKEN>>>, | ||
expires_in: <<<EXPIRY_IN_MILLISECONDS>>> | ||
} | ||
``` | ||
#### Using in the browser | ||
In the browser only the **Provided** mode explained above is default, JWT is not supported. | ||
This is because the JWT workflow requires direct access to the private key and should be done in the server for security reasons. With Provided mode the access token can be acquired via a standard OAuth authentication flow and then used by adobe-fetch to call Adobe APIs. | ||
Using ```require('@adobe/fetch')``` in a web app will automatically use the browser version. | ||
You can also include the [bundled JS](dist/client.js) file directly in a script tag. | ||
#### Predefined Headers | ||
@@ -114,0 +162,0 @@ |
@@ -30,5 +30,9 @@ /* | ||
const AdobeFetch = require('../index.js'); | ||
const { AUTH_MODES } = AdobeFetch; | ||
async function main() { | ||
const adobefetch = require('../index.js').config({ | ||
const adobefetch = AdobeFetch.config({ | ||
auth: { | ||
mode: AUTH_MODES.JWT, | ||
clientId: process.env.APIKEY, | ||
@@ -35,0 +39,0 @@ clientSecret: process.env.SECRET, |
@@ -14,4 +14,2 @@ /* eslint-disable require-atomic-updates */ | ||
const storage = require('./storage'); | ||
// Consider a token expired 60 seconds before its calculated expiry time. | ||
@@ -91,6 +89,8 @@ const EXPIRY_THRESHOLD = 60 * 1000; | ||
function config(options) { | ||
function config(options, defaultStorage) { | ||
const disableStorage = (options && options.disableStorage) || false; | ||
const readFunc = options.storage ? options.storage.read : storage.read; | ||
const writeFunc = options.storage ? options.storage.write : storage.write; | ||
const readFunc = options.storage ? options.storage.read : defaultStorage.read; | ||
const writeFunc = options.storage | ||
? options.storage.write | ||
: defaultStorage.write; | ||
@@ -97,0 +97,0 @@ const cache = { |
@@ -21,3 +21,9 @@ /* | ||
const TOKEN_KEY = `${CLIENT_ID}|${SCOPES.join(',')}`; | ||
const TOKEN_PROVIDED_KEY = `${CLIENT_ID}|org-${ORG_ID}`; | ||
const MOCK_URL = 'https://mock.com/mock'; | ||
const DEFAULT_TOKEN = { | ||
token_type: 'bearer', | ||
access_token: 'abcdef', | ||
expires_in: 86399956 | ||
}; | ||
@@ -58,8 +64,19 @@ module.exports = { | ||
}, | ||
providedConfig: { | ||
mode: 'provided', | ||
clientId: CLIENT_ID, | ||
orgId: ORG_ID, | ||
tokenProvider: async () => DEFAULT_TOKEN | ||
}, | ||
customProvidedConfig: provider => { | ||
return { | ||
mode: 'provided', | ||
clientId: CLIENT_ID, | ||
orgId: ORG_ID, | ||
tokenProvider: provider | ||
}; | ||
}, | ||
token_key: TOKEN_KEY, | ||
token: { | ||
token_type: 'bearer', | ||
access_token: 'abcdef', | ||
expires_in: 86399956 | ||
}, | ||
token_provided_key: TOKEN_PROVIDED_KEY, | ||
token: DEFAULT_TOKEN, | ||
token2: { | ||
@@ -76,2 +93,8 @@ token_type: 'bearer', | ||
expires_at: Date.now() + 100000 | ||
}, | ||
[TOKEN_PROVIDED_KEY]: { | ||
token_type: 'bearer', | ||
access_token: 'abcabc', | ||
expires_in: 86399956, | ||
expires_at: Date.now() + 100000 | ||
} | ||
@@ -78,0 +101,0 @@ }, |
@@ -16,4 +16,19 @@ /* | ||
const adobefetch = require('../index'); | ||
const adobefetchBrowser = require('../index.client'); | ||
const mockData = require('./mockData'); | ||
const localStorageMock = (function() { | ||
let store = {}; | ||
return { | ||
getItem: key => store[key] || null, | ||
setItem: (key, value) => (store[key] = value.toString()), | ||
removeItem: key => delete store[key], | ||
clear: () => (store = {}) | ||
}; | ||
})(); | ||
Object.defineProperty(window, 'localStorage', { | ||
value: localStorageMock | ||
}); | ||
jest.mock('@adobe/jwt-auth'); | ||
@@ -176,1 +191,43 @@ jest.mock('node-fetch'); | ||
}); | ||
describe('Validate local storage', () => { | ||
test('reads from local storage', async () => { | ||
expect.assertions(2); | ||
const token = mockData.valid_token[mockData.token_provided_key]; | ||
fetch.mockImplementation((url, options) => { | ||
expect(options.headers).toBeDefined(); | ||
expect(options.headers['authorization']).toBe( | ||
`Bearer ${token.access_token}` | ||
); | ||
return Promise.resolve(mockData.responseOK); | ||
}); | ||
window.localStorage.clear(); | ||
window.localStorage.setItem('tokens', JSON.stringify(mockData.valid_token)); | ||
await adobefetchBrowser.config({ | ||
auth: mockData.providedConfig | ||
})(mockData.url); | ||
}); | ||
test('write to local storage', async () => { | ||
expect.assertions(1); | ||
const token = mockData.valid_token[mockData.token_provided_key]; | ||
window.localStorage.clear(); | ||
fetch.mockImplementation(() => Promise.resolve(mockData.responseOK)); | ||
await adobefetchBrowser.config({ | ||
auth: mockData.customProvidedConfig( | ||
() => mockData.valid_token[mockData.token_provided_key] | ||
) | ||
})(mockData.url); | ||
const tokens = JSON.parse(window.localStorage.getItem('tokens')); | ||
expect(tokens[mockData.token_provided_key]).toStrictEqual(token); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1175815
36
1510
267
8
3
25
Updateddotenv@^8.2.0