@openstax/ts-utils 1.1.40 → 1.1.43

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.
@@ -10,3 +10,7 @@ export declare class NotFoundError extends Error {
10
10
  static readonly TYPE = "NotFoundError";
11
11
  static matches: (e: any) => e is typeof NotFoundError;
12
12
  }
13
+ export declare class SessionExpiredError extends Error {
14
+ static readonly TYPE = "SessionExpiredError";
15
+ static matches: (e: any) => e is typeof SessionExpiredError;
16
+ }
@@ -24,3 +24,7 @@ export class NotFoundError extends Error {
24
24
  }
25
25
  NotFoundError.TYPE = 'NotFoundError';
26
26
  NotFoundError.matches = errorIsType(NotFoundError);
27
+ export class SessionExpiredError extends Error {
28
+ }
29
+ SessionExpiredError.TYPE = 'SessionExpiredError';
30
+ SessionExpiredError.matches = errorIsType(SessionExpiredError);
@@ -2,6 +2,7 @@ import * as pathToRegexp from 'path-to-regexp';
2
2
  import queryString from 'query-string';
3
3
  import { merge } from '../..';
4
4
  import { resolveConfigValue } from '../../config';
5
+ import { SessionExpiredError, UnauthorizedError } from '../../errors';
5
6
  export const loadResponse = (response) => () => {
6
7
  const [contentType] = (response.headers.get('content-type') || '').split(';');
7
8
  switch (contentType) {
@@ -31,16 +32,24 @@ const makeRouteClient = (initializer, config, route, authProvider) => {
31
32
  ...fetchConfig === null || fetchConfig === void 0 ? void 0 : fetchConfig.headers,
32
33
  ...(body ? { 'content-type': 'application/json' } : {}),
33
34
  }
34
- })).then(response => ({
35
- status: response.status,
36
- acceptStatus: (...status) => {
37
- if (!status.includes(response.status)) {
38
- throw new Error('unexpected response from api');
39
- }
40
- return { status: response.status, load: loadResponse(response) };
41
- },
42
- load: loadResponse(response),
43
- }));
35
+ })).then(response => {
36
+ if (response.status === 401) {
37
+ throw new UnauthorizedError();
38
+ }
39
+ if (response.status === 440) {
40
+ throw new SessionExpiredError();
41
+ }
42
+ return {
43
+ status: response.status,
44
+ acceptStatus: (...status) => {
45
+ if (!status.includes(response.status)) {
46
+ throw new Error('unexpected response from api');
47
+ }
48
+ return { status: response.status, load: loadResponse(response) };
49
+ },
50
+ load: loadResponse(response),
51
+ };
52
+ });
44
53
  };
45
54
  routeClient.renderUrl = renderUrl;
46
55
  return routeClient;
@@ -1,6 +1,7 @@
1
1
  import * as crypto from 'crypto';
2
2
  import { TextEncoder } from 'util';
3
3
  import jwt from 'jsonwebtoken';
4
+ import { SessionExpiredError } from '../../../errors';
4
5
  import { isPlainObject } from '../../../guards';
