@openstax/ts-utils 1.1.3 → 1.1.4

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/index.d.ts CHANGED
@@ -10,6 +10,10 @@ declare type HashCompoundValue = Array<HashValue> | {
10
10
  };
11
11
  export declare const hashValue: (value: HashValue) => string;
12
12
  export declare const once: <F extends (...args: any[]) => any>(fn: F) => F;
13
+ export declare const partitionSequence: <T, P>(getPartition: (thing: T, previous?: P | undefined) => {
14
+ matches?: boolean | undefined;
15
+ value: P;
16
+ }, sequence: T[]) => [P, T[]][];
13
17
  export declare const memoize: <F extends (...args: any[]) => any>(fn: F) => F;
14
18
  export declare const roundToPrecision: (num: number, places: number) => number;
15
19
  export declare const getCommonProperties: <T1 extends {}, T2 extends {}>(thing1: T1, thing2: T2) => (keyof T1 & keyof T2)[];
package/dist/index.js CHANGED
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.tuple = exports.merge = exports.getCommonProperties = exports.roundToPrecision = exports.memoize = exports.once = exports.hashValue = exports.mapFind = exports.fnIf = exports.putKeyValue = exports.getKeyValueOr = exports.getKeyValue = void 0;
6
+ exports.tuple = exports.merge = exports.getCommonProperties = exports.roundToPrecision = exports.memoize = exports.partitionSequence = exports.once = exports.hashValue = exports.mapFind = exports.fnIf = exports.putKeyValue = exports.getKeyValueOr = exports.getKeyValue = void 0;
4
7
  const crypto_1 = require("crypto");
8
+ const deep_equal_1 = __importDefault(require("deep-equal"));
5
9
  const guards_1 = require("./guards");
6
10
  /*
7
11
  * there was a reason i made these instead of using lodash/fp but i forget what it was. i think maybe
@@ -72,6 +76,54 @@ const once = (fn) => {
72
76
  return ((...args) => result || (result = fn(...args)));
73
77
  };
74
78
  exports.once = once;
79
+ /*
80
+ * partitions a sequence based on a partition function returning {value: any; matches?: boolean}
81
+ * - if the function returns `matches` explicitly then adjacent matching elements will
82
+ * be grouped and the predicate value in the result will be from the last item in the group
83
+ * - if the function returns only a value then matching will be evaluated based on the deep
84
+ * equality of the value with its neighbors
85
+ *
86
+ * this is different from lodash/partition and lodash/groupBy because:
87
+ * - it preserves the order of the items, items will only be grouped if they are already adjacent
88
+ * - there can be any number of groups
89
+ * - it tells you the partition value
90
+ * - the partition value can be reduced, if you care (so you can like, partition on sequential values)
91
+ *
92
+ * simple predicate:
93
+ * returns: [[0, [1,2]], [1, [3,4,5]]]
94
+ * partitionSequence((n: number) => ({value: Math.floor(n / 3)}), [1,2,3,4,5])
95
+ *
96
+ * mutating partition:
97
+ * returns: [
98
+ * [{min: 1,max: 3}, [1,2,3]],
99
+ * [{min: 5,max: 6}, [5,6]],
100
+ * [{min: 8,max: 8}, [8]],
101
+ * ]
102
+ * partitionSequence(
103
+ * (n: number, p?: {min: number; max: number}) =>
104
+ * p && p.max + 1 === n
105
+ * ? {value: {...p, max: n}, matches: true}
106
+ * : {value: {min: n, max: n}, matches: false}
107
+ * , [1,2,3,5,6,8]
108
+ * )
109
+ */
110
+ const partitionSequence = (getPartition, sequence) => {
111
+ const appendItem = (result, item) => {
112
+ const current = result[result.length - 1];
113
+ const itemPartition = getPartition(item, current === null || current === void 0 ? void 0 : current[0]);
114
+ if (current && ((itemPartition.matches === undefined && (0, deep_equal_1.default)(current[0], itemPartition.value))
115
+ || itemPartition.matches)) {
116
+ current[0] = itemPartition.value;
117
+ current[1].push(item);
118
+ }
119
+ else {
120
+ result.push([itemPartition.value, [item]]);
121
+ }
122
+ return result;
123
+ };
124
+ return sequence.reduce(appendItem, []);
125
+ };
126
+ exports.partitionSequence = partitionSequence;
75
127
  /*
76
128
  * memoizes a function with any number of arguments
77
129
  */
@@ -6,12 +6,62 @@ declare type Config = {
6
6
  };
