dce-reactkit 3.11.4 → 3.12.1-beta-cross-server.2

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 (41) hide show
  1. package/.vscode/settings.json +1 -3
  2. package/README.md +20 -0
  3. package/dist/cjs/index.js +294 -162
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/types/helpers/dataSigner.d.ts +38 -0
  6. package/dist/cjs/types/helpers/genRouteHandler.d.ts +10 -10
  7. package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/index.d.ts +15 -13
  8. package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +0 -6
  9. package/dist/cjs/types/index.d.ts +3 -2
  10. package/dist/cjs/types/server/initCrossServerCredentialCollection.d.ts +8 -0
  11. package/dist/cjs/types/server/initServer.d.ts +11 -0
  12. package/dist/cjs/types/types/CrossServerCredential.d.ts +11 -0
  13. package/dist/cjs/types/types/ReactKitErrorCode.d.ts +13 -1
  14. package/dist/esm/index.js +293 -162
  15. package/dist/esm/index.js.map +1 -1
  16. package/dist/esm/types/helpers/dataSigner.d.ts +38 -0
  17. package/dist/esm/types/helpers/genRouteHandler.d.ts +10 -10
  18. package/dist/esm/types/helpers/visitEndpointOnAnotherServer/index.d.ts +15 -13
  19. package/dist/esm/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +0 -6
  20. package/dist/esm/types/index.d.ts +3 -2
  21. package/dist/esm/types/server/initCrossServerCredentialCollection.d.ts +8 -0
  22. package/dist/esm/types/server/initServer.d.ts +11 -0
  23. package/dist/esm/types/types/CrossServerCredential.d.ts +11 -0
  24. package/dist/esm/types/types/ReactKitErrorCode.d.ts +13 -1
  25. package/dist/index.d.ts +62 -35
  26. package/genEncodedSecret.ts +28 -0
  27. package/package.json +5 -2
  28. package/src/components/IntelliTable.tsx +1 -1
  29. package/src/components/ItemPicker/NestableItemList.tsx +0 -1
  30. package/src/helpers/dataSigner.ts +232 -0
  31. package/src/helpers/genRouteHandler.ts +55 -95
  32. package/src/helpers/visitEndpointOnAnotherServer/index.ts +42 -41
  33. package/src/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.ts +3 -5
  34. package/src/index.ts +4 -2
  35. package/src/server/initCrossServerCredentialCollection.ts +16 -0
  36. package/src/server/initServer.ts +18 -0
  37. package/src/types/CrossServerCredential.ts +16 -0
  38. package/src/types/ReactKitErrorCode.tsx +13 -1
  39. package/dist/cjs/types/helpers/getWordCount.d.ts +0 -10
  40. package/dist/esm/types/helpers/getWordCount.d.ts +0 -10
  41. package/src/helpers/getWordCount.ts +0 -26
