sharetribe-flex-sdk 1.14.0

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 (87) hide show
  1. package/.circleci/config.yml +22 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.js +19 -0
  4. package/CHANGELOG.md +231 -0
  5. package/LICENSE +201 -0
  6. package/README.md +76 -0
  7. package/build/sharetribe-flex-sdk-node.js +13592 -0
  8. package/build/sharetribe-flex-sdk-web.js +1 -0
  9. package/docs/README.md +20 -0
  10. package/docs/authentication.md +179 -0
  11. package/docs/calling-the-api.md +217 -0
  12. package/docs/configurations.md +95 -0
  13. package/docs/developing-sdk.md +131 -0
  14. package/docs/docpress.json +14 -0
  15. package/docs/features.md +14 -0
  16. package/docs/keep-alive.md +48 -0
  17. package/docs/object-query-parameters.md +36 -0
  18. package/docs/scripts.js +41 -0
  19. package/docs/serializing-types-to-json.md +40 -0
  20. package/docs/sharing-session-between-client-and-server.md +19 -0
  21. package/docs/styles.css +95 -0
  22. package/docs/token-store.md +114 -0
  23. package/docs/try-it-in-browser.md +32 -0
  24. package/docs/try-it-in-the-playground.md +153 -0
  25. package/docs/types.md +27 -0
  26. package/docs/writing-your-own-token-store.md +29 -0
  27. package/docs/your-own-types.md +61 -0
  28. package/examples/README.md +5 -0
  29. package/examples/getting-started-browser/README.md +22 -0
  30. package/examples/getting-started-browser/index.html +89 -0
  31. package/examples/getting-started-browser/index.js +156 -0
  32. package/examples/getting-started-browser/screenshots/screenshot1.png +0 -0
  33. package/examples/getting-started-browser/screenshots/screenshot2.png +0 -0
  34. package/examples/getting-started-node/README.md +23 -0
  35. package/examples/getting-started-node/index.js +139 -0
  36. package/examples/getting-started-node/screenshots/screenshot.png +0 -0
  37. package/package.json +83 -0
  38. package/playground.js +295 -0
  39. package/src/browser_cookie_store.js +26 -0
  40. package/src/context_runner.js +151 -0
  41. package/src/context_runner.test.js +185 -0
  42. package/src/detect.js +11 -0
  43. package/src/express_cookie_store.js +57 -0
  44. package/src/fake/adapter.js +130 -0
  45. package/src/fake/api.js +137 -0
  46. package/src/fake/auth.js +84 -0
  47. package/src/fake/token_store.js +231 -0
  48. package/src/index.js +25 -0
  49. package/src/interceptors/.eslintrc.js +5 -0
  50. package/src/interceptors/add_auth_header.js +32 -0
  51. package/src/interceptors/add_auth_header.test.js +50 -0
  52. package/src/interceptors/add_auth_token_response.js +16 -0
  53. package/src/interceptors/add_client_id_to_params.js +12 -0
  54. package/src/interceptors/add_client_secret_to_params.js +15 -0
  55. package/src/interceptors/add_grant_type_to_params.js +23 -0
  56. package/src/interceptors/add_idp_client_id_to_params.js +17 -0
  57. package/src/interceptors/add_idp_id_to_params.js +17 -0
  58. package/src/interceptors/add_idp_token_to_params.js +17 -0
  59. package/src/interceptors/add_scope_to_params.js +18 -0
  60. package/src/interceptors/add_subject_token_to_params.js +18 -0
  61. package/src/interceptors/add_token_exchange_grant_type_to_params.js +12 -0
  62. package/src/interceptors/auth_info.js +50 -0
  63. package/src/interceptors/clear_token_after_revoke.js +45 -0
  64. package/src/interceptors/default_params.js +12 -0
  65. package/src/interceptors/fetch_auth_token_from_api.js +33 -0
  66. package/src/interceptors/fetch_auth_token_from_store.js +27 -0
  67. package/src/interceptors/fetch_refresh_token_for_revoke.js +24 -0
  68. package/src/interceptors/multipart_request.js +35 -0
  69. package/src/interceptors/retry_with_anon_token.js +58 -0
  70. package/src/interceptors/retry_with_refresh_token.js +70 -0
  71. package/src/interceptors/save_token.js +20 -0
  72. package/src/interceptors/transit_request.js +27 -0
  73. package/src/interceptors/transit_request.test.js +58 -0
  74. package/src/interceptors/transit_response.js +27 -0
  75. package/src/memory_store.js +19 -0
  76. package/src/params_serializer.js +65 -0
  77. package/src/params_serializer.test.js +58 -0
  78. package/src/sdk.js +894 -0
  79. package/src/sdk.test.js +908 -0
  80. package/src/serializer.js +279 -0
  81. package/src/serializer.test.js +229 -0
  82. package/src/token_store.js +15 -0
  83. package/src/types.js +108 -0
  84. package/src/types.test.js +75 -0
  85. package/src/utils.js +68 -0
  86. package/src/utils.test.js +85 -0
  87. package/webpack.config.babel.js +47 -0