7
7
  interface Initializer<C> {
8
8
  configSpace?: C;
9
+ window: Window;
10
+ }
11
+ export declare type EventHandler = (e: {
12
+ data: any;
13
+ origin: string;
14
+ source: Pick<Window, 'postMessage'>;
15
+ }) => void;
16
+ export interface Window {
9
17
  fetch: GenericFetch;
18
+ top: {} | null;
19
+ parent: Pick<Window, 'postMessage'> | null;
20
+ location: {
21
+ search: string;
22
+ };
23
+ document: {
24
+ referrer: string;
25
+ };
26
+ postMessage: (data: any, origin: string) => void;
27
+ addEventListener: (event: 'message', callback: EventHandler) => void;
28
+ removeEventListener: (event: 'message', callback: EventHandler) => void;
10
29
  }
11
- export declare const browserAuthProvider: <C extends string = "auth">(initializer: Initializer<C>) => (configProvider: { [key in C]: {
30
+ export declare const browserAuthProvider: <C extends string = "auth">({ window, configSpace }: Initializer<C>) => (configProvider: { [key in C]: {
12
31
  accountsUrl: import("../../config").ConfigValueProvider<string>;
13
- }; }) => (queryString?: string) => {
32
+ }; }) => {
33
+ /**
34
+ * adds auth parameters to the url. this is only safe to use when using javascript to navigate
35
+ * within the current window, eg `window.location = 'https://my.otherservice.com';` anchors
36
+ * should use getAuthorizedLinkUrl for their href.
37
+ *
38
+ * result unreliable unless `getUser` is resolved first.
39
+ */
40
+ getAuthorizedUrl: (urlString: string) => string;
41
+ /**
42
+ * all link href-s must be rendered with auth tokens so that they work when opened in a new tab
43
+ *
44
+ * result unreliable unless `getUser` is resolved first.
45
+ */
46
+ getAuthorizedLinkUrl: (urlString: string) => string;
47
+ /**
48
+ * gets an authorized url for an iframe src. sets params on the url and saves its
49
+ * origin to trust releasing user identity to it
50
+ *
51
+ * result unreliable unless `getUser` is resolved first.
52
+ */
53
+ getAuthorizedEmbedUrl: (urlString: string) => string;
54
+ /**
55
+ * gets second argument for `fetch` that has authentication token or cookie
56
+ *
57
+ * result unreliable unless `getUser` is resolved first.
58
+ */
14
59
  getAuthorizedFetchConfig: () => FetchConfig;
60
+ /**
61
+ * loads current user identity. does not reflect changes in identity after being called the first time.
62
+ *
63
+ * result unreliable unless `getUser` is resolved first.
64
+ */
15
65
  getUser: () => Promise<User | undefined>;
16
66
  };
17
67
  export {};
@@ -4,27 +4,129 @@ exports.browserAuthProvider = void 0;
4
4
  const __1 = require("../..");
5
5
  const config_1 = require("../../config");
6
6
  const guards_1 = require("../../guards");
7
- const browserAuthProvider = (initializer) => (configProvider) => {
8
- const config = configProvider[(0, guards_1.ifDefined)(initializer.configSpace, 'auth')];
7
+ var PostMessageTypes;
8
+ (function (PostMessageTypes) {
9
+ PostMessageTypes["ReceiveUser"] = "receive-user";
10
+ PostMessageTypes["RequestUser"] = "request-user";
11
+ })(PostMessageTypes || (PostMessageTypes = {}));
12
+ const browserAuthProvider = ({ window, configSpace }) => (configProvider) => {
13
+ const config = configProvider[(0, guards_1.ifDefined)(configSpace, 'auth')];
9
14
  const accountsUrl = (0, config_1.resolveConfigValue)(config.accountsUrl);
10
- return (queryString = '') => {
11
- const token = new URLSearchParams(queryString).get('auth');
12
- // *note* that this does not actually prevent cookies from being sent on same-origin
13
- // requests, i'm not sure if its possible to stop browsers from sending cookies in
14
- // that case
15
- const getAuthorizedFetchConfig = () => token ? {
16
- headers: { Authorization: `Bearer ${token}` },
17
- } : {
18
- credentials: 'include',
19
- };
20
- const getUser = (0, __1.once)(async () => {
21
- return initializer.fetch((await accountsUrl).replace(/\/+$/, '') + '/accounts/api/user', getAuthorizedFetchConfig())
22
- .then(response => response.status === 200 ? response.json() : undefined);
23
- });
24
- return {
25
- getAuthorizedFetchConfig,
26
- getUser
15
+ const queryString = window.location.search;
16
+ const queryKey = 'auth';
17
+ const embeddedQueryValue = 'embedded';
18
+ const authQuery = new URLSearchParams(queryString).get(queryKey);
19
+ const referrer = window.document.referrer ? new URL(window.document.referrer) : undefined;
20
+ const isEmbedded = window.top && window.top !== window;
21
+ const trustedParent = isEmbedded && referrer && referrer.hostname.match(/^(openstax\.org|((.*)(\.openstax\.org|local|localhost)))$/) ? referrer : undefined;
22
+ const trustedEmbeds = new Set();
23
+ let userData = {
24
+ token: [null, embeddedQueryValue].includes(authQuery) ? null : authQuery
25
+ };
26
+ window.addEventListener('message', event => {
27
+ if (event.data.type === PostMessageTypes.RequestUser && trustedEmbeds.has(event.origin)) {
28
+ getUser().then(() => {
29
+ event.source.postMessage({ type: PostMessageTypes.ReceiveUser, userData }, event.origin);
30
+ });
31
+ }
32
+ });
33
+ const getAuthorizedEmbedUrl = (urlString) => {
34
+ const url = new URL(urlString);
35
+ trustedEmbeds.add(url.origin);
36
+ url.searchParams.set(queryKey, embeddedQueryValue);
37
+ return url.href;
38
+ };
39
+ const getAuthorizedLinkUrl = (urlString) => {
40
+ const url = new URL(urlString);
41
+ if (userData.token) {
42
+ url.searchParams.set(queryKey, userData.token);
43
+ }
44
+ return url.href;
45
+ };
46
+ const getAuthorizedUrl = (urlString) => {
47
+ const url = new URL(urlString);
48
+ if (authQuery) {
49
+ url.searchParams.set(queryKey, authQuery);
50
+ }
51
+ return url.href;
52
+ };
53
+ // *note* that this does not actually prevent cookies from being sent on same-origin
54
+ // requests, i'm not sure if its possible to stop browsers from sending cookies in
55
+ // that case
56
+ const getAuthorizedFetchConfig = () => userData.token ? {
57
+ headers: { Authorization: `Bearer ${userData.token}` },
58
+ } : {
59
+ credentials: 'include',
60
+ };
61
+ /*
62
+ * requests user identity from parent window via postMessage
63
+ */
64
+ const getParentWindowUser = () => new Promise((resolve, reject) => {
65
+ if (!window.parent || !trustedParent) {
66
+ return reject(new Error('parent window is undefined or not trusted'));
67
+ }
68
+ const handler = (event) => {
69
+ if (event.data.type === PostMessageTypes.ReceiveUser && event.origin === trustedParent.origin) {
70
+ clearTimeout(timeout);
71
+ window.removeEventListener('message', handler);
72
+ resolve(event.data.userData);
73
+ }
27
74
  };
75
+ window.addEventListener('message', handler);
76
+ window.parent.postMessage({ type: PostMessageTypes.RequestUser }, trustedParent.origin);
77
+ const timeout = setTimeout(() => {
78
+ window.removeEventListener('message', handler);
79
+ reject(new Error('loading user identity timed out'));
80
+ }, 100);
81
+ });
82
+ /*
83
+ * requests user identity from accounts api using given token or cookie
84
+ */
85
+ const getFetchUser = async () => {
86
+ return await window.fetch((await accountsUrl).replace(/\/+$/, '') + '/accounts/api/user', getAuthorizedFetchConfig())
87
+ .then(response => response.status === 200 ? response.json() : undefined)
88
+ .then(user => ({ ...userData, user }));
89
+ };
90
+ const getUser = (0, __1.once)(async () => {
91
+ userData = authQuery === embeddedQueryValue
92
+ ? await getParentWindowUser()
93
+ : await getFetchUser();
94
+ return userData.user;
95
+ });
96
+ return {
97
+ /**
98
+ * adds auth parameters to the url. this is only safe to use when using javascript to navigate
99
+ * within the current window, eg `window.location = 'https://my.otherservice.com';` anchors
100
+ * should use getAuthorizedLinkUrl for their href.
101
+ *
102
+ * result unreliable unless `getUser` is resolved first.
103
+ */
104
+ getAuthorizedUrl,
105
+ /**
106
+ * all link href-s must be rendered with auth tokens so that they work when opened in a new tab
107
+ *
108
+ * result unreliable unless `getUser` is resolved first.
109
+ */
110
+ getAuthorizedLinkUrl,
111
+ /**
112
+ * gets an authorized url for an iframe src. sets params on the url and saves its
113
+ * origin to trust releasing user identity to it
114
+ *
115
+ * result unreliable unless `getUser` is resolved first.
116
+ */
117
+ getAuthorizedEmbedUrl,
118
+ /**
119
+ * gets second argument for `fetch` that has authentication token or cookie
120
+ *
121
+ * result unreliable unless `getUser` is resolved first.
122
+ */
123
+ getAuthorizedFetchConfig,
124
+ /**
125
+ * loads current user identity. does not reflect changes in identity after being called the first time.
126
+ *
127
+ * result unreliable unless `getUser` is resolved first.
128
+ */
129
+ getUser
28
130
  };
29
131
  };
30
132
  exports.browserAuthProvider = browserAuthProvider;