@openstax/ts-utils 1.1.39 → 1.1.42
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/lambdaParameterConfig.js +1 -1
- package/dist/cjs/errors.d.ts +4 -0
- package/dist/cjs/errors.js +6 -1
- package/dist/cjs/services/apiGateway/index.js +19 -10
- package/dist/cjs/services/authProvider/utils/decryptAndVerify.js +6 -2
- package/dist/cjs/services/lrsGateway/attempt-utils.js +3 -6
- package/dist/cjs/services/lrsGateway/index.js +44 -35
- package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
- package/dist/esm/config/lambdaParameterConfig.js +1 -1
- package/dist/esm/errors.d.ts +4 -0
- package/dist/esm/errors.js +4 -0
- package/dist/esm/services/apiGateway/index.js +19 -10
- package/dist/esm/services/authProvider/utils/decryptAndVerify.js +6 -2
- package/dist/esm/services/lrsGateway/attempt-utils.js +3 -6
- package/dist/esm/services/lrsGateway/index.js +44 -35
- package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@ export const lambdaParameterConfig = (parameterName) => async () => {
|
|
|
13
13
|
if (!lambdaExtensionReadyPromise) {
|
|
14
14
|
// This request will return 400 Bad Request,
|
|
15
15
|
// but we only care that it'll block until the extension is ready
|
|
16
|
-
lambdaExtensionReadyPromise = fetch(lambdaExtensionUrl);
|
|
16
|
+
lambdaExtensionReadyPromise = retryWithDelay(() => fetch(lambdaExtensionUrl));
|
|
17
17
|
}
|
|
18
18
|
await lambdaExtensionReadyPromise;
|
|
19
19
|
const resp = await retryWithDelay(() => fetch(
|
package/dist/esm/errors.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/dist/esm/errors.js
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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)
|
|
87
|
+
return loadRemaining(await getXapiStatements(...args));
|
|
79
88
|
};
|
|
80
89
|
return {
|
|
81
90
|
putXapiStatements,
|