@@ -0,0 +1,57 @@
1
+ const generateKey = (clientId, namespace) => `${namespace}-${clientId}-token`;
2
+
3
+ const createStore = ({ clientId, req, res, secure }) => {
4
+ const expiration = 180; // 180 days
5
+ const namespace = 'st';
6
+ const key = generateKey(clientId, namespace);
7
+
8
+ // A mutable variable containing the current token.
9
+ // When a `setToken` is called, the current token will be
10
+ // stored to this variable. `getToken` will read subsequent
11
+ // calls from this variable.
12
+ let currentToken;
13
+
14
+ const readCookie = () => {
15
+ const cookie = req.cookies[key];
16
+
17
+ if (cookie) {
18
+ return JSON.parse(cookie);
19
+ }
20
+
21
+ return null;
22
+ };
23
+
24
+ const getToken = () => {
25
+ currentToken = currentToken || readCookie();
26
+
27
+ return currentToken;
28
+ };
29
+
30
+ const setToken = tokenData => {
31
+ currentToken = tokenData;
32
+ const secureFlag = secure ? { secure: true } : {};
33
+
34
+ // Manually stringify tokenData.
35
+ // Express supports passing object to `res.cookie` which will be then automatically
36
+ // JSON stringified. However, we CAN NOT use it, because it seems to output invalid JSON
37
+ // with a "j" tag in front of the content (`"j:{ ...json here... }`). Because we want
38
+ // to read that cookie also in browser, we don't want to produce invalid JSON.
39
+ res.cookie(key, JSON.stringify(tokenData), {
40
+ maxAge: 1000 * 60 * 60 * 24 * expiration,
41
+ ...secureFlag,
42
+ });
43
+ };
44
+
45
+ const removeToken = () => {
46
+ currentToken = null;
47
+ res.clearCookie(key);
48
+ };
49
+
50
+ return {
51
+ getToken,
52
+ setToken,
53
+ removeToken,
54
+ };
55
+ };
56
+
57
+ export default createStore;
@@ -0,0 +1,130 @@
1
+ import _ from 'lodash';
2
+ import createTokenStore from './token_store';
3
+ import * as auth from './auth';
4
+ import * as api from './api';
5
+
6
+ /**
7
+ This file implements a fake adapters for testing purposes only.
8
+
9
+ The test responses are copy-pasted from real API responses.
10
+ */
11
+
12
+ const adapterHelper = adapterDef => config =>
13
+ new Promise((resolve, reject) => {
14
+ const rejectWithError = errorOrResponse => {
15
+ if (errorOrResponse instanceof Error) {
16
+ return reject(errorOrResponse);
17
+ }
18
+
19
+ const error = new Error(`Request failed with status code ${errorOrResponse.status}`);
20
+ error.response = errorOrResponse;
21
+
22
+ return reject(error);
23
+ };
24
+
25
+ adapterDef.call(null, config, resolve, rejectWithError);
26
+ });
27
+
28
+ const parseAuthorizationHeader = value => {
29
+ if (!_.isString(value)) {
30
+ return {};
31
+ }
32
+
33
+ const splitted = value.split(' ');
34
+
35
+ return {
36
+ tokenType: splitted[0],
37
+ accessToken: splitted[1],
38
+ };
39
+ };
40
+
41
+ const requireAuth = (config, reject, tokenStore) => {
42
+ const { accessToken, tokenType } = parseAuthorizationHeader(config.headers.Authorization);
43
+
44
+ if (!accessToken && !tokenType) {
45
+ return reject({
46
+ status: 401,
47
+ data: '{}', // FIXME This is not what the server sends
48
+
49
+ __additionalTestInfo: 'Authorization header missing',
50
+ });
51
+ }
52
+
53
+ const validToken = tokenStore.validToken(accessToken, tokenType);
54
+
55
+ if (validToken) {
56
+ return Promise.resolve();
57
+ }
58
+
59
+ return reject({
60
+ status: 401,
61
+ data: '{}', // FIXME This is not what the server sends
62
+ });
63
+ };
64
+
65
+ const defaultHandler = (config, resolve, reject, tokenStore) => {
66
+ switch (config.url) {
67
+ case 'api/users/show':
68
+ return requireAuth(config, reject, tokenStore).then(() => api.users.show(config, resolve));
69
+ case 'api/marketplace/show':
70
+ return requireAuth(config, reject, tokenStore).then(() =>
71
+ api.marketplace.show(config, resolve)
72
+ );
73
+ case 'api/listings/search':
74
+ return requireAuth(config, reject, tokenStore).then(() =>
75
+ api.listings.search(config, resolve)
76
+ );
77
+ case 'api/own_listings/create':
78
+ return requireAuth(config, reject, tokenStore).then(() =>
79
+ api.ownListings.create(config, resolve, reject)
80
+ );
81
+ case 'auth/token':
82
+ return auth.token(config, resolve, reject, tokenStore);
83
+ case 'auth/revoke':
84
+ return requireAuth(config, reject, tokenStore).then(() =>
85
+ auth.revoke(config, resolve, reject, tokenStore)
86
+ );
87
+ case 'auth/auth_with_idp':
88
+ return auth.authWithIdp(config, resolve, reject, tokenStore);
89
+ default:
90
+ throw new Error(`Not implemented to Fake adapter: ${config.url}`);
91
+ }
92
+ };
93
+
94
+ /**
95
+ Create a fake adapter instance.
96
+
97
+ Features:
98
+
99
+ - Handle requests
100
+ - Store all requests (so that they can be inspected in tests)
101
+ - Implement fake token store
102
+ */
103
+ const createAdapter = handlerFn => {
104
+ const requests = [];
105
+ const tokenStore = createTokenStore();
106
+ let offlineAfter;
107
+ const offline = () => offlineAfter != null && requests.length > offlineAfter;
108
+ const handler = handlerFn || defaultHandler;
109
+
110
+ return {
111
+ requests,
112
+ tokenStore,
113
+ offlineAfter: numOfRequests => {
114
+ offlineAfter = numOfRequests;
115
+ },
116
+ adapterFn: adapterHelper((config, resolve, reject) => {
117
+ // Store each request to `requests` array
118
+ requests.push(config);
119
+
120
+ if (offline()) {
121
+ return reject(new Error('Network error'));
122
+ }
123
+
124
+ // Call router to handle the request
125
+ return handler(config, resolve, reject, tokenStore);
126
+ }),
127
+ };
128
+ };
129
+
130
+ export default createAdapter;
@@ -0,0 +1,137 @@
1
+ import transit from 'transit-js';
2
+
3
+ const reader = transit.reader('json');
4
+
5
+ export const marketplace = {
6
+ show: (config, resolve) => {
7
+ const res = `["^ ",
8
+ "~:data", ["^ ",
9
+ "~:id", "~u${config.params.id}",
10
+ "~:type", "~:marketplace",
11
+ "~:attributes", ["^ ",
12
+ "~:name", "Awesome skies.",
13
+ "~:description", "Meet and greet with fanatical sky divers."],
14
+ "~:relationships", ["^ "]],
15
+ "~:meta", ["^ "],
16
+ "~:included", []]`;
17
+
18
+ return resolve({ data: res });
19
+ },
20
+ };
21
+
22
+ export const users = {
23
+ show: (config, resolve) => {
24
+ const res = `["^ ",
25
+ "~:data", ["^ ",
26
+ "~:id", "~u0e0b60fe-d9a2-11e6-bf26-cec0c932ce01",
27
+ "~:type", "~:user",
28
+ "~:attributes", ["^ ",
29
+ "~:email", "user@sharetribe.com",
30
+ "~:description", "A team member"],
31
+ "~:relationships", ["^ "]],
32
+ "~:meta", ["^ "],
33
+ "~:included", []]`;
34
+
35
+ return resolve({ data: res });
36
+ },
37
+ };
38
+
39
+ export const listings = {
40
+ search: (config, resolve) => {
41
+ const res = `["^ ",
42
+ "~:data", [
43
+ ["^ ",
44
+ "~:id", "~u9009efe1-25ec-4ed5-9413-e80c584ff6bf",
45
+ "~:type", "~:listing",
46
+ "~:links", ["^ ",
47
+ "~:self", "/v1/api/listings/show?id=9009efe1-25ec-4ed5-9413-e80c584ff6bf"],
48
+ "~:attributes", ["^ ",
49
+ "~:title", "Nishiki 401",
50
+ "~:description", "27-speed Hybrid. Fully functional.",
51
+ "~:address", "230 Hamilton Ave, Staten Island, NY 10301, USA",
52
+ "~:geolocation", [
53
+ "~#geo", [40.64542, -74.08508]]],
54
+ "~:relationships", ["^ ",
55
+ "~:author", ["^ ",
56
+ "^4", ["^ ",
57
+ "~:related", "/v1/api/users/show?id=3c073fae-6172-4e75-8b92-f560d58cd47c"]],
58
+ "~:marketplace", ["^ ",
59
+ "^4", ["^ ",
60
+ "^>", "/v1/api/marketplace/show"]]]],
61
+ ["^ ",
62
+ "^1", "~u5e1f2086-522c-46f3-87b4-451c6770c833",
63
+ "^2", "^3",
64
+ "^4", ["^ ",
65
+ "^5", "/v1/api/listings/show?id=5e1f2086-522c-46f3-87b4-451c6770c833"],
66
+ "^6", ["^ ",
67
+ "^7", "Pelago Brooklyn",
68
+ "^8", "Goes together perfectly with a latte and a bow tie.",
69
+ "^9", "230 Hamilton Ave, Staten Island, NY 10301, USA",
70
+ "^:", [
71
+ "^;", [40.64542, -74.08508]]],
72
+ "^<", ["^ ",
73
+ "^=", ["^ ",
74
+ "^4", ["^ ",
75
+ "^>", "/v1/api/users/show?id=3c073fae-6172-4e75-8b92-f560d58cd47c"]],
76
+ "^?", ["^ ",
77
+ "^4", ["^ ",
78
+ "^>", "/v1/api/marketplace/show"]]]]],
79
+ "~:meta", ["^ "],
80
+ "~:included", []]`;
81
+
82
+ return resolve({ data: res });
83
+ },
84
+ };
85
+
86
+ export const ownListings = {
87
+ create: (config, resolve, reject) => {
88
+ const body = reader.read(config.data);
89
+
90
+ const requiredFields = ['title', 'description', 'address', 'geolocation'].map(k =>
91
+ body.get(transit.keyword(k))
92
+ );
93
+
94
+ if (requiredFields.some(v => v == null)) {
95
+ return reject({
96
+ status: 400,
97
+ statusText: 'Bad Request',
98
+ data: `["^ ",
99
+ "~:errors", [
100
+ ["^ ",
101
+ "~:id", "~u57b3f476-19a0-4e07-9a44-923d9dbbe361",
102
+ "~:status", 400,
103
+ "~:code", "bad-request",
104
+ "~:title", "Bad request",
105
+ "~:details", ["^ ",
106
+ "~:error", ["^ ",
107
+ "~:body-params", ["^ ",
108
+ "^4", "missing-required-key",
109
+ "~:description", "missing-required-key",
110
+ "~:address", "missing-required-key",
111
+ "~:geolocation", "missing-required-key"]]]]]]`,
112
+ });
113
+ }
114
+
115
+ let res;
116
+
117
+ if (config.params.expand === true) {
118
+ res = `["^ ",
119
+ "~:data", ["^ ",
120
+ "~:id", "~u58c660f5-a39a-49a5-9270-8a917b7d6c9e",
121
+ "~:type", "~:ownListing",
122
+ "~:attributes", ["^ ",
123
+ "~:title", "Pelago bike",
124
+ "~:description", "City bike for city hipster!",
125
+ "~:price", ["~#mn", [12000, "USD"]],
126
+ "~:address", "Bulevardi 14, 00200 Helsinki, Finland",
127
+ "~:geolocation", ["~#geo", [40.0, 73.0]]]]]`;
128
+ } else {
129
+ res = `["^ ",
130
+ "~:data", ["^ ",
131
+ "~:id", "~u58c6610d-1ffd-4fa5-b386-4f9b6e46e732",
132
+ "~:type", "~:ownListing"]]`;
133
+ }
134
+
135
+ return resolve({ data: res });
136
+ },
137
+ };
@@ -0,0 +1,84 @@
1
+ import _ from 'lodash';
2
+
3
+ const parseFormData = data =>
4
+ _.fromPairs(data.split('&').map(keyValue => keyValue.split('=').map(decodeURIComponent)));
5
+
6
+ export const revoke = (config, resolve, reject, tokenStore) => {
7
+ const formData = parseFormData(config.data);
8
+
9
+ if (formData.token) {
10
+ if (tokenStore) {
11
+ const revoked = tokenStore.revokeRefreshToken(formData.token);
12
+
13
+ if (revoked.length) {
14
+ return resolve({ data: { action: 'revoked' } });
15
+ }
16
+ }
17
+
18
+ // FIXME The `data` is not what the server returns
19
+ return resolve({ data: { action: 'nothing' } });
20
+ }
21
+
22
+ // FIXME The `data` is not what the server returns
23
+ return reject({ data: {}, __additionalTestInfo: formData });
24
+ };
25
+
26
+ export const token = (config, resolve, reject, fakeTokenStore) => {
27
+ const formData = parseFormData(config.data);
28
+ let res;
29
+
30
+ if (formData.client_id === '08ec69f6-d37e-414d-83eb-324e94afddf0') {
31
+ if (formData.grant_type === 'client_credentials') {
32
+ res = fakeTokenStore.createAnonToken();
33
+ } else if (formData.grant_type === 'password') {
34
+ res = fakeTokenStore.createTokenWithCredentials(formData.username, formData.password);
35
+ } else if (formData.grant_type === 'authorization_code') {
36
+ res = fakeTokenStore.createTokenWithAuthorizationCode(formData.code);
37
+ } else if (formData.grant_type === 'refresh_token') {
38
+ res = fakeTokenStore.freshToken(formData.refresh_token);
39
+ } else if (
40
+ formData.grant_type === 'token_exchange' &&
41
+ formData.client_secret === '8af2bf99c380b3a303ab90ae4012c8cd8f69d309'
42
+ ) {
43
+ res = fakeTokenStore.exchangeToken(formData.subject_token);
44
+ }
45
+ }
46
+
47
+ if (res) {
48
+ return resolve({ data: JSON.stringify(res) });
49
+ }
50
+
51
+ return reject({
52
+ status: 401,
53
+ statusText: 'Unauthorized',
54
+ data: 'Unauthorized',
55
+
56
+ // Add additional information to help debugging when testing.
57
+ // This key is NOT returned by the real API.
58
+ __additionalTestInfo: { formData },
59
+ });
60
+ };
61
+
62
+ export const authWithIdp = (config, resolve, reject, fakeTokenStore) => {
63
+ const formData = parseFormData(config.data);
64
+ const { idpId, idpClientId, idpToken } = formData;
65
+ let res;
66
+
67
+ if (formData.client_id === '08ec69f6-d37e-414d-83eb-324e94afddf0') {
68
+ res = fakeTokenStore.createTokenWithIdp(idpId, idpClientId, idpToken);
69
+ }
70
+
71
+ if (res) {
72
+ return resolve({ data: JSON.stringify(res) });
73
+ }
74
+
75
+ return reject({
76
+ status: 401,
77
+ statusText: 'Unauthorized',
78
+ data: 'Unauthorized',
79
+
80
+ // Add additional information to help debugging when testing.
81
+ // This key is NOT returned by the real API.
82
+ __additionalTestInfo: { formData },
83
+ });
84
+ };
@@ -0,0 +1,231 @@
1
+ import _ from 'lodash';
2
+
3
+ const createTokenStore = () => {
4
+ const tokens = [];
5
+ let anonAccessTokenCount = 0;
6
+ let accessTokenCount = 0;
7
+ let refreshTokenCount = 0;
8
+
9
+ const knownUsers = [['joe.dunphy@example.com', 'secret-joe']];
10
+
11
+ const knownAuthorizationCodes = [
12
+ { code: 'flex-authorization-code', username: 'joe.dunphy@example.com' },
13
+ ];
14
+
15
+ const knownIdpTokens = [
16
+ {
17
+ id: 'facebook',
18
+ token: 'idp-token',
19
+ clientId: 'idp-client-id',
20
+ username: 'joe.dunphy@example.com',
21
+ },
22
+ ];
23
+
24
+ // Private
25
+
26
+ const generateAnonAccessToken = () => {
27
+ anonAccessTokenCount += 1;
28
+ return `anonymous-access-${anonAccessTokenCount}`;
29
+ };
30
+
31
+ const generateAccessToken = username => {
32
+ accessTokenCount += 1;
33
+ return `${username}-access-${accessTokenCount}`;
34
+ };
35
+
36
+ const generateRefreshToken = username => {
37
+ refreshTokenCount += 1;
38
+ return `${username}-refresh-${refreshTokenCount}`;
39
+ };
40
+
41
+ // Public
42
+
43
+ const validToken = (accessToken, tokenType) =>
44
+ _.find(
45
+ tokens,
46
+ ({ token }) =>
47
+ token.access_token &&
48
+ accessToken &&
49
+ token.token_type &&
50
+ tokenType &&
51
+ token.access_token.toLowerCase() === accessToken.toLowerCase() &&
52
+ token.token_type.toLowerCase() === tokenType.toLowerCase()
53
+ );
54
+
55
+ const createAnonToken = () => {
56
+ const token = {
57
+ token: {
58
+ access_token: generateAnonAccessToken(),
59
+ token_type: 'bearer',
60
+ expires_in: 86400,
61
+ scope: 'public-read',
62
+ },
63
+ };
64
+ tokens.push(token);
65
+
66
+ return token.token;
67
+ };
68
+
69
+ const createTokenWithCredentials = (username, password) => {
70
+ const user = _.find(knownUsers, u => _.isEqual(u, [username, password]));
71
+
72
+ if (!user) {
73
+ return null;
74
+ }
75
+
76
+ const token = {
77
+ token: {
78
+ access_token: generateAccessToken(username),
79
+ refresh_token: generateRefreshToken(username),
80
+ token_type: 'bearer',
81
+ expires_in: 86400,
82
+ scope: 'user',
83
+ },
84
+ user: {
85
+ username,
86
+ },
87
+ };
88
+ tokens.push(token);
89
+
90
+ return token.token;
91
+ };
92
+
93
+ const createTokenWithAuthorizationCode = authorizationCode => {
94
+ const knownCode = _.find(knownAuthorizationCodes, ({ code }) => code === authorizationCode);
95
+
96
+ if (!knownCode) {
97
+ return null;
98
+ }
99
+
100
+ const { username } = knownCode;
101
+ const token = {
102
+ token: {
103
+ access_token: generateAccessToken(username),
104
+ refresh_token: generateRefreshToken(username),
105
+ token_type: 'bearer',
106
+ expires_in: 86400,
107
+ scope: 'user:limited',
108
+ },
109
+ user: {
110
+ username,
111
+ },
112
+ };
113
+ tokens.push(token);
114
+
115
+ return token.token;
116
+ };
117
+
118
+ const createTokenWithIdp = (idpId, idpClientId, idpToken) => {
119
+ const knownIdpToken = _.find(
120
+ knownIdpTokens,
121
+ ({ id, token, clientId }) => id === idpId && token === idpToken && clientId === idpClientId
122
+ );
123
+
124
+ if (!knownIdpToken) {
125
+ return null;
126
+ }
127
+
128
+ const { username } = knownIdpToken;
129
+ const token = {
130
+ token: {
131
+ access_token: generateAccessToken(username),
132
+ refresh_token: generateRefreshToken(username),
133
+ token_type: 'bearer',
134
+ expires_in: 86400,
135
+ scope: 'user',
136
+ },
137
+ user: {
138
+ username,
139
+ },
140
+ };
141
+ tokens.push(token);
142
+
143
+ return token.token;
144
+ };
145
+
146
+ const exchangeToken = accessToken => {
147
+ const currentToken = _.find(
148
+ tokens,
149
+ ({ token }) =>
150
+ token.access_token &&
151
+ accessToken &&
152
+ token.access_token.toLowerCase() === accessToken.toLowerCase()
153
+ );
154
+
155
+ if (!currentToken) {
156
+ return null;
157
+ }
158
+
159
+ const { username } = currentToken.user;
160
+
161
+ const trustedToken = {
162
+ token: {
163
+ access_token: generateAccessToken(username),
164
+ refresh_token: generateRefreshToken(username),
165
+ token_type: 'bearer',
166
+ expires_in: 86400,
167
+ scope: 'trusted:user',
168
+ },
169
+ user: {
170
+ username,
171
+ },
172
+ };
173
+ tokens.push(trustedToken);
174
+
175
+ return trustedToken.token;
176
+ };
177
+
178
+ const expireAccessToken = accessToken => {
179
+ _.map(tokens, t => {
180
+ const { token } = t;
181
+
182
+ if (token.access_token === accessToken) {
183
+ token.access_token = null;
184
+ }
185
+
186
+ return t;
187
+ });
188
+ };
189
+
190
+ const revokeRefreshToken = refreshToken =>
191
+ _.remove(tokens, t => t.token.refresh_token === refreshToken);
192
+
193
+ const freshToken = refreshToken => {
194
+ const existingToken = revokeRefreshToken(refreshToken);
195
+
196
+ if (existingToken.length) {
197
+ const { username } = existingToken[0].user;
198
+
199
+ const token = {
200
+ token: {
201
+ access_token: generateAccessToken(username),
202
+ refresh_token: generateRefreshToken(username),
203
+ token_type: 'bearer',
204
+ expires_in: 86400,
205
+ },
206
+ user: {
207
+ username,
208
+ },
209
+ };
210
+ tokens.push(token);
211
+
212
+ return token.token;
213
+ }
214
+
215
+ return null;
216
+ };
217
+
218
+ return {
219
+ createAnonToken,
220
+ createTokenWithCredentials,
221
+ createTokenWithAuthorizationCode,
222
+ createTokenWithIdp,
223
+ exchangeToken,
224
+ freshToken,
225
+ revokeRefreshToken,
226
+ validToken,
227
+ expireAccessToken,
228
+ };
229
+ };
230
+
231
+ export default createTokenStore;
package/src/index.js ADDED
@@ -0,0 +1,25 @@
1
+ import SharetribeSdk from './sdk';
2
+ import * as types from './types';
3
+ import browserCookieStore from './browser_cookie_store';
4
+ import expressCookieStore from './express_cookie_store';
5
+ import memoryStore from './memory_store';
6
+ import { read, write } from './serializer';
7
+ import { objectQueryString } from './utils';
8
+
9
+ const createInstance = config => new SharetribeSdk(config);
10
+
11
+ // Export token stores
12
+ const tokenStore = {
13
+ memoryStore,
14
+ browserCookieStore,
15
+ expressCookieStore,
16
+ };
17
+
18
+ // Export Transit serialization helpers
19
+ const transit = { read, write };
20
+
21
+ // Export util functions
22
+ const util = { objectQueryString };
23
+
24
+ /* eslint-disable import/prefer-default-export */
25
+ export { createInstance, types, tokenStore, transit, util };
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ rules: {
3
+ 'class-methods-use-this': 'off',
4
+ },
5
+ };
@@ -0,0 +1,32 @@
1
+ const constructAuthHeader = authToken => {
2
+ /* eslint-disable camelcase */
3
+ const token_type = authToken.token_type && authToken.token_type.toLowerCase();
4
+
5
+ switch (token_type) {
6
+ case 'bearer':
7
+ return `Bearer ${authToken.access_token}`;
8
+ default:
9
+ throw new Error(`Unknown token type: ${token_type}`);
10
+ }
11
+ /* eslint-enable camelcase */
12
+ };
13
+
14
+ /**
15
+ Read `authToken` from `ctx`. Then construct Authorize header and add it to `headers`.
16
+
17
+ Changes to `ctx`:
18
+
19
+ - Add `headers.Authorize`
20
+ */
21
+ export default class AddAuthHeader {
22
+ enter(ctx) {
23
+ const { authToken, headers = {} } = ctx;
24
+
25
+ if (!authToken) {
26
+ return ctx;
27
+ }
28
+
29
+ const authHeaders = { Authorization: constructAuthHeader(authToken) };
30
+ return { ...ctx, headers: { ...headers, ...authHeaders } };
31
+ }
32
+ }