package/dist/esm/index.js CHANGED
@@ -4,6 +4,8 @@ import { faExclamationTriangle, faHourglassEnd, faCircle, faDotCircle, faCheckSq
4
4
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5
5
  import ReactDOM, { createPortal } from 'react-dom';
6
6
  import { faCircle as faCircle$1, faSquareMinus, faSquare } from '@fortawesome/free-regular-svg-icons';
7
+ import { createHash } from 'crypto';
8
+ import jwt from 'jwt-simple';
7
9
  import qs from 'qs';
8
10
 
9
11
  /******************************************************************************
@@ -31,7 +33,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
31
33
  });
32
34
  }
33
35
 
34
- // Highest error code = DRK18
36
+ // Highest error code = DRK30
35
37
  /**
36
38
  * List of error codes built into the react kit
37
39
  * @author Gabe Abrams
@@ -57,6 +59,18 @@ var ReactKitErrorCode;
57
59
  ReactKitErrorCode["NotConnected"] = "DRK14";
58
60
  ReactKitErrorCode["SelfSigned"] = "DRK15";
59
61
  ReactKitErrorCode["ResponseParseError"] = "DRK16";
62
+ ReactKitErrorCode["PackUnparseable"] = "DRK28";
63
+ ReactKitErrorCode["PackInvalidMethod"] = "DRK19";
64
+ ReactKitErrorCode["PackInvalidPath"] = "DRK20";
65
+ ReactKitErrorCode["PackInvalidCollection"] = "DRK21";
66
+ ReactKitErrorCode["PackInvalidCredential"] = "DRK23";
67
+ ReactKitErrorCode["PackInvalidScope"] = "DRK22";
68
+ ReactKitErrorCode["PackInvalidTimestamp"] = "DRK24";
69
+ ReactKitErrorCode["PackInvalidSignature"] = "DRK25";
70
+ ReactKitErrorCode["PackInvalidBody"] = "DRK26";
71
+ ReactKitErrorCode["CrossServerNoCredentialsToSignWith"] = "DRK27";
72
+ ReactKitErrorCode["CrossServerNoPack"] = "DRK29";
73
+ ReactKitErrorCode["CrossServerNoCredentialEncodingSalt"] = "DRK30";
60
74
  })(ReactKitErrorCode || (ReactKitErrorCode = {}));
61
75
  var ReactKitErrorCode$1 = ReactKitErrorCode;
62
76
 
@@ -2644,7 +2658,7 @@ const NestableItemList = (props) => {
2644
2658
  React__default.createElement(FontAwesomeIcon, { icon: childExpanded[item.id] ? faChevronDown : faChevronRight })))),
2645
2659
  React__default.createElement(CheckboxButton, { className: `NestableItemList-CheckboxButton-${item.id}`, text: item.name, checked: item.isGroup ? allChecked(item.children) : item.checked, dashed: item.isGroup ? !noneChecked(item.children) : false, onChanged: (checked) => {
2646
2660
  onChanged(changeChecked(item.id, checked, items));
2647
- }, ariaLabel: `Select ${item.name}`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Light }),
2661
+ }, ariaLabel: `Select ${item.name}`, checkedVariant: Variant$1.Light }),
2648
2662
  (item.isGroup && childExpanded[item.id]) && (React__default.createElement("div", { className: "NestableItemList-children-container", style: {
2649
2663
  paddingLeft: '2.2rem',
2650
2664
  } },
@@ -3049,7 +3063,7 @@ const IntelliTable = (props) => {
3049
3063
  param: column.param,
3050
3064
  visible: checked,
3051
3065
  });
3052
- }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column in the ${title} table`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Light }));
3066
+ }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column in the ${title} table`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Secondary }));
3053
3067
  }),
3054
3068
  React__default.createElement("div", { className: "mt-3" }, "Or you can:"),
3055
3069
  React__default.createElement("button", { type: "button", id: `IntelliTable-${id}-select-all-columns`, className: "btn btn-secondary me-2", "aria-label": `show all columns in the ${title} table`, onClick: () => {
@@ -14021,6 +14035,8 @@ const LOG_REVIEW_STATUS_ROUTE = `${ROUTE_PATH_PREFIX}/logs/access_allowed`;
14021
14035
  let _cacclGetLaunchInfo;
14022
14036
  // Stored copy of dce-mango log collection
14023
14037
  let _logCollection;
14038
+ // Stored copy of dce-mango cross-server credential collection
14039
+ let _crossServerCredentialCollection;
14024
14040
  /*------------------------------------------------------------------------*/
14025
14041
  /* Helpers */
14026
14042
  /*------------------------------------------------------------------------*/
@@ -14045,6 +14061,15 @@ const cacclGetLaunchInfo = (req) => {
14045
14061
  const internalGetLogCollection = () => {
14046
14062
  return _logCollection !== null && _logCollection !== void 0 ? _logCollection : null;
14047
14063
  };
14064
+ /**
14065
+ * Get cross-server credential collection
14066
+ * @author Gabe Abrams
14067
+ * @return cross-server credential collection if one was included during launch or null
14068
+ * if we don't have a cross-server credential collection (yet)
14069
+ */
14070
+ const internalGetCrossServerCredentialCollection = () => {
14071
+ return _crossServerCredentialCollection !== null && _crossServerCredentialCollection !== void 0 ? _crossServerCredentialCollection : null;
14072
+ };
14048
14073
  /*------------------------------------------------------------------------*/
14049
14074
  /* Main */
14050
14075
  /*------------------------------------------------------------------------*/
@@ -14064,10 +14089,14 @@ const internalGetLogCollection = () => {
14064
14089
  * userIds are allowed to review logs. If a dce-mango collection, only
14065
14090
  * Canvas admins with entries in that collection ({ userId, ...}) are allowed
14066
14091
  * to review logs
14092
+ * @param [opts.crossServerCredentialCollection] mongo collection from dce-mango to use for
14093
+ * storing cross-server credentials. If none is included, cross-server credentials
14094
+ * are not supported
14067
14095
  */
14068
14096
  const initServer = (opts) => {
14069
14097
  _cacclGetLaunchInfo = opts.getLaunchInfo;
14070
14098
  _logCollection = opts.logCollection;
14099
+ _crossServerCredentialCollection = opts.crossServerCredentialCollection;
14071
14100
  /*----------------------------------------*/
14072
14101
  /* Logging */
14073
14102
  /*----------------------------------------*/
@@ -14610,6 +14639,154 @@ const parseUserAgent = (userAgent) => {
14610
14639
  };
14611
14640
  };
14612
14641
 
14642
+ /*------------------------------------------------------------------------*/
14643
+ /* ------------------------------ Encoding ------------------------------ */
14644
+ /*------------------------------------------------------------------------*/
14645
+ /**
14646
+ * Validate a JWT token
14647
+ * @author Gabe Abrams
14648
+ * @param encodedSecret the encoded secret
14649
+ * @return the secret
14650
+ */
14651
+ const decodeCredentialSecret = (encodedSecret) => {
14652
+ // Get the credential encoding salt
14653
+ const credentialEncodingSalt = process.env.REACTKIT_CRED_ENCODING_SALT;
14654
+ if (!credentialEncodingSalt) {
14655
+ throw new ErrorWithCode('Cannot decode a cross-server credential secret because the credential encoding salt was not found in env.', ReactKitErrorCode$1.CrossServerNoCredentialEncodingSalt);
14656
+ }
14657
+ // Decode the secret
14658
+ return jwt.decode(encodedSecret, credentialEncodingSalt);
14659
+ };
14660
+ /*------------------------------------------------------------------------*/
14661
+ /* ------------------------------- Signing ------------------------------ */
14662
+ /*------------------------------------------------------------------------*/
14663
+ /**
14664
+ * Sign using sha 256
14665
+ * @author Gabe Abrams
14666
+ * @param opts object containing all arguments
14667
+ * @param opts.pack the pack to sign
14668
+ * @param opts.secret the reactkit secret to sign with
14669
+ * @return the signed hash
14670
+ */
14671
+ const genSignature = (opts) => {
14672
+ // Generate signature
14673
+ return (createHash('sha256')
14674
+ .update(JSON.stringify({ pack: opts.pack, secret: opts.secret }))
14675
+ .digest('base64'));
14676
+ };
14677
+ /**
14678
+ * Sign data with a private reactkit key, package it into a signed data pack
14679
+ * @author Gabe Abrams
14680
+ * @param opts object containing all arguments
14681
+ * @param opts.method the method to sign
14682
+ * @param opts.path the http request path
14683
+ * @param opts.params the data in the body to sign
14684
+ * @param opts.key the reactkit key to sign with
14685
+ * @param opts.secret the reactkit secret to sign with
14686
+ * @return the signed data
14687
+ */
14688
+ const createSignedPack = (opts) => {
14689
+ // Create a timestamp
14690
+ const timestamp = Date.now();
14691
+ // Create the pack
14692
+ const pack = JSON.stringify({
14693
+ method: opts.method,
14694
+ path: opts.path,
14695
+ params: opts.params,
14696
+ key: opts.key,
14697
+ timestamp,
14698
+ });
14699
+ // Generate signature
14700
+ const signature = genSignature({
14701
+ pack,
14702
+ secret: opts.secret,
14703
+ });
14704
+ // Create a signed pack
14705
+ const signedPack = encodeURIComponent(JSON.stringify({
14706
+ pack,
14707
+ signature,
14708
+ }));
14709
+ // Return the signed pack
14710
+ return signedPack;
14711
+ };
14712
+ /**
14713
+ * Parse signed pack. Throws an error if invalid
14714
+ * @author Gabe Abrams
14715
+ * @param opts object containing all arguments
14716
+ * @param opts.method the method of the data validate
14717
+ * @param opts.path the http request path to validate
14718
+ * @param opts.scope the name of the scope to validate
14719
+ * @param opts.signedPack the signed data pack to validate
14720
+ * @returns parsed and validated params
14721
+ */
14722
+ const parseSignedPack = (opts) => __awaiter(void 0, void 0, void 0, function* () {
14723
+ // Extract signature
14724
+ let pack;
14725
+ let signature;
14726
+ let method;
14727
+ let path;
14728
+ let key;
14729
+ let timestamp;
14730
+ let params;
14731
+ try {
14732
+ ({
14733
+ pack,
14734
+ signature,
14735
+ } = JSON.parse(decodeURIComponent(opts.signedPack)));
14736
+ // Unpack
14737
+ ({
14738
+ method,
14739
+ path,
14740
+ params,
14741
+ key,
14742
+ timestamp,
14743
+ } = JSON.parse(pack));
14744
+ }
14745
+ catch (err) {
14746
+ throw new ErrorWithCode('Could not validate a cross-server request because the request could not be parsed.', ReactKitErrorCode$1.PackUnparseable);
14747
+ }
14748
+ // Make sure the method and path match
14749
+ if (method !== opts.method) {
14750
+ throw new ErrorWithCode('Could not validate a cross-server request because the method did not match.', ReactKitErrorCode$1.PackInvalidMethod);
14751
+ }
14752
+ if (path !== opts.path) {
14753
+ throw new ErrorWithCode('Could not validate a cross-server request because the path did not match.', ReactKitErrorCode$1.PackInvalidPath);
14754
+ }
14755
+ // Make sure the timestamp was recent enough
14756
+ const elapsedMs = Math.abs(Date.now() - timestamp);
14757
+ if (elapsedMs < MINUTE_IN_MS) {
14758
+ throw new ErrorWithCode('Could not validate a cross-server request because the request was too old.', ReactKitErrorCode$1.PackInvalidTimestamp);
14759
+ }
14760
+ // Get the cross-server credential collection
14761
+ const crossServerCredentialCollection = internalGetCrossServerCredentialCollection();
14762
+ if (!crossServerCredentialCollection) {
14763
+ throw new ErrorWithCode('Could not validate a cross-server request because the cross-server credential collection was not ready in time.', ReactKitErrorCode$1.PackInvalidCollection);
14764
+ }
14765
+ // Get the cross-server credential
14766
+ const crossServerCredential = yield crossServerCredentialCollection.find({ key });
14767
+ if (!crossServerCredential) {
14768
+ throw new ErrorWithCode('Could not validate a cross-server request because the credential was not found.', ReactKitErrorCode$1.PackInvalidCredential);
14769
+ }
14770
+ // Make sure the scope is included
14771
+ const allowedScopes = crossServerCredential.scopes;
14772
+ if (!allowedScopes.includes(opts.scope)) {
14773
+ throw new ErrorWithCode('Could not validate a cross-server request because the scope was not included.', ReactKitErrorCode$1.PackInvalidScope);
14774
+ }
14775
+ // Decode the secret
14776
+ const secret = decodeCredentialSecret(crossServerCredential.encodedeSecret);
14777
+ // Generate signature
14778
+ const expectedSignature = genSignature({
14779
+ pack,
14780
+ secret,
14781
+ });
14782
+ // Make sure the signature matches
14783
+ if (signature !== expectedSignature) {
14784
+ throw new ErrorWithCode('Could not validate a cross-server request because the signature did not match.', ReactKitErrorCode$1.PackInvalidSignature);
14785
+ }
14786
+ // Return body
14787
+ return params !== null && params !== void 0 ? params : {};
14788
+ });
14789
+
14613
14790
  /**
14614
14791
  * Generate an express API route handler
14615
14792
  * @author Gabe Abrams
@@ -14617,14 +14794,15 @@ const parseUserAgent = (userAgent) => {
14617
14794
  * @param opts.paramTypes map containing the types for each parameter that is
14618
14795
  * included in the request (map: param name => type)
14619
14796
  * @param opts.handler function that processes the request
14620
- * @param [opts.skipSessionCheck] if true, skip the session check (allow users
14621
- * to not be logged in and launched via LTI)
14622
- * @param [opts.allowedHosts] if included, only allow requests from these hosts
14623
- * (start a hostname with a "*" to only check the end of the hostname)
14624
- * you can include just one string instead of an array
14625
- * @param [opts.bannedHosts] if included, do not allow requests from these hosts
14626
- * (start a hostname with a "*" to only check the end of the hostname)
14627
- * you can include just one string instead of an array
14797
+ * @param [opts.crossServerScope] the scope associated with this endpoint.
14798
+ * If defined, this is a cross-server endpoint, which will never
14799
+ * have any launch data, will never check Canvas roles or launch status, and will
14800
+ * instead use scopes and reactkit credentials to sign and validate requests.
14801
+ * Never start the path with /api/ttm or /api/admin if the endpoint is a cross-server
14802
+ * endpoint because those roles will not be validated
14803
+ * @param [opts.skipSessionCheck=true if crossServerScope defined] if true, skip
14804
+ * the session check (allow users to not be logged in and launched via LTI).
14805
+ * If crossServerScope is defined, this is always true
14628
14806
  * @param [opts.unhandledErrorMessagePrefix] if included, when an error that
14629
14807
  * is not of type ErrorWithCode is thrown, the client will receive an error
14630
14808
  * where the error message is prefixed with this string. For example,
@@ -14652,86 +14830,51 @@ const parseUserAgent = (userAgent) => {
14652
14830
  const genRouteHandler = (opts) => {
14653
14831
  // Return a route handler
14654
14832
  return (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
14655
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
14833
+ /*----------------------------------------*/
14834
+ /* ------------- Preparation ------------ */
14835
+ /*----------------------------------------*/
14836
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
14656
14837
  // Output params
14657
14838
  const output = {};
14839
+ // Determine cross server scopes
14840
+ let crossServerScope = null;
14841
+ if (opts.crossServerScope) {
14842
+ crossServerScope = (_a = opts.crossServerScope) !== null && _a !== void 0 ? _a : null;
14843
+ }
14844
+ // Determine whether we're skipping the session check
14845
+ const skipSessionCheck = !!(opts.skipSessionCheck
14846
+ || crossServerScope);
14847
+ // Get body from everywhere it can come from
14848
+ let requestBody = Object.assign(Object.assign(Object.assign({}, req.body), req.query), req.params);
14658
14849
  /*----------------------------------------*/
14659
- /* ----------- Hostname Check ----------- */
14850
+ /* ------- Cross-Server Validation ------ */
14660
14851
  /*----------------------------------------*/
14661
- // Get hostnames
14662
- const originURL = String(req.get('origin')
14663
- || req.headers.origin
14664
- || req.headers.referer);
14665
- const originHostname = (originURL
14666
- // Remove protocol
14667
- .replace(/(^\w+:|^)\/\//, '')
14668
- // Remove port
14669
- .replace(/:\d+$/, ''));
14670
- const serverHostname = String(req.hostname);
14671
- // Check allowed
14672
- if (opts.allowedHosts) {
14673
- // Only accept requests from allowed hosts
14674
- const allowedArray = (Array.isArray(opts.allowedHosts)
14675
- ? opts.allowedHosts
14676
- : [opts.allowedHosts]);
14677
- // Check if server is localhost
14678
- if (serverHostname === 'localhost') {
14679
- // Allow localhost
14680
- allowedArray.push('localhost');
14681
- }
14682
- // Check if current host is allowed
14683
- const allowed = allowedArray.some((allowedHost) => {
14684
- if (allowedHost.startsWith('*')) {
14685
- // Check end of hostname
14686
- return originHostname.endsWith(allowedHost.substring(1));
14687
- }
14688
- // Check full hostname
14689
- return originHostname.toLowerCase() === allowedHost.toLowerCase();
14690
- });
14691
- // If not allowed, return error
14692
- if (!allowed) {
14693
- return handleError(res, {
14694
- message: 'You are not allowed to access this endpoint.',
14695
- code: ReactKitErrorCode$1.HostNotAllowed,
14696
- status: 403,
14697
- });
14852
+ if (crossServerScope) {
14853
+ // Get the signed pack
14854
+ const { signedPack } = requestBody;
14855
+ // If no pack, throw error
14856
+ if (!signedPack || typeof signedPack !== 'string') {
14857
+ throw new ErrorWithCode('Could not process a cross server request because there was no valid signed pack.', ReactKitErrorCode$1.CrossServerNoPack);
14698
14858
  }
14699
- }
14700
- // Check banned
14701
- if (opts.bannedHosts) {
14702
- // Do not allow requests from banned hosts
14703
- const bannedArray = (Array.isArray(opts.bannedHosts)
14704
- ? opts.bannedHosts
14705
- : [opts.bannedHosts]);
14706
- // Check if current host is banned
14707
- const banned = bannedArray.some((bannedHost) => {
14708
- if (bannedHost.startsWith('*')) {
14709
- // Check end of hostname
14710
- return originHostname.endsWith(bannedHost.substring(1));
14711
- }
14712
- // Check full hostname
14713
- return originHostname.toLowerCase() === bannedHost.toLowerCase();
14859
+ // Validate the request
14860
+ const crossServerParams = yield parseSignedPack({
14861
+ method: (_b = req.method) !== null && _b !== void 0 ? _b : 'GET',
14862
+ path: req.path,
14863
+ scope: crossServerScope,
14864
+ signedPack,
14714
14865
  });
14715
- // If banned, return error
14716
- if (banned) {
14717
- return handleError(res, {
14718
- message: 'You are not allowed to access this endpoint.',
14719
- code: ReactKitErrorCode$1.HostBanned,
14720
- status: 403,
14721
- });
14722
- }
14866
+ // Replace body with params from the pack
14867
+ requestBody = crossServerParams !== null && crossServerParams !== void 0 ? crossServerParams : {};
14723
14868
  }
14724
14869
  /*----------------------------------------*/
14725
14870
  /* ------------ Parse Params ------------ */
14726
14871
  /*----------------------------------------*/
14727
14872
  // Process items one by one
14728
- const paramList = Object.entries((_a = opts.paramTypes) !== null && _a !== void 0 ? _a : {});
14873
+ const paramList = Object.entries((_c = opts.paramTypes) !== null && _c !== void 0 ? _c : {});
14729
14874
  for (let i = 0; i < paramList.length; i++) {
14730
14875
  const [name, type] = paramList[i];
14731
14876
  // Find the value as a string
14732
- const value = (req.params[name]
14733
- || req.query[name]
14734
- || req.body[name]);
14877
+ const value = requestBody[name];
14735
14878
  // Parse
14736
14879
  if (type === ParamType$1.Boolean || type === ParamType$1.BooleanOptional) {
14737
14880
  // Boolean
@@ -14889,7 +15032,7 @@ const genRouteHandler = (opts) => {
14889
15032
  // Not launched
14890
15033
  (!launched || !launchInfo)
14891
15034
  // Not skipping the session check
14892
- && !opts.skipSessionCheck) {
15035
+ && !skipSessionCheck) {
14893
15036
  return handleError(res, {
14894
15037
  message: 'Your session has expired. Please refresh the page and try again.',
14895
15038
  code: ReactKitErrorCode$1.SessionExpired,
@@ -14909,7 +15052,7 @@ const genRouteHandler = (opts) => {
14909
15052
  && !launchInfo.isLearner
14910
15053
  && !launchInfo.isAdmin))
14911
15054
  // Not skipping the session check
14912
- && !opts.skipSessionCheck) {
15055
+ && !skipSessionCheck) {
14913
15056
  return handleError(res, {
14914
15057
  message: 'Your session was invalid. Please refresh the page and try again.',
14915
15058
  code: ReactKitErrorCode$1.SessionExpired,
@@ -14919,34 +15062,34 @@ const genRouteHandler = (opts) => {
14919
15062
  // Add launch info to output
14920
15063
  output.userId = (launchInfo
14921
15064
  ? launchInfo.userId
14922
- : ((_b = output.userId) !== null && _b !== void 0 ? _b : undefined));
15065
+ : ((_d = output.userId) !== null && _d !== void 0 ? _d : undefined));
14923
15066
  output.userFirstName = (launchInfo
14924
15067
  ? launchInfo.userFirstName
14925
- : ((_c = output.userFirstName) !== null && _c !== void 0 ? _c : undefined));
15068
+ : ((_e = output.userFirstName) !== null && _e !== void 0 ? _e : undefined));
14926
15069
  output.userLastName = (launchInfo
14927
15070
  ? launchInfo.userLastName
14928
- : ((_d = output.userLastName) !== null && _d !== void 0 ? _d : undefined));
15071
+ : ((_f = output.userLastName) !== null && _f !== void 0 ? _f : undefined));
14929
15072
  output.userEmail = (launchInfo
14930
15073
  ? launchInfo.userEmail
14931
- : ((_e = output.userEmail) !== null && _e !== void 0 ? _e : undefined));
15074
+ : ((_g = output.userEmail) !== null && _g !== void 0 ? _g : undefined));
14932
15075
  output.userAvatarURL = (launchInfo
14933
- ? ((_f = launchInfo.userImage) !== null && _f !== void 0 ? _f : 'http://www.gravatar.com/avatar/?d=identicon')
14934
- : ((_g = output.userAvatarURL) !== null && _g !== void 0 ? _g : undefined));
15076
+ ? ((_h = launchInfo.userImage) !== null && _h !== void 0 ? _h : 'http://www.gravatar.com/avatar/?d=identicon')
15077
+ : ((_j = output.userAvatarURL) !== null && _j !== void 0 ? _j : undefined));
14935
15078
  output.isLearner = (launchInfo
14936
15079
  ? !!launchInfo.isLearner
14937
- : ((_h = output.isLearner) !== null && _h !== void 0 ? _h : undefined));
15080
+ : ((_k = output.isLearner) !== null && _k !== void 0 ? _k : undefined));
14938
15081
  output.isTTM = (launchInfo
14939
15082
  ? !!launchInfo.isTTM
14940
- : ((_j = output.isTTM) !== null && _j !== void 0 ? _j : undefined));
15083
+ : ((_l = output.isTTM) !== null && _l !== void 0 ? _l : undefined));
14941
15084
  output.isAdmin = (launchInfo
14942
15085
  ? !!launchInfo.isAdmin
14943
- : ((_k = output.isAdmin) !== null && _k !== void 0 ? _k : undefined));
15086
+ : ((_m = output.isAdmin) !== null && _m !== void 0 ? _m : undefined));
14944
15087
  output.courseId = (launchInfo
14945
- ? ((_l = output.courseId) !== null && _l !== void 0 ? _l : launchInfo.courseId)
14946
- : ((_m = output.courseId) !== null && _m !== void 0 ? _m : undefined));
15088
+ ? ((_o = output.courseId) !== null && _o !== void 0 ? _o : launchInfo.courseId)
15089
+ : ((_p = output.courseId) !== null && _p !== void 0 ? _p : undefined));
14947
15090
  output.courseName = (launchInfo
14948
15091
  ? launchInfo.contextLabel
14949
- : ((_o = output.courseName) !== null && _o !== void 0 ? _o : undefined));
15092
+ : ((_q = output.courseName) !== null && _q !== void 0 ? _q : undefined));
14950
15093
  // Add other session variables
14951
15094
  Object.keys(req.session).forEach((propName) => {
14952
15095
  // Skip if prop already in output
@@ -15020,7 +15163,7 @@ const genRouteHandler = (opts) => {
15020
15163
  * @author Gabe Abrams
15021
15164
  */
15022
15165
  const logServerEvent = (logOpts) => __awaiter(void 0, void 0, void 0, function* () {
15023
- var _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1;
15166
+ var _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3;
15024
15167
  // NOTE: internally, we slip through an opts.overrideAsClientEvent boolean
15025
15168
  // that indicates that this is actually a client event, but we don't
15026
15169
  // include that in the LogFunction type because this is internal and
@@ -15052,24 +15195,24 @@ const genRouteHandler = (opts) => {
15052
15195
  timestamp,
15053
15196
  context: (typeof logOpts.context === 'string'
15054
15197
  ? logOpts.context
15055
- : ((_q = ((_p = logOpts.context) !== null && _p !== void 0 ? _p : {})._) !== null && _q !== void 0 ? _q : LogBuiltInMetadata.Context.Uncategorized)),
15056
- subcontext: ((_r = logOpts.subcontext) !== null && _r !== void 0 ? _r : LogBuiltInMetadata.Context.Uncategorized),
15057
- tags: ((_s = logOpts.tags) !== null && _s !== void 0 ? _s : []),
15058
- level: ((_t = logOpts.level) !== null && _t !== void 0 ? _t : LogLevel$1.Info),
15059
- metadata: ((_u = logOpts.metadata) !== null && _u !== void 0 ? _u : {}),
15198
+ : ((_s = ((_r = logOpts.context) !== null && _r !== void 0 ? _r : {})._) !== null && _s !== void 0 ? _s : LogBuiltInMetadata.Context.Uncategorized)),
15199
+ subcontext: ((_t = logOpts.subcontext) !== null && _t !== void 0 ? _t : LogBuiltInMetadata.Context.Uncategorized),
15200
+ tags: ((_u = logOpts.tags) !== null && _u !== void 0 ? _u : []),
15201
+ level: ((_v = logOpts.level) !== null && _v !== void 0 ? _v : LogLevel$1.Info),
15202
+ metadata: ((_w = logOpts.metadata) !== null && _w !== void 0 ? _w : {}),
15060
15203
  };
15061
15204
  // Type-specific info
15062
15205
  const typeSpecificInfo = (('error' in opts && opts.error)
15063
15206
  ? {
15064
15207
  type: LogType$1.Error,
15065
- errorMessage: (_v = logOpts.error.message) !== null && _v !== void 0 ? _v : 'Unknown message',
15066
- errorCode: (_w = logOpts.error.code) !== null && _w !== void 0 ? _w : ReactKitErrorCode$1.NoCode,
15067
- errorStack: (_x = logOpts.error.stack) !== null && _x !== void 0 ? _x : 'No stack',
15208
+ errorMessage: (_x = logOpts.error.message) !== null && _x !== void 0 ? _x : 'Unknown message',
15209
+ errorCode: (_y = logOpts.error.code) !== null && _y !== void 0 ? _y : ReactKitErrorCode$1.NoCode,
15210
+ errorStack: (_z = logOpts.error.stack) !== null && _z !== void 0 ? _z : 'No stack',
15068
15211
  }
15069
15212
  : {
15070
15213
  type: LogType$1.Action,
15071
- target: ((_y = logOpts.target) !== null && _y !== void 0 ? _y : LogBuiltInMetadata.Target.NoTarget),
15072
- action: ((_z = logOpts.action) !== null && _z !== void 0 ? _z : LogAction$1.Unknown),
15214
+ target: ((_0 = logOpts.target) !== null && _0 !== void 0 ? _0 : LogBuiltInMetadata.Target.NoTarget),
15215
+ action: ((_1 = logOpts.action) !== null && _1 !== void 0 ? _1 : LogAction$1.Unknown),
15073
15216
  });
15074
15217
  // Source-specific info
15075
15218
  const sourceSpecificInfo = (logOpts.overrideAsClientEvent
@@ -15104,7 +15247,7 @@ const genRouteHandler = (opts) => {
15104
15247
  catch (err) {
15105
15248
  // Print because we cannot store the error
15106
15249
  // eslint-disable-next-line no-console
15107
- console.error('Could not log the following:', logOpts, 'due to this error:', ((_0 = err) !== null && _0 !== void 0 ? _0 : {}).message, ((_1 = err) !== null && _1 !== void 0 ? _1 : {}).stack);
15250
+ console.error('Could not log the following:', logOpts, 'due to this error:', ((_2 = err) !== null && _2 !== void 0 ? _2 : {}).message, ((_3 = err) !== null && _3 !== void 0 ? _3 : {}).stack);
15108
15251
  // Create a dummy log to return
15109
15252
  const dummyMainInfo = {
15110
15253
  id: '-1',
@@ -15457,6 +15600,18 @@ const initLogCollection = (Collection) => {
15457
15600
  });
15458
15601
  };
15459
15602
 
15603
+ /**
15604
+ * Initialize a cross-server credential collection given the dce-mango Collection class
15605
+ * @author Gabe Abrams
15606
+ * @param Collection the Collection class from dce-mango
15607
+ * @returns initialized logCollection
15608
+ */
15609
+ const initCrossServerCredentialCollection = (Collection) => {
15610
+ return new Collection('CrossServerCredential', {
15611
+ uniqueIndexKey: 'key',
15612
+ });
15613
+ };
15614
+
15460
15615
  /*------------------------------------------------------------------------*/
15461
15616
  /* -------------------------------- Cache ------------------------------- */
15462
15617
  /*------------------------------------------------------------------------*/
@@ -16206,9 +16361,6 @@ const shuffleArray = (arr) => {
16206
16361
  * @param [opts.host] host to send request to
16207
16362
  * @param [opts.method=GET] http method to use
16208
16363
  * @param [opts.params] body/data to include in the request
16209
- * @param [opts.headers] headers to include in the request
16210
- * @param [opts.sendCrossDomainCredentials=true if in development mode] if true,
16211
- * send cross-domain credentials even if not in dev mode
16212
16364
  * @param [opts.responseType=JSON] expected response type
16213
16365
  * @returns { body, status, headers } on success
16214
16366
  */
@@ -16245,7 +16397,7 @@ const sendServerToServerRequest = (opts) => __awaiter(void 0, void 0, void 0, fu
16245
16397
  url = `https://${opts.host}${opts.path}${query}`;
16246
16398
  }
16247
16399
  // Update headers
16248
- const headers = opts.headers || {};
16400
+ const headers = {};
16249
16401
  let data = null;
16250
16402
  if (!headers['Content-Type']) {
16251
16403
  // Form encoded
@@ -16318,49 +16470,49 @@ const sendServerToServerRequest = (opts) => __awaiter(void 0, void 0, void 0, fu
16318
16470
  });
16319
16471
 
16320
16472
  /**
16321
- * Send a server-to-server request from this sever to another server that uses
16322
- * dce-reactkit [for server only]
16473
+ * Visit an endpoint on another server
16323
16474
  * @author Gabe Abrams
16324
16475
  * @param opts object containing all arguments
16325
- * @param opts.host - the host of the other server
16326
- * @param opts.path - the path of the other server's endpoint
16327
- * @param [opts.method=GET] - the method of the endpoint
16328
- * @param [opts.params] - query/body parameters to include
16329
- * @param [opts.headers] - headers to include
16330
- * @returns response from server
16476
+ * @param opts.method the method of the endpoint
16477
+ * @param opts.path the path of the other server's endpoint
16478
+ * @param opts.host the host of the other server
16479
+ * @param [opts.key=process.env.REACTKIT_CROSS_SERVER_CREDENTIAL_KEY] reactkit cross-server
16480
+ * credential key
16481
+ * @param [opts.secret=process.env.REACTKIT_CROSS_SERVER_CREDENTIAL_SECRET reactkit cross-server
16482
+ * credential secret
16483
+ * @param [opts.params={}] query/body parameters to include
16484
+ * @param [opts.responseType=JSON] the response type from the other server
16331
16485
  */
16332
16486
  const visitEndpointOnAnotherServer = (opts) => __awaiter(void 0, void 0, void 0, function* () {
16333
- var _a;
16334
- // Remove properties with undefined values
16335
- let params;
16336
- if (opts.params) {
16337
- params = Object.fromEntries(Object
16338
- .entries(opts.params)
16339
- .filter(([, value]) => {
16340
- return value !== undefined;
16341
- }));
16342
- }
16343
- // Automatically JSONify arrays and objects
16344
- if (params) {
16345
- params = Object.fromEntries(Object
16346
- .entries(params)
16347
- .map(([key, value]) => {
16348
- if (Array.isArray(value) || typeof value === 'object') {
16349
- return [key, JSON.stringify(value)];
16350
- }
16351
- return [key, value];
16352
- }));
16353
- }
16487
+ var _a, _b, _c;
16488
+ // Get cross-server credentials
16489
+ const key = (_a = opts.key) !== null && _a !== void 0 ? _a : process.env.REACTKIT_CROSS_SERVER_KEY;
16490
+ const secret = (_b = opts.secret) !== null && _b !== void 0 ? _b : process.env.REACTKIT_CROSS_SERVER_SECRET;
16491
+ // Throw error if no credentials
16492
+ if (!key || !secret) {
16493
+ throw new ErrorWithCode('Cannot send cross-server signed request because either or both the key and secret were not included or found in env.', ReactKitErrorCode$1.CrossServerNoCredentialsToSignWith);
16494
+ }
16495
+ // Create signed pack
16496
+ const signedPack = createSignedPack({
16497
+ method: opts.method,
16498
+ path: opts.path,
16499
+ params: (_c = opts.params) !== null && _c !== void 0 ? _c : {},
16500
+ key,
16501
+ secret,
16502
+ });
16354
16503
  // Send the request
16355
16504
  const response = yield sendServerToServerRequest({
16356
- host: opts.host,
16357
16505
  path: opts.path,
16358
- method: (_a = opts.method) !== null && _a !== void 0 ? _a : 'GET',
16359
- params,
16506
+ host: opts.host,
16507
+ method: opts.method,
16508
+ params: {
16509
+ signedPack,
16510
+ },
16511
+ responseType: opts.responseType,
16360
16512
  });
16361
16513
  // Check for failure
16362
16514
  if (!response || !response.body) {
16363
- throw new ErrorWithCode('We didn\'t get a response from the server. Please check your internet connection.', ReactKitErrorCode$1.NoResponse);
16515
+ throw new ErrorWithCode('We didn\'t get a response from the other server. Please check your internet connection.', ReactKitErrorCode$1.NoResponse);
16364
16516
  }
16365
16517
  if (!response.body.success) {
16366
16518
  // Other errors
@@ -16374,27 +16526,6 @@ const visitEndpointOnAnotherServer = (opts) => __awaiter(void 0, void 0, void 0,
16374
16526
  return body;
16375
16527
  });
16376
16528
 
16377
- const punctuationRegex = /[!@#$%^&*(),.?\/;:'"\-\[\]]/g;
16378
- /**
16379
- * Get number of words in string
16380
- * @author Gardenia Liu
16381
- * @author Allison Zhang
16382
- * @author Gabe Abrams
16383
- * @param text the string to check
16384
- * @returns number of words in the string
16385
- */
16386
- const getWordCount = (text) => {
16387
- const trimmedTextWithoutPunctuation = (text
16388
- // Remove leading and trailing whitespace
16389
- .trim()
16390
- // Remove punctuation
16391
- .replace(punctuationRegex, ''));
16392
- if (trimmedTextWithoutPunctuation.length === 0) {
16393
- return 0;
16394
- }
16395
- return trimmedTextWithoutPunctuation.split(/\s+/g).length;
16396
- };
16397
-
16398
16529
  /**
16399
16530
  * Days of the week
16400
16531
  * @author Gabe Abrams
@@ -16411,5 +16542,5 @@ var DayOfWeek;
16411
16542
  })(DayOfWeek || (DayOfWeek = {}));
16412
16543
  var DayOfWeek$1 = DayOfWeek;
16413
16544
 
16414
- export { AppWrapper, AutoscrollToBottomContainer, ButtonInputGroup, CSVDownloadButton, CheckboxButton, CopiableBox, DAY_IN_MS, DBEntryFieldType$1 as DBEntryFieldType, DBEntryManagerPanel, DayOfWeek$1 as DayOfWeek, Drawer, Dropdown, DropdownItemType$1 as DropdownItemType, DynamicWord, ErrorBox, ErrorWithCode, HOUR_IN_MS, IntelliTable, ItemPicker, LoadingSpinner, LogAction$1 as LogAction, LogBuiltInMetadata, LogReviewer, LogSource$1 as LogSource, LogType$1 as LogType, MINUTE_IN_MS, Modal, ModalButtonType$1 as ModalButtonType, ModalSize$1 as ModalSize, ModalType$1 as ModalType, MultiSwitch, ParamType$1 as ParamType, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode$1 as ReactKitErrorCode, SimpleDateChooser, TabBox, ToggleSwitch, Tooltip, Variant$1 as Variant, abbreviate, addDBEditorEndpoints, addFatalErrorHandler, alert, avg, canReviewLogs, capitalize, ceilToNumDecimals, combineClassNames, compareArraysByProp, confirm, everyAsync, extractProp, filterAsync, floorToNumDecimals, forEachAsync, forceNumIntoBounds, genCSV, genCommaList, genRouteHandler, getHumanReadableDate, getLocalTimeInfo, getMonthName, getOrdinal, getPartOfDay, getTimeInfoInET, getWordCount, handleError, handleSuccess, idify, initClient, initLogCollection, initServer, isMobileOrTablet, leaveToURL, logClientEvent, makeLinksClickable, mapAsync, onlyKeepLetters, padDecimalZeros, padZerosLeft, parallelLimit, prefixWithAOrAn, prompt, roundToNumDecimals, setClientEventMetadataPopulator, showFatalError, shuffleArray, someAsync, startMinWait, stringsToHumanReadableList, stubServerEndpoint, sum, useForceRender, validateEmail, validatePhoneNumber, validateString, visitEndpointOnAnotherServer, visitServerEndpoint, waitMs };
16545
+ export { AppWrapper, AutoscrollToBottomContainer, ButtonInputGroup, CSVDownloadButton, CheckboxButton, CopiableBox, DAY_IN_MS, DBEntryFieldType$1 as DBEntryFieldType, DBEntryManagerPanel, DayOfWeek$1 as DayOfWeek, Drawer, Dropdown, DropdownItemType$1 as DropdownItemType, DynamicWord, ErrorBox, ErrorWithCode, HOUR_IN_MS, IntelliTable, ItemPicker, LoadingSpinner, LogAction$1 as LogAction, LogBuiltInMetadata, LogReviewer, LogSource$1 as LogSource, LogType$1 as LogType, MINUTE_IN_MS, Modal, ModalButtonType$1 as ModalButtonType, ModalSize$1 as ModalSize, ModalType$1 as ModalType, MultiSwitch, ParamType$1 as ParamType, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode$1 as ReactKitErrorCode, SimpleDateChooser, TabBox, ToggleSwitch, Tooltip, Variant$1 as Variant, abbreviate, addDBEditorEndpoints, addFatalErrorHandler, alert, avg, canReviewLogs, capitalize, ceilToNumDecimals, combineClassNames, compareArraysByProp, confirm, everyAsync, extractProp, filterAsync, floorToNumDecimals, forEachAsync, forceNumIntoBounds, genCSV, genCommaList, genRouteHandler, getHumanReadableDate, getLocalTimeInfo, getMonthName, getOrdinal, getPartOfDay, getTimeInfoInET, handleError, handleSuccess, idify, initClient, initCrossServerCredentialCollection, initLogCollection, initServer, isMobileOrTablet, leaveToURL, logClientEvent, makeLinksClickable, mapAsync, onlyKeepLetters, padDecimalZeros, padZerosLeft, parallelLimit, prefixWithAOrAn, prompt, roundToNumDecimals, setClientEventMetadataPopulator, showFatalError, shuffleArray, someAsync, startMinWait, stringsToHumanReadableList, stubServerEndpoint, sum, useForceRender, validateEmail, validatePhoneNumber, validateString, visitEndpointOnAnotherServer, visitServerEndpoint, waitMs };
16415
16546
  //# sourceMappingURL=index.js.map