@openstax/ts-utils 1.9.1 → 1.11.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.
- package/dist/cjs/config/envConfig.d.ts +2 -2
- package/dist/cjs/config/envConfig.js +13 -6
- package/dist/cjs/config/resolveConfigValue.d.ts +1 -1
- package/dist/cjs/errors/index.js +4 -4
- package/dist/cjs/services/authProvider/browser.js +27 -9
- package/dist/cjs/services/authProvider/decryption.js +6 -1
- package/dist/cjs/services/authProvider/utils/decryptAndVerify.d.ts +20 -4
- package/dist/cjs/services/authProvider/utils/decryptAndVerify.js +74 -62
- package/dist/cjs/services/authProvider/utils/embeddedAuthProvider.d.ts +6 -2
- package/dist/cjs/services/authProvider/utils/embeddedAuthProvider.js +10 -3
- package/dist/cjs/services/exercisesGateway/index.js +2 -1
- package/dist/cjs/services/fileServer/index.d.ts +17 -0
- package/dist/cjs/services/fileServer/index.js +19 -0
- package/dist/cjs/services/fileServer/localFileServer.d.ts +13 -0
- package/dist/cjs/services/fileServer/localFileServer.js +23 -0
- package/dist/cjs/services/fileServer/s3FileServer.d.ts +16 -0
- package/dist/cjs/services/fileServer/s3FileServer.js +25 -0
- package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
- package/dist/esm/config/envConfig.d.ts +2 -2
- package/dist/esm/config/envConfig.js +12 -5
- package/dist/esm/config/resolveConfigValue.d.ts +1 -1
- package/dist/esm/errors/index.js +2 -2
- package/dist/esm/services/authProvider/browser.js +27 -9
- package/dist/esm/services/authProvider/decryption.js +6 -1
- package/dist/esm/services/authProvider/utils/decryptAndVerify.d.ts +20 -4
- package/dist/esm/services/authProvider/utils/decryptAndVerify.js +72 -36
- package/dist/esm/services/authProvider/utils/embeddedAuthProvider.d.ts +6 -2
- package/dist/esm/services/authProvider/utils/embeddedAuthProvider.js +10 -3
- package/dist/esm/services/exercisesGateway/index.js +2 -1
- package/dist/esm/services/fileServer/index.d.ts +17 -0
- package/dist/esm/services/fileServer/index.js +13 -0
- package/dist/esm/services/fileServer/localFileServer.d.ts +13 -0
- package/dist/esm/services/fileServer/localFileServer.js +16 -0
- package/dist/esm/services/fileServer/s3FileServer.d.ts +16 -0
- package/dist/esm/services/fileServer/s3FileServer.js +21 -0
- package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
- package/package.json +19 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigValueProvider } from '.';
|
|
1
|
+
import type { ConfigValueProvider } from '.';
|
|
2
2
|
/**
|
|
3
3
|
* A list of environment variables that were requested at build time. Used by webpack to
|
|
4
4
|
* capture build-time environment variables values.
|
|
@@ -21,4 +21,4 @@ export declare const ENV_BUILD_CONFIGS: string[];
|
|
|
21
21
|
*
|
|
22
22
|
* @example const config = { configValue: envConfig('environment_variable_name') };
|
|
23
23
|
*/
|
|
24
|
-
export declare const envConfig: (name: string, type?:
|
|
24
|
+
export declare const envConfig: (name: string, type?: "build" | "runtime" | undefined, defaultValue?: ConfigValueProvider<string> | undefined) => ConfigValueProvider<string>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveConfigValue } from '
|
|
1
|
+
import { resolveConfigValue } from './resolveConfigValue';
|
|
2
2
|
/**
|
|
3
3
|
* A list of environment variables that were requested at build time. Used by webpack to
|
|
4
4
|
* capture build-time environment variables values.
|
|
@@ -21,7 +21,12 @@ export const ENV_BUILD_CONFIGS = [];
|
|
|
21
21
|
*
|
|
22
22
|
* @example const config = { configValue: envConfig('environment_variable_name') };
|
|
23
23
|
*/
|
|
24
|
-
export const envConfig = (name, type
|
|
24
|
+
export const envConfig = (name, type, defaultValue) => {
|
|
25
|
+
// this doesn't use a default parameter value because of a:
|
|
26
|
+
// "Regular parameters should not come after default parameters."
|
|
27
|
+
// error that occurs when the defaultValue optional default of `undefined`
|
|
28
|
+
// gets optimized out, causing a problem in cloudfront functions.
|
|
29
|
+
type !== null && type !== void 0 ? type : (type = 'build');
|
|
25
30
|
if (type === 'build') {
|
|
26
31
|
ENV_BUILD_CONFIGS.push(name);
|
|
27
32
|
}
|
|
@@ -30,8 +35,10 @@ export const envConfig = (name, type = 'build', defaultValue) => {
|
|
|
30
35
|
// @ts-ignore - hack to get around the way webpack/define works
|
|
31
36
|
// - https://github.com/webpack/webpack/issues/14800
|
|
32
37
|
// - https://github.com/webpack/webpack/issues/5392
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
// also, spread operator not supported in cloudfront functions
|
|
39
|
+
const envs = Object.assign({}, process.env, typeof __PROCESS_ENV !== 'undefined' ? __PROCESS_ENV : {});
|
|
40
|
+
const value = envs[name];
|
|
41
|
+
if (value === undefined) {
|
|
35
42
|
if (defaultValue === undefined) {
|
|
36
43
|
throw new Error(`expected to find environment variable with name: ${name}`);
|
|
37
44
|
}
|
|
@@ -40,7 +47,7 @@ export const envConfig = (name, type = 'build', defaultValue) => {
|
|
|
40
47
|
}
|
|
41
48
|
}
|
|
42
49
|
else {
|
|
43
|
-
return
|
|
50
|
+
return value;
|
|
44
51
|
}
|
|
45
52
|
};
|
|
46
53
|
};
|
package/dist/esm/errors/index.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* error instanceof InvalidRequestError
|
|
11
11
|
*
|
|
12
12
|
*/
|
|
13
|
-
const errorIsType = (
|
|
14
|
-
&& e.constructor.TYPE === TYPE;
|
|
13
|
+
const errorIsType = (t) => (e) => e instanceof Error
|
|
14
|
+
&& e.constructor.TYPE === t.TYPE;
|
|
15
15
|
/**
|
|
16
16
|
* Returns true if the error is defined in this library
|
|
17
17
|
*/
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { isEqual } from 'lodash';
|
|
1
2
|
import { once } from '../..';
|
|
2
3
|
import { resolveConfigValue } from '../../config';
|
|
4
|
+
import { UnauthorizedError } from '../../errors';
|
|
3
5
|
import { ifDefined } from '../../guards';
|
|
4
6
|
import { unsafePayloadValidator } from '../../routing/helpers';
|
|
5
7
|
import { embeddedAuthProvider, PostMessageTypes } from './utils/embeddedAuthProvider';
|
|
@@ -9,14 +11,14 @@ export const browserAuthProvider = ({ window, configSpace }) => (configProvider)
|
|
|
9
11
|
const accountsBase = once(() => resolveConfigValue(config.accountsBase));
|
|
10
12
|
const queryString = window.location.search;
|
|
11
13
|
const queryKey = 'auth';
|
|
12
|
-
const
|
|
14
|
+
const urlSearchParams = new URLSearchParams(queryString);
|
|
15
|
+
const authQuery = urlSearchParams.get(queryKey);
|
|
13
16
|
const referrer = window.document.referrer ? new URL(window.document.referrer) : undefined;
|
|
14
17
|
const isEmbedded = window.parent !== window;
|
|
15
18
|
const trustedParent = isEmbedded && referrer && referrer.hostname.match(/^(openstax\.org|((.*)(\.openstax\.org|local|localhost)))$/) ? referrer : undefined;
|
|
16
|
-
const { embeddedQueryValue, getAuthorizedEmbedUrl } = embeddedAuthProvider(() => getUserData(), { queryKey, window });
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
19
|
+
const { embeddedQueryKey, embeddedQueryValue, getAuthorizedEmbedUrl } = embeddedAuthProvider(() => getUserData(), { authQuery: { key: queryKey, value: authQuery }, window });
|
|
20
|
+
const embeddedQuery = urlSearchParams.get(embeddedQueryKey);
|
|
21
|
+
let userData = { token: authQuery };
|
|
20
22
|
const getAuthToken = async () => {
|
|
21
23
|
return (await getUserData()).token;
|
|
22
24
|
};
|
|
@@ -32,6 +34,9 @@ export const browserAuthProvider = ({ window, configSpace }) => (configProvider)
|
|
|
32
34
|
if (authQuery) {
|
|
33
35
|
url.searchParams.set(queryKey, authQuery);
|
|
34
36
|
}
|
|
37
|
+
if (embeddedQuery) {
|
|
38
|
+
url.searchParams.set(embeddedQueryKey, embeddedQuery);
|
|
39
|
+
}
|
|
35
40
|
return url.href;
|
|
36
41
|
};
|
|
37
42
|
// *note* that this does not actually prevent cookies from being sent on same-origin
|
|
@@ -83,10 +88,23 @@ export const browserAuthProvider = ({ window, configSpace }) => (configProvider)
|
|
|
83
88
|
throw new Error(`Error response from Accounts ${response.status}: ${message}`);
|
|
84
89
|
};
|
|
85
90
|
const getUserData = once(async () => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
// For backwards compatibility
|
|
92
|
+
if (authQuery === 'embedded') {
|
|
93
|
+
return getParentWindowUser();
|
|
94
|
+
}
|
|
95
|
+
// getFetchUser() will throw here if authQuery is not set
|
|
96
|
+
if (embeddedQuery !== embeddedQueryValue) {
|
|
97
|
+
return getFetchUser();
|
|
98
|
+
}
|
|
99
|
+
const userDataFromParentWindow = await getParentWindowUser();
|
|
100
|
+
if (!authQuery) {
|
|
101
|
+
return userDataFromParentWindow;
|
|
102
|
+
}
|
|
103
|
+
const userDataFromAuthQuery = await getFetchUser();
|
|
104
|
+
if (isEqual(userDataFromAuthQuery, userDataFromParentWindow)) {
|
|
105
|
+
return userDataFromAuthQuery;
|
|
106
|
+
}
|
|
107
|
+
throw new UnauthorizedError();
|
|
90
108
|
});
|
|
91
109
|
const getUser = async () => {
|
|
92
110
|
return (await getUserData()).user;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveConfigValue } from '../../config/resolveConfigValue';
|
|
2
|
+
import { SessionExpiredError } from '../../errors';
|
|
2
3
|
import { ifDefined } from '../../guards';
|
|
3
4
|
import { once } from '../../misc/helpers';
|
|
4
5
|
import { decryptAndVerify } from './utils/decryptAndVerify';
|
|
@@ -22,7 +23,11 @@ export const decryptionAuthProvider = (initializer) => (configProvider) => {
|
|
|
22
23
|
if (!token) {
|
|
23
24
|
return undefined;
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
const result = decryptAndVerify(token, await encryptionPrivateKey(), await signaturePublicKey());
|
|
27
|
+
if ('error' in result && result.error == 'expired token') {
|
|
28
|
+
throw new SessionExpiredError();
|
|
29
|
+
}
|
|
30
|
+
return 'user' in result ? result.user : undefined;
|
|
26
31
|
};
|
|
27
32
|
return {
|
|
28
33
|
getAuthorizedFetchConfig,
|
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import type { User } from '..';
|
|
3
|
+
export declare const decryptJwe: (jwe: string, encryptionPrivateKey: Buffer | string) => string | undefined;
|
|
4
|
+
declare type MaybeAccountsSSOToken = {
|
|
5
|
+
iss?: string;
|
|
6
|
+
sub?: User | string;
|
|
7
|
+
aud?: string;
|
|
8
|
+
exp?: number;
|
|
9
|
+
nbf?: number;
|
|
10
|
+
iat?: number;
|
|
11
|
+
jti?: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const verifyJws: (jws: string, signaturePublicKey: Buffer | string) => MaybeAccountsSSOToken | undefined;
|
|
2
14
|
/**
|
|
3
15
|
* Decrypts and verifies a SSO cookie.
|
|
4
16
|
*
|
|
5
17
|
* @param token the encrypted token
|
|
6
18
|
* @param encryptionPrivateKey the private key used to encrypt the token
|
|
7
19
|
* @param signaturePublicKey the public key used to verify the decrypted token
|
|
8
|
-
* @
|
|
9
|
-
* @returns User (success) or undefined (failure)
|
|
20
|
+
* @returns {user: User} (success) or {error: string} (failure)
|
|
10
21
|
*/
|
|
11
|
-
export declare const decryptAndVerify: (token: string, encryptionPrivateKey: string, signaturePublicKey: string) =>
|
|
22
|
+
export declare const decryptAndVerify: (token: string, encryptionPrivateKey: string, signaturePublicKey: string) => {
|
|
23
|
+
user: User;
|
|
24
|
+
} | {
|
|
25
|
+
error: string;
|
|
26
|
+
};
|
|
27
|
+
export {};
|
|
@@ -1,22 +1,55 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { TextEncoder } from 'util';
|
|
3
|
-
import jwt from 'jsonwebtoken';
|
|
4
|
-
import { SessionExpiredError } from '../../../errors';
|
|
1
|
+
import { createDecipheriv, verify } from 'crypto';
|
|
5
2
|
import { isPlainObject } from '../../../guards';
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
3
|
+
export const decryptJwe = (jwe, encryptionPrivateKey) => {
|
|
4
|
+
const jweParts = jwe.split('.', 6);
|
|
5
|
+
if (jweParts.length !== 5 || jweParts[1]) {
|
|
6
|
+
return undefined;
|
|
7
|
+
} // Invalid/unsupported JWE
|
|
8
|
+
const header = JSON.parse(Buffer.from(jweParts[0], 'base64url').toString());
|
|
9
|
+
if (header.alg !== 'dir' || header.enc !== 'A256GCM') {
|
|
10
|
+
// Unsupported signature/encryption algorithm
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const aad = Buffer.from(jweParts[0]);
|
|
14
|
+
const iv = Buffer.from(jweParts[2], 'base64url');
|
|
15
|
+
const cipherText = Buffer.from(jweParts[3], 'base64url');
|
|
16
|
+
const authTag = Buffer.from(jweParts[4], 'base64url');
|
|
17
|
+
// Verify token signature and decrypt
|
|
18
|
+
const decipher = createDecipheriv('aes-256-gcm', encryptionPrivateKey, iv, { authTagLength: 16 });
|
|
13
19
|
decipher.setAAD(aad, { plaintextLength: cipherText.length });
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
decipher.update(cipherText)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
try {
|
|
21
|
+
decipher.setAuthTag(authTag);
|
|
22
|
+
return `${decipher.update(cipherText)}${decipher.final()}`;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
// Invalid cipherText or authTag
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const issuer = 'OpenStax Accounts';
|
|
30
|
+
const audience = 'OpenStax';
|
|
31
|
+
const clockTolerance = 300; // 5 minutes
|
|
32
|
+
export const verifyJws = (jws, signaturePublicKey) => {
|
|
33
|
+
const jwsParts = jws.split('.', 4);
|
|
34
|
+
if (jwsParts.length !== 3) {
|
|
35
|
+
return undefined;
|
|
36
|
+
} // Invalid JWS
|
|
37
|
+
const header = JSON.parse(Buffer.from(jwsParts[0], 'base64url').toString());
|
|
38
|
+
if (header.alg !== 'RS256' || header.typ !== 'JWT') {
|
|
39
|
+
return undefined;
|
|
40
|
+
} // Unsupported JWS
|
|
41
|
+
const signedContent = Buffer.from(`${jwsParts[0]}.${jwsParts[1]}`);
|
|
42
|
+
const signature = Buffer.from(jwsParts[2], 'base64url');
|
|
43
|
+
if (!verify('RSA-SHA256', signedContent, signaturePublicKey, signature)) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const payload = Buffer.from(jwsParts[1], 'base64url').toString();
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(payload);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
20
53
|
};
|
|
21
54
|
/**
|
|
22
55
|
* Decrypts and verifies a SSO cookie.
|
|
@@ -24,26 +57,29 @@ const decrypt = (input, key) => {
|
|
|
24
57
|
* @param token the encrypted token
|
|
25
58
|
* @param encryptionPrivateKey the private key used to encrypt the token
|
|
26
59
|
* @param signaturePublicKey the public key used to verify the decrypted token
|
|
27
|
-
* @
|
|
28
|
-
* @returns User (success) or undefined (failure)
|
|
60
|
+
* @returns {user: User} (success) or {error: string} (failure)
|
|
29
61
|
*/
|
|
30
62
|
export const decryptAndVerify = (token, encryptionPrivateKey, signaturePublicKey) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return
|
|
63
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
64
|
+
const jws = decryptJwe(token, encryptionPrivateKey);
|
|
65
|
+
if (!jws) {
|
|
66
|
+
return { error: 'invalid token' };
|
|
67
|
+
}
|
|
68
|
+
const payload = verifyJws(jws, signaturePublicKey);
|
|
69
|
+
// Ensure payload contains all the claims we expect
|
|
70
|
+
// Normally "sub" would be a string but Accounts uses an object for it instead
|
|
71
|
+
if (!isPlainObject(payload) ||
|
|
72
|
+
!isPlainObject(payload.sub) || !payload.sub.uuid ||
|
|
73
|
+
payload.iss !== issuer ||
|
|
74
|
+
payload.aud !== audience ||
|
|
75
|
+
!payload.exp ||
|
|
76
|
+
!payload.nbf || payload.nbf > timestamp + clockTolerance ||
|
|
77
|
+
!payload.iat || payload.iat > timestamp + clockTolerance ||
|
|
78
|
+
!payload.jti) {
|
|
79
|
+
return { error: 'invalid token' };
|
|
80
|
+
}
|
|
81
|
+
if (payload.exp < timestamp - clockTolerance) {
|
|
82
|
+
return { error: 'expired token' };
|
|
48
83
|
}
|
|
84
|
+
return { user: payload.sub };
|
|
49
85
|
};
|
|
@@ -9,10 +9,14 @@ export declare enum PostMessageTypes {
|
|
|
9
9
|
ReceiveUser = "receive-user",
|
|
10
10
|
RequestUser = "request-user"
|
|
11
11
|
}
|
|
12
|
-
export declare const embeddedAuthProvider: (getUserData: UserDataLoader, {
|
|
13
|
-
|
|
12
|
+
export declare const embeddedAuthProvider: (getUserData: UserDataLoader, { authQuery, window }: {
|
|
13
|
+
authQuery?: {
|
|
14
|
+
key: string;
|
|
15
|
+
value: string | null;
|
|
16
|
+
} | undefined;
|
|
14
17
|
window: Window;
|
|
15
18
|
}) => {
|
|
19
|
+
embeddedQueryKey: string;
|
|
16
20
|
embeddedQueryValue: string;
|
|
17
21
|
getAuthorizedEmbedUrl: (urlString: string, extraParams?: {
|
|
18
22
|
[key: string]: string;
|
|
@@ -4,9 +4,10 @@ export var PostMessageTypes;
|
|
|
4
4
|
PostMessageTypes["ReceiveUser"] = "receive-user";
|
|
5
5
|
PostMessageTypes["RequestUser"] = "request-user";
|
|
6
6
|
})(PostMessageTypes || (PostMessageTypes = {}));
|
|
7
|
-
export const embeddedAuthProvider = (getUserData, {
|
|
7
|
+
export const embeddedAuthProvider = (getUserData, { authQuery, window }) => {
|
|
8
8
|
const trustedEmbeds = new Set();
|
|
9
|
-
const
|
|
9
|
+
const embeddedQueryKey = 'embedded';
|
|
10
|
+
const embeddedQueryValue = 'true';
|
|
10
11
|
const messageHandler = event => {
|
|
11
12
|
if (event.data.type === PostMessageTypes.RequestUser && trustedEmbeds.has(event.origin)) {
|
|
12
13
|
getUserData().then(data => {
|
|
@@ -19,10 +20,16 @@ export const embeddedAuthProvider = (getUserData, { queryKey = 'auth', window })
|
|
|
19
20
|
const url = new URL(urlString);
|
|
20
21
|
trustedEmbeds.add(url.origin);
|
|
21
22
|
const params = queryString.parse(url.search);
|
|
22
|
-
url.search = queryString.stringify({
|
|
23
|
+
url.search = queryString.stringify({
|
|
24
|
+
...params,
|
|
25
|
+
...extraParams,
|
|
26
|
+
...(authQuery && authQuery.value ? { [authQuery.key]: authQuery.value } : {}),
|
|
27
|
+
[embeddedQueryKey]: embeddedQueryValue
|
|
28
|
+
});
|
|
23
29
|
return url.href;
|
|
24
30
|
};
|
|
25
31
|
return {
|
|
32
|
+
embeddedQueryKey,
|
|
26
33
|
embeddedQueryValue,
|
|
27
34
|
getAuthorizedEmbedUrl,
|
|
28
35
|
unmount: () => {
|
|
@@ -25,7 +25,8 @@ export const exercisesGateway = (initializer) => (configProvider) => {
|
|
|
25
25
|
question.collaborator_solutions = [
|
|
26
26
|
{ solution_type: 'detailed', images: [], content_html: defaultHint }
|
|
27
27
|
];
|
|
28
|
-
for (
|
|
28
|
+
for (let index = 0; index < question.answers.length; index++) {
|
|
29
|
+
const answer = question.answers[index];
|
|
29
30
|
answer.correctness = defaultCorrectIndex === index ? '1.0' : '0.0';
|
|
30
31
|
answer.feedback_html = defaultCorrectIndex === index ? 'This is the good one!' : defaultHint;
|
|
31
32
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
export declare type FileValue = {
|
|
3
|
+
dataType: 'file';
|
|
4
|
+
mimeType: string;
|
|
5
|
+
path: string;
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
export declare type FolderValue = {
|
|
9
|
+
dataType: 'folder';
|
|
10
|
+
files: FileValue[];
|
|
11
|
+
};
|
|
12
|
+
export declare const isFileValue: (thing: any) => thing is FileValue;
|
|
13
|
+
export declare const isFolderValue: (thing: any) => thing is FolderValue;
|
|
14
|
+
export interface FileServerAdapter {
|
|
15
|
+
getFileContent: (source: FileValue) => Promise<Buffer>;
|
|
16
|
+
}
|
|
17
|
+
export declare const isFileOrFolder: (thing: any) => thing is FileValue | FolderValue;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isPlainObject } from '../../guards';
|
|
2
|
+
export const isFileValue = (thing) => isPlainObject(thing)
|
|
3
|
+
&& Object.keys(thing).every(key => ['dataType', 'path', 'label', 'mimeType'].includes(key))
|
|
4
|
+
&& thing.dataType === 'file'
|
|
5
|
+
&& typeof thing.mimeType === 'string'
|
|
6
|
+
&& typeof thing.path === 'string'
|
|
7
|
+
&& typeof thing.label === 'string';
|
|
8
|
+
export const isFolderValue = (thing) => isPlainObject(thing)
|
|
9
|
+
&& Object.keys(thing).every(key => ['dataType', 'files'].includes(key))
|
|
10
|
+
&& thing.dataType === 'folder'
|
|
11
|
+
&& thing.files instanceof Array
|
|
12
|
+
&& thing.files.every(isFileValue);
|
|
13
|
+
export const isFileOrFolder = (thing) => isFileValue(thing) || isFolderValue(thing);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ConfigProviderForConfig } from '../../config';
|
|
2
|
+
import { FileServerAdapter } from '.';
|
|
3
|
+
export declare type Config = {
|
|
4
|
+
storagePrefix: string;
|
|
5
|
+
};
|
|
6
|
+
interface Initializer<C> {
|
|
7
|
+
dataDir: string;
|
|
8
|
+
configSpace?: C;
|
|
9
|
+
}
|
|
10
|
+
export declare const localFileServer: <C extends string = "local">(initializer: Initializer<C>) => (configProvider: { [key in C]: {
|
|
11
|
+
storagePrefix: import("../../config").ConfigValueProvider<string>;
|
|
12
|
+
}; }) => FileServerAdapter;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { resolveConfigValue } from '../../config';
|
|
4
|
+
import { ifDefined } from '../../guards';
|
|
5
|
+
export const localFileServer = (initializer) => (configProvider) => {
|
|
6
|
+
const config = configProvider[ifDefined(initializer.configSpace, 'local')];
|
|
7
|
+
const storagePrefix = resolveConfigValue(config.storagePrefix);
|
|
8
|
+
const fileDir = storagePrefix.then((prefix) => path.join(initializer.dataDir, prefix));
|
|
9
|
+
const getFileContent = async (source) => {
|
|
10
|
+
const filePath = path.join(await fileDir, source.path);
|
|
11
|
+
return fs.promises.readFile(filePath);
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
getFileContent,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
import { ConfigProviderForConfig } from '../../config';
|
|
3
|
+
import { FileServerAdapter } from '.';
|
|
4
|
+
export declare type Config = {
|
|
5
|
+
bucketName: string;
|
|
6
|
+
bucketRegion: string;
|
|
7
|
+
};
|
|
8
|
+
interface Initializer<C> {
|
|
9
|
+
configSpace?: C;
|
|
10
|
+
s3Client?: typeof S3Client;
|
|
11
|
+
}
|
|
12
|
+
export declare const s3FileServer: <C extends string = "deployed">(initializer: Initializer<C>) => (configProvider: { [key in C]: {
|
|
13
|
+
bucketName: import("../../config").ConfigValueProvider<string>;
|
|
14
|
+
bucketRegion: import("../../config").ConfigValueProvider<string>;
|
|
15
|
+
}; }) => FileServerAdapter;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
import { once } from '../..';
|
|
3
|
+
import { assertDefined } from '../../assertions';
|
|
4
|
+
import { resolveConfigValue } from '../../config';
|
|
5
|
+
import { ifDefined } from '../../guards';
|
|
6
|
+
export const s3FileServer = (initializer) => (configProvider) => {
|
|
7
|
+
const config = configProvider[ifDefined(initializer.configSpace, 'deployed')];
|
|
8
|
+
const bucketName = once(() => resolveConfigValue(config.bucketName));
|
|
9
|
+
const bucketRegion = once(() => resolveConfigValue(config.bucketRegion));
|
|
10
|
+
const client = ifDefined(initializer.s3Client, S3Client);
|
|
11
|
+
const s3Service = once(async () => new client({ apiVersion: '2012-08-10', region: await bucketRegion() }));
|
|
12
|
+
const getFileContent = async (source) => {
|
|
13
|
+
const bucket = await bucketName();
|
|
14
|
+
const command = new GetObjectCommand({ Bucket: bucket, Key: source.path });
|
|
15
|
+
const response = await (await s3Service()).send(command);
|
|
16
|
+
return Buffer.from(await assertDefined(response.Body, new Error('Invalid Response from s3')).transformToByteArray());
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
getFileContent,
|
|
20
|
+
};
|
|
21
|
+
};
|