5
6
  const decrypt = (input, key) => {
6
7
  const splitInput = input.split('.');
@@ -22,7 +23,7 @@ export const decryptAndVerify = (token, encryptionPrivateKey, signaturePublicKey
22
23
  // Decrypt SSO cookie
23
24
  const plaintext = decrypt(token, encryptionPrivateKey);
24
25
  const payload = jwt.verify(plaintext, signaturePublicKey, {
25
- clockTolerance: 300 // 5 minutes
26
+ clockTolerance: 300 // 5 minutes
26
27
  });
27
28
  if (!isPlainObject(payload) || !isPlainObject(payload.sub) || !payload.sub.uuid) {
28
29
  return undefined;
@@ -30,7 +31,10 @@ export const decryptAndVerify = (token, encryptionPrivateKey, signaturePublicKey
30
31
  // TS is confused because the library types the `sub` as a string
31
32
  return payload.sub;
32
33
  }
33
- catch {
34
+ catch (err) {
35
+ if (err instanceof jwt.TokenExpiredError) {
36
+ throw new SessionExpiredError();
37
+ }
34
38
  return undefined;
35
39
  }
36
40
  };
@@ -186,8 +186,7 @@ export const createAttemptStatement = (activity, parentActivity) => {
186
186
  };
187
187
  /* resolves with the statement id */
188
188
  export const putAttemptStatement = async (gateway, activity, parentActivity) => {
189
- return await gateway.putXapiStatements([createAttemptStatement(activity, parentActivity)])
190
- .then(statements => statements[0]);
189
+ return (await gateway.putXapiStatements([createAttemptStatement(activity, parentActivity)]))[0];
191
190
  };
192
191
  /*
193
192
  * creates a statement under the given attempt.
@@ -209,8 +208,7 @@ export const createAttemptActivityStatement = (attemptStatement, verb, result) =
209
208
  };
210
209
  };
211
210
  export const putAttemptActivityStatement = async (gateway, attemptStatement, verb, result) => {
212
- return await gateway.putXapiStatements([createAttemptActivityStatement(attemptStatement, verb, result)])
213
- .then(statements => statements[0]);
211
+ return (await gateway.putXapiStatements([createAttemptActivityStatement(attemptStatement, verb, result)]))[0];
214
212
  };
215
213
  /*
216
214
  * creates a statement that completes the given attempt.
@@ -246,6 +244,5 @@ export const createCompletedStatement = (attemptStatement, result) => {
246
244
  };
247
245
  };
248
246
  export const putCompletedStatement = async (gateway, attemptStatement, result) => {
249
- return await gateway.putXapiStatements([createCompletedStatement(attemptStatement, result)])
250
- .then(statements => statements[0]);
247
+ return (await gateway.putXapiStatements([createCompletedStatement(attemptStatement, result)]))[0];
251
248
  };
@@ -11,6 +11,7 @@ export const lrsGateway = (initializer) => (configProvider) => {
11
11
  const lrsHost = once(() => resolveConfigValue(config.lrsHost));
12
12
  const lrsAuthorization = once(() => resolveConfigValue(config.lrsAuthorization));
13
13
  return (authProvider) => {
14
+ // Note: This method actually uses POST
14
15
  const putXapiStatements = async (statements) => {
15
16
  const user = assertDefined(await authProvider.getUser(), new UnauthorizedError);
16
17
  const statementsWithDefaults = statements.map(statement => ({
@@ -24,7 +25,7 @@ export const lrsGateway = (initializer) => (configProvider) => {
24
25
  },
25
26
  timestamp: formatISO(new Date())
26
27
  }));
27
- return initializer.fetch((await lrsHost()).replace(/\/+$/, '') + '/data/xAPI/statements', {
28
+ const response = await initializer.fetch((await lrsHost()).replace(/\/+$/, '') + '/data/xAPI/statements', {
28
29
  body: JSON.stringify(statementsWithDefaults),
29
30
  headers: {
30
31
  Authorization: await lrsAuthorization(),
@@ -32,42 +33,50 @@ export const lrsGateway = (initializer) => (configProvider) => {
32
33
  'X-Experience-API-Version': '1.0.0',
33
34
  },
34
35
  method: METHOD.POST,
35
- })
36
- .then(response => response.json())
37
- .then(ids => ids.map((id, index) => ({ id, ...statementsWithDefaults[index] })));
36
+ });
37
+ if (![200, 201].includes(response.status)) {
38
+ throw new Error(`Unexpected LRS POST statements response code ${response.status} with body:
39
+
40
+ ${await response.text()}`);
41
+ }
42
+ const ids = await response.json();
43
+ return ids.map((id, index) => ({ id, ...statementsWithDefaults[index] }));
38
44
  };
39
- const getMoreXapiStatements = async (more) => {
40
- return initializer.fetch((await lrsHost()).replace(/\/+$/, '') + more, {
41
- headers: {
42
- Authorization: await lrsAuthorization(),
43
- 'X-Experience-API-Version': '1.0.0',
44
- },
45
- })
46
- .then(response => response.json())
47
- .then(json => json);
45
+ // Note: This code does not currently handle a single statement response,
46
+ // which can return 404 if the statement is not found
47
+ const formatGetXapiStatementsResponse = async (responsePromise) => {
48
+ const response = await responsePromise;
49
+ if (response.status !== 200) {
50
+ throw new Error(`Unexpected LRS GET statements response code ${response.status} with body:
51
+
52
+ ${await response.text()}`);
53
+ }
54
+ return response.json();
48
55
  };
49
- const getXapiStatements = async ({ user, anyUser, ...options }) => {
50
- return initializer.fetch((await lrsHost()).replace(/\/+$/, '') + '/data/xAPI/statements?' + queryString.stringify({
51
- ...options,
52
- ...(anyUser === true ? {} : {
53
- agent: JSON.stringify({
54
- account: {
55
- homePage: 'https://openstax.org',
56
- name: user || assertDefined(await authProvider.getUser(), new UnauthorizedError()).uuid,
57
- },
58
- objectType: 'Agent',
59
- }),
60
- })
61
- }), {
62
- headers: {
63
- Authorization: await lrsAuthorization(),
64
- 'X-Experience-API-Version': '1.0.0',
65
- },
56
+ const getMoreXapiStatements = async (more) => formatGetXapiStatementsResponse(initializer.fetch((await lrsHost()).replace(/\/+$/, '') + more, {
57
+ headers: {
58
+ Authorization: await lrsAuthorization(),
59
+ 'X-Experience-API-Version': '1.0.0',
60
+ },
61
+ }));
62
+ const getXapiStatements = async ({ user, anyUser, ...options }) => formatGetXapiStatementsResponse(initializer.fetch((await lrsHost()).replace(/\/+$/, '') + '/data/xAPI/statements?' + queryString.stringify({
63
+ ...options,
64
+ ...(anyUser === true ? {} : {
65
+ agent: JSON.stringify({
66
+ account: {
67
+ homePage: 'https://openstax.org',
68
+ name: user || assertDefined(await authProvider.getUser(), new UnauthorizedError()).uuid,
69
+ },
70
+ objectType: 'Agent',
71
+ }),
66
72
  })
67
- .then(response => response.json())
68
- .then(json => json);
69
- };
70
- const getAllXapiStatements = (...args) => {
73
+ }), {
74
+ headers: {
75
+ Authorization: await lrsAuthorization(),
76
+ 'X-Experience-API-Version': '1.0.0',
77
+ },
78
+ }));
79
+ const getAllXapiStatements = async (...args) => {
71
80
  const loadRemaining = async (result) => {
72
81
  if (!result.more) {
73
82
  return result.statements;
@@ -75,7 +84,7 @@ export const lrsGateway = (initializer) => (configProvider) => {
75
84
  const { more, statements } = await getMoreXapiStatements(result.more);
76
85
  return loadRemaining({ more, statements: [...result.statements, ...statements] });
77
86
  };
78
- return getXapiStatements(...args).then(loadRemaining);
87
+ return loadRemaining(await getXapiStatements(...args));
79
88
  };
80
89
  return {
81
90
  putXapiStatements,
@@ -10,6 +10,10 @@ declare type Field = {
10
10
  key: string;
11
11
  type: 'keyword';
12
12
  };
13
+ export interface IndexOptions<T> {
14
+ body: T;
15
+ id: string;
16
+ }
13
17
  export interface SearchOptions {
14
18
  page?: number;
15
19
  query: string | undefined;
@@ -1,7 +1,8 @@
1
- import { SearchOptions } from '.';
1
+ import { IndexOptions, SearchOptions } from '.';
2
2
  export declare const memorySearchTheBadWay: <T>({ loadAllDocumentsTheBadWay }: {
3
3
  loadAllDocumentsTheBadWay: () => Promise<T[]>;
4
4
  }) => {
5
+ index: (_options: IndexOptions<T>) => Promise<undefined>;
5
6
  search: (options: SearchOptions) => Promise<{
6
7
  items: T[];
7
8
  pageSize: number;
@@ -5,6 +5,7 @@ const MAX_RESULTS = 10;
5
5
  const resolveField = (document, field) => field.key.toString().split('.').reduce((result, key) => result[key], document);
6
6
  export const memorySearchTheBadWay = ({ loadAllDocumentsTheBadWay }) => {
7
7
  return {
8
+ index: async (_options) => undefined,
8
9
  search: async (options) => {
9
10
  const results = (await loadAllDocumentsTheBadWay())
10
11
  .map(document => {