Skip to content

Commit e6bef42

Browse files
committed
Add credential provider helper methods
1 parent 3cc21c6 commit e6bef42

File tree

14 files changed

+264
-1
lines changed

14 files changed

+264
-1
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"description": "AWS SDK for JavaScript from the future",
66
"main": "index.js",
77
"scripts": {
8+
"bootstrap": "lerna bootstrap",
89
"test": "lerna run test"
910
},
1011
"repository": {
@@ -14,7 +15,7 @@
1415
"author": "[email protected]",
1516
"license": "UNLICENSED",
1617
"devDependencies": {
17-
"lerna": "^2.0.0-rc.1",
18+
"lerna": "^2.0.0-rc.2",
1819
"typescript": "^2.2.2"
1920
}
2021
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {chain} from "../lib/chain";
2+
import {fromCredentials} from "../lib/fromCredentials";
3+
import {isCredentials} from "../lib/isCredentials";
4+
5+
describe('chain', () => {
6+
it('should distill many credential providers into one', async () => {
7+
const provider = chain(
8+
fromCredentials({accessKeyId: 'foo', secretKey: 'bar'}),
9+
fromCredentials({accessKeyId: 'baz', secretKey: 'quux'}),
10+
);
11+
12+
expect(isCredentials(await provider())).toBe(true);
13+
});
14+
15+
it('should return the resolved value of the first successful promise', async () => {
16+
const creds = {accessKeyId: 'foo', secretKey: 'bar'};
17+
const provider = chain(
18+
() => Promise.reject('Move along'),
19+
() => Promise.reject('Nothing to see here'),
20+
fromCredentials(creds)
21+
);
22+
23+
expect(await provider()).toEqual(creds);
24+
});
25+
26+
it('should not invoke subsequent providers one resolves', async () => {
27+
const creds = {accessKeyId: 'foo', secretKey: 'bar'};
28+
const providers = [
29+
jest.fn(() => Promise.reject('Move along')),
30+
jest.fn(() => Promise.resolve(creds)),
31+
jest.fn(() => fail('This provider should not be invoked'))
32+
];
33+
34+
expect(await chain(...providers)()).toEqual(creds);
35+
expect(providers[0].mock.calls.length).toBe(1);
36+
expect(providers[1].mock.calls.length).toBe(1);
37+
expect(providers[2].mock.calls.length).toBe(0);
38+
});
39+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {CredentialProvider} from "../lib/CredentialProvider";
2+
import {Credentials} from "../lib/Credentials";
3+
import {fromCredentials} from "../lib/fromCredentials";
4+
5+
describe('fromCredentials', () => {
6+
it('should convert credentials into a credential provider', async () => {
7+
const credentials: Credentials = {accessKeyId: 'foo', secretKey: 'bar'};
8+
const provider: CredentialProvider = fromCredentials(credentials);
9+
10+
expect(typeof provider).toBe('function');
11+
expect(provider()).toBeInstanceOf(Promise);
12+
expect(await provider()).toEqual(credentials);
13+
});
14+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {isCredentials} from "../lib/isCredentials";
2+
3+
describe('isCredentials', () => {
4+
const minimalCredentials = {accessKeyId: 'foo', secretKey: 'bar'};
5+
6+
it('should reject scalar values', () => {
7+
for (let scalar of ['foo', 12, 1.2, true, null, undefined]) {
8+
expect(isCredentials(scalar)).toBe(false);
9+
}
10+
});
11+
12+
it('should accept an object with an accessKeyId and secretKey', () => {
13+
expect(isCredentials(minimalCredentials)).toBe(true);
14+
});
15+
16+
it('should reject objects where accessKeyId is not a string', () => {
17+
expect(isCredentials(
18+
Object.assign({}, minimalCredentials, {accessKeyId: 123})
19+
)).toBe(false);
20+
});
21+
22+
it('should reject objects where secretKey is not a string', () => {
23+
expect(isCredentials(
24+
Object.assign({}, minimalCredentials, {secretKey: 123})
25+
)).toBe(false);
26+
});
27+
28+
it('should accept credentials with a sessionToken', () => {
29+
expect(isCredentials(
30+
Object.assign({sessionToken: 'baz'}, minimalCredentials)
31+
)).toBe(true);
32+
});
33+
34+
it('should reject credentials where sessionToken is not a string', () => {
35+
expect(isCredentials(
36+
Object.assign({sessionToken: 123}, minimalCredentials)
37+
)).toBe(false);
38+
});
39+
40+
it('should accept credentials with an expiration', () => {
41+
expect(isCredentials(
42+
Object.assign({expiration: 0}, minimalCredentials)
43+
)).toBe(true);
44+
});
45+
46+
it('should reject credentials where expiration is not a number', () => {
47+
expect(isCredentials(
48+
Object.assign({expiration: 'quux'}, minimalCredentials)
49+
)).toBe(false);
50+
});
51+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {memoize} from "../lib/memoize";
2+
3+
describe('memoize', () => {
4+
it('should cache the resolved provider for permanent credentials', async () => {
5+
const creds = {accessKeyId: 'foo', secretKey: 'bar'};
6+
const provider = jest.fn(() => Promise.resolve(creds));
7+
const memoized = memoize(provider);
8+
9+
expect(await memoized()).toEqual(creds);
10+
expect(provider.mock.calls.length).toBe(1);
11+
expect(await memoized()).toEqual(creds);
12+
expect(provider.mock.calls.length).toBe(1);
13+
});
14+
15+
it('should invoke provider again when credentials expire', async () => {
16+
const clockMock = Date.now = jest.fn();
17+
clockMock.mockReturnValue(0);
18+
const provider = jest.fn(() => Promise.resolve({
19+
accessKeyId: 'foo',
20+
secretKey: 'bar',
21+
expiration: Date.now() + 600, // expires in ten minutes
22+
}));
23+
const memoized = memoize(provider);
24+
25+
expect((await memoized()).accessKeyId).toEqual('foo');
26+
expect(provider.mock.calls.length).toBe(1);
27+
expect((await memoized()).secretKey).toEqual('bar');
28+
expect(provider.mock.calls.length).toBe(1);
29+
30+
clockMock.mockReset();
31+
clockMock.mockReturnValue(601000); // One second past previous expiration
32+
33+
expect((await memoized()).accessKeyId).toEqual('foo');
34+
expect(provider.mock.calls.length).toBe(2);
35+
expect((await memoized()).secretKey).toEqual('bar');
36+
expect(provider.mock.calls.length).toBe(2);
37+
});
38+
});

packages/credential-provider/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './lib/chain';
2+
export * from './lib/fromCredentials';
3+
export * from './lib/isCredentials';
4+
export * from './lib/memoize';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {Credentials} from './Credentials';
2+
3+
export type CredentialProvider = () => Promise<Credentials>;
4+
5+
// TODO remove this file when the types package is merged to master
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface Credentials {
2+
readonly accessKeyId: string;
3+
readonly secretKey: string;
4+
readonly sessionToken?: string;
5+
readonly expiration?: number;
6+
}
7+
8+
// TODO remove this file when the types package is merged to master
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {CredentialProvider} from "./CredentialProvider";
2+
import {Credentials} from "./Credentials";
3+
4+
export function chain(...providers: Array<CredentialProvider>): CredentialProvider {
5+
return () => {
6+
providers = providers.slice(0);
7+
let provider = providers.shift();
8+
if (provider === undefined) {
9+
return Promise.reject<Credentials>('No credential providers in chain');
10+
}
11+
let promise = provider();
12+
while (provider = providers.shift()) {
13+
promise = promise.catch(provider);
14+
}
15+
16+
return promise;
17+
}
18+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {CredentialProvider} from "./CredentialProvider";
2+
import {Credentials} from "./Credentials";
3+
4+
export function fromCredentials(
5+
credentials: Credentials
6+
): CredentialProvider {
7+
return () => Promise.resolve(credentials);
8+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {Credentials} from './Credentials';
2+
3+
export function isCredentials(arg: any): arg is Credentials {
4+
return typeof arg === 'object'
5+
&& arg !== null
6+
&& typeof arg.accessKeyId === 'string'
7+
&& typeof arg.secretKey === 'string'
8+
&& ['string', 'undefined'].indexOf(typeof arg.sessionToken) > -1
9+
&& ['number', 'undefined'].indexOf(typeof arg.expiration) > -1;
10+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {CredentialProvider} from "./CredentialProvider";
2+
import {Credentials} from "./Credentials";
3+
4+
export function memoize(
5+
provider: CredentialProvider
6+
): CredentialProvider {
7+
let result: Promise<Credentials> = provider();
8+
let isConstant: boolean = false;
9+
10+
return () => {
11+
if (isConstant) {
12+
return result;
13+
}
14+
15+
return result.then<Credentials>(credentials => {
16+
if (!credentials.expiration) {
17+
isConstant = true;
18+
return credentials;
19+
}
20+
21+
if (credentials.expiration - 300 > getEpochTs()) {
22+
return credentials;
23+
}
24+
25+
return result = provider();
26+
});
27+
}
28+
}
29+
30+
function getEpochTs() {
31+
return Math.floor(Date.now() / 1000);
32+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "@aws/credentials",
3+
"version": "0.0.1",
4+
"private": true,
5+
"description": "AWS credential provider for node",
6+
"main": "index.js",
7+
"scripts": {
8+
"prepublishOnly": "tsc",
9+
"pretest": "tsc",
10+
"test": "jest"
11+
},
12+
"keywords": [
13+
"aws",
14+
"credentials"
15+
],
16+
"author": "[email protected]",
17+
"license": "UNLICENSED",
18+
"devDependencies": {
19+
"@types/jest": "^19.2.2",
20+
"@types/node": "^7.0.12",
21+
"jest": "^19.0.2",
22+
"typescript": "^2.2.2"
23+
}
24+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"alwaysStrict": true,
4+
"module": "commonjs",
5+
"target": "es6",
6+
"strictNullChecks": true,
7+
"noImplicitAny": true,
8+
"noImplicitThis": true,
9+
"sourceMap": true
10+
}
11+
}

0 commit comments

Comments
 (0)