@lti-tool/core 0.9.0 → 0.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/CHANGELOG.md +12 -0
- package/dist/ltiTool.d.ts +88 -3
- package/dist/ltiTool.d.ts.map +1 -1
- package/dist/ltiTool.js +157 -3
- package/dist/schemas/client.schema.d.ts +1 -1
- package/dist/schemas/client.schema.d.ts.map +1 -1
- package/dist/schemas/client.schema.js +1 -1
- package/dist/schemas/common.schema.d.ts +1 -1
- package/dist/schemas/common.schema.d.ts.map +1 -1
- package/dist/schemas/common.schema.js +1 -1
- package/dist/schemas/deployment.schema.d.ts +1 -1
- package/dist/schemas/deployment.schema.d.ts.map +1 -1
- package/dist/schemas/deployment.schema.js +1 -1
- package/dist/schemas/lti13/ags/lineItem.schema.d.ts +80 -0
- package/dist/schemas/lti13/ags/lineItem.schema.d.ts.map +1 -0
- package/dist/schemas/lti13/ags/lineItem.schema.js +49 -0
- package/dist/schemas/lti13/ags/result.schema.d.ts +65 -0
- package/dist/schemas/lti13/ags/result.schema.d.ts.map +1 -0
- package/dist/schemas/lti13/ags/result.schema.js +35 -0
- package/dist/schemas/lti13/ags/scoreSubmission.schema.d.ts +25 -4
- package/dist/schemas/lti13/ags/scoreSubmission.schema.d.ts.map +1 -1
- package/dist/schemas/lti13/ags/scoreSubmission.schema.js +2 -1
- package/dist/schemas/lti13/claims/baseJwtClaims.schema.d.ts +1 -1
- package/dist/schemas/lti13/claims/baseJwtClaims.schema.d.ts.map +1 -1
- package/dist/schemas/lti13/claims/baseJwtClaims.schema.js +1 -1
- package/dist/schemas/lti13/claims/contextClaims.schema.d.ts +1 -1
- package/dist/schemas/lti13/claims/contextClaims.schema.d.ts.map +1 -1
- package/dist/schemas/lti13/claims/contextClaims.schema.js +1 -1
- package/dist/schemas/lti13/claims/coreLtiClaims.schema.d.ts +1 -1
- package/dist/schemas/lti13/claims/coreLtiClaims.schema.d.ts.map +1 -1
- package/dist/schemas/lti13/claims/coreLtiClaims.schema.js +1 -1
- package/dist/schemas/lti13/claims/platformClaims.schema.d.ts +1 -1
- package/dist/schemas/lti13/claims/platformClaims.schema.d.ts.map +1 -1
- package/dist/schemas/lti13/claims/platformClaims.schema.js +1 -1
- package/dist/schemas/lti13/claims/privacyClaims.schema.d.ts +1 -1
- package/dist/schemas/lti13/claims/privacyClaims.schema.d.ts.map +1 -1
- package/dist/schemas/lti13/claims/privacyClaims.schema.js +1 -1
- package/dist/schemas/lti13/claims/serviceClaims.schema.d.ts +1 -1
- package/dist/schemas/lti13/claims/serviceClaims.schema.d.ts.map +1 -1
- package/dist/schemas/lti13/claims/serviceClaims.schema.js +1 -1
- package/dist/schemas/lti13/lti13JwtPayload.schema.d.ts +1 -1
- package/dist/schemas/lti13/lti13JwtPayload.schema.d.ts.map +1 -1
- package/dist/schemas/lti13/lti13JwtPayload.schema.js +1 -1
- package/dist/schemas/lti13/lti13Launch.schema.d.ts +1 -1
- package/dist/schemas/lti13/lti13Launch.schema.d.ts.map +1 -1
- package/dist/schemas/lti13/lti13Launch.schema.js +1 -1
- package/dist/schemas/lti13/lti13Login.schema.d.ts +1 -1
- package/dist/schemas/lti13/lti13Login.schema.d.ts.map +1 -1
- package/dist/schemas/lti13/lti13Login.schema.js +1 -1
- package/dist/schemas/lti13/nrps/contextMembership.schema.d.ts +65 -0
- package/dist/schemas/lti13/nrps/contextMembership.schema.d.ts.map +1 -0
- package/dist/schemas/lti13/nrps/contextMembership.schema.js +47 -0
- package/dist/services/ags.service.d.ts +86 -0
- package/dist/services/ags.service.d.ts.map +1 -1
- package/dist/services/ags.service.js +184 -8
- package/dist/services/nrps.service.d.ts +28 -0
- package/dist/services/nrps.service.d.ts.map +1 -0
- package/dist/services/nrps.service.js +51 -0
- package/dist/services/token.service.d.ts.map +1 -1
- package/dist/services/token.service.js +2 -1
- package/package.json +1 -1
- package/src/ltiTool.ts +195 -5
- package/src/schemas/client.schema.ts +1 -1
- package/src/schemas/common.schema.ts +1 -1
- package/src/schemas/deployment.schema.ts +1 -1
- package/src/schemas/lti13/ags/lineItem.schema.ts +85 -0
- package/src/schemas/lti13/ags/result.schema.ts +55 -0
- package/src/schemas/lti13/ags/scoreSubmission.schema.ts +3 -1
- package/src/schemas/lti13/claims/baseJwtClaims.schema.ts +1 -1
- package/src/schemas/lti13/claims/contextClaims.schema.ts +1 -1
- package/src/schemas/lti13/claims/coreLtiClaims.schema.ts +1 -1
- package/src/schemas/lti13/claims/platformClaims.schema.ts +1 -1
- package/src/schemas/lti13/claims/privacyClaims.schema.ts +1 -1
- package/src/schemas/lti13/claims/serviceClaims.schema.ts +1 -1
- package/src/schemas/lti13/lti13JwtPayload.schema.ts +1 -1
- package/src/schemas/lti13/lti13Launch.schema.ts +1 -1
- package/src/schemas/lti13/lti13Login.schema.ts +1 -1
- package/src/schemas/lti13/nrps/contextMembership.schema.ts +55 -0
- package/src/services/ags.service.ts +253 -16
- package/src/services/nrps.service.ts +80 -0
- package/src/services/token.service.ts +4 -1
|
@@ -37,11 +37,7 @@ export class AGSService {
|
|
|
37
37
|
if (!session.services?.ags?.lineitem) {
|
|
38
38
|
throw new Error('AGS not available for this session');
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
const launchConfig = await getValidLaunchConfig(this.storage, session.platform.issuer, session.platform.clientId, session.platform.deploymentId);
|
|
42
|
-
const token = await this.tokenService.getBearerToken(session.platform.clientId,
|
|
43
|
-
// Need to get token URL from platform storage
|
|
44
|
-
launchConfig.tokenUrl, 'https://purl.imsglobal.org/spec/lti-ags/scope/score');
|
|
40
|
+
const token = await this.getAGSToken(session, 'https://purl.imsglobal.org/spec/lti-ags/scope/score');
|
|
45
41
|
const scorePayload = {
|
|
46
42
|
userId: score.userId,
|
|
47
43
|
scoreGiven: score.scoreGiven,
|
|
@@ -59,11 +55,191 @@ export class AGSService {
|
|
|
59
55
|
},
|
|
60
56
|
body: JSON.stringify(scorePayload),
|
|
61
57
|
});
|
|
58
|
+
await this.validateAGSResponse(response, 'score submission');
|
|
59
|
+
return response;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Retrieves all scores for a specific line item from the platform using Assignment and Grade Services.
|
|
63
|
+
*
|
|
64
|
+
* @param session - Active LTI session containing AGS line item endpoint configuration
|
|
65
|
+
* @returns Promise resolving to the HTTP response containing scores data for the line item
|
|
66
|
+
* @throws {Error} When AGS line item service is not available for the session or request fails
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* const response = await agsService.getScores(session);
|
|
71
|
+
* const scores = await response.json();
|
|
72
|
+
* console.log('All scores for this line item:', scores);
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
async getScores(session) {
|
|
76
|
+
if (!session.services?.ags?.lineitem) {
|
|
77
|
+
throw new Error('AGS line item not available for this session');
|
|
78
|
+
}
|
|
79
|
+
const token = await this.getAGSToken(session, 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly');
|
|
80
|
+
// cleanse the results URL
|
|
81
|
+
// we cannot include a search / query param
|
|
82
|
+
const lineItemUrl = new URL(session.services.ags.lineitem);
|
|
83
|
+
lineItemUrl.search = '';
|
|
84
|
+
const resultsUrl = `${lineItemUrl.toString()}/results`;
|
|
85
|
+
const response = await fetch(resultsUrl, {
|
|
86
|
+
method: 'GET',
|
|
87
|
+
headers: {
|
|
88
|
+
Authorization: `Bearer ${token}`,
|
|
89
|
+
Accept: 'application/vnd.ims.lis.v2.resultcontainer+json',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
await this.validateAGSResponse(response, 'get scores');
|
|
93
|
+
return response;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Retrieves line items (gradebook columns) from the platform using Assignment and Grade Services.
|
|
97
|
+
*
|
|
98
|
+
* @param session - Active LTI session containing AGS line items endpoint configuration
|
|
99
|
+
* @returns Promise resolving to the HTTP response containing line items data
|
|
100
|
+
* @throws {Error} When AGS line items service is not available for the session or request fails
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* const response = await agsService.listLineItems(session);
|
|
105
|
+
* const lineItems = await response.json();
|
|
106
|
+
* console.log('Available gradebook columns:', lineItems);
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
async listLineItems(session) {
|
|
110
|
+
if (!session.services?.ags?.lineitems) {
|
|
111
|
+
throw new Error('AGS list line items not available for this session');
|
|
112
|
+
}
|
|
113
|
+
const token = await this.getAGSToken(session, 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly');
|
|
114
|
+
const response = await fetch(`${session.services.ags.lineitems}`, {
|
|
115
|
+
method: 'GET',
|
|
116
|
+
headers: {
|
|
117
|
+
Authorization: `Bearer ${token}`,
|
|
118
|
+
Accept: 'application/vnd.ims.lis.v2.lineitemcontainer+json',
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
await this.validateAGSResponse(response, 'list line items');
|
|
122
|
+
return response;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Retrieves a specific line item (gradebook column) from the platform using Assignment and Grade Services.
|
|
126
|
+
*
|
|
127
|
+
* @param session - Active LTI session containing AGS line item endpoint configuration
|
|
128
|
+
* @returns Promise resolving to the HTTP response containing the line item data
|
|
129
|
+
* @throws {Error} When AGS line item service is not available for the session or request fails
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* const response = await agsService.getLineItem(session);
|
|
134
|
+
* const lineItem = await response.json();
|
|
135
|
+
* console.log('Line item details:', lineItem);
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
async getLineItem(session) {
|
|
139
|
+
if (!session.services?.ags?.lineitem) {
|
|
140
|
+
throw new Error('AGS line item not available for this session');
|
|
141
|
+
}
|
|
142
|
+
const token = await this.getAGSToken(session, 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly');
|
|
143
|
+
const response = await fetch(`${session.services.ags.lineitem}`, {
|
|
144
|
+
method: 'GET',
|
|
145
|
+
headers: {
|
|
146
|
+
Authorization: `Bearer ${token}`,
|
|
147
|
+
Accept: 'application/vnd.ims.lis.v2.lineitem+json',
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
await this.validateAGSResponse(response, 'get line item');
|
|
151
|
+
return response;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Creates a new line item (gradebook column) on the platform using Assignment and Grade Services.
|
|
155
|
+
*
|
|
156
|
+
* @param session - Active LTI session containing AGS line items endpoint configuration
|
|
157
|
+
* @param createLineItem - Line item data including label, scoreMaximum, and optional metadata
|
|
158
|
+
* @returns Promise resolving to the HTTP response containing the created line item with generated ID
|
|
159
|
+
* @throws {Error} When AGS line item creation service is not available for the session or creation fails
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const response = await agsService.createLineItem(session, {
|
|
164
|
+
* label: 'Quiz 1',
|
|
165
|
+
* scoreMaximum: 100,
|
|
166
|
+
* tag: 'quiz',
|
|
167
|
+
* resourceId: 'quiz-001'
|
|
168
|
+
* });
|
|
169
|
+
* const newLineItem = await response.json();
|
|
170
|
+
* console.log('Created line item:', newLineItem.id);
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
async createLineItem(session, createLineItem) {
|
|
174
|
+
if (!session.services?.ags?.lineitems) {
|
|
175
|
+
throw new Error('AGS create line items not available for this session');
|
|
176
|
+
}
|
|
177
|
+
const token = await this.getAGSToken(session, 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem');
|
|
178
|
+
const response = await fetch(`${session.services.ags.lineitems}`, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: {
|
|
181
|
+
Authorization: `Bearer ${token}`,
|
|
182
|
+
'Content-Type': 'application/vnd.ims.lis.v2.lineitem+json',
|
|
183
|
+
},
|
|
184
|
+
body: JSON.stringify(createLineItem),
|
|
185
|
+
});
|
|
186
|
+
await this.validateAGSResponse(response, 'create line item');
|
|
187
|
+
return response;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Updates an existing line item (gradebook column) on the platform using Assignment and Grade Services.
|
|
191
|
+
*
|
|
192
|
+
* @param session - Active LTI session containing AGS line item endpoint configuration
|
|
193
|
+
* @param updateLineItem - Updated line item data including all required fields
|
|
194
|
+
* @returns Promise resolving to the HTTP response containing the updated line item
|
|
195
|
+
* @throws {Error} When AGS line item service is not available for the session or update fails
|
|
196
|
+
*/
|
|
197
|
+
async updateLineItem(session, updateLineItem) {
|
|
198
|
+
if (!session.services?.ags?.lineitem) {
|
|
199
|
+
throw new Error('AGS line item not available for this session');
|
|
200
|
+
}
|
|
201
|
+
const token = await this.getAGSToken(session, 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem');
|
|
202
|
+
const response = await fetch(session.services.ags.lineitem, {
|
|
203
|
+
method: 'PUT',
|
|
204
|
+
headers: {
|
|
205
|
+
Authorization: `Bearer ${token}`,
|
|
206
|
+
'Content-Type': 'application/vnd.ims.lis.v2.lineitem+json',
|
|
207
|
+
},
|
|
208
|
+
body: JSON.stringify(updateLineItem),
|
|
209
|
+
});
|
|
210
|
+
await this.validateAGSResponse(response, 'update line item');
|
|
211
|
+
return response;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Deletes a line item (gradebook column) from the platform using Assignment and Grade Services.
|
|
215
|
+
*
|
|
216
|
+
* @param session - Active LTI session containing AGS line item endpoint configuration
|
|
217
|
+
* @returns Promise resolving to the HTTP response (typically 204 No Content on success)
|
|
218
|
+
* @throws {Error} When AGS line item service is not available for the session or deletion fails
|
|
219
|
+
*/
|
|
220
|
+
async deleteLineItem(session) {
|
|
221
|
+
if (!session.services?.ags?.lineitem) {
|
|
222
|
+
throw new Error('AGS line item not available for this session');
|
|
223
|
+
}
|
|
224
|
+
const token = await this.getAGSToken(session, 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem');
|
|
225
|
+
const response = await fetch(session.services.ags.lineitem, {
|
|
226
|
+
method: 'DELETE',
|
|
227
|
+
headers: {
|
|
228
|
+
Authorization: `Bearer ${token}`,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
await this.validateAGSResponse(response, 'delete line item');
|
|
232
|
+
return response;
|
|
233
|
+
}
|
|
234
|
+
async getAGSToken(session, scope) {
|
|
235
|
+
const launchConfig = await getValidLaunchConfig(this.storage, session.platform.issuer, session.platform.clientId, session.platform.deploymentId);
|
|
236
|
+
return this.tokenService.getBearerToken(session.platform.clientId, launchConfig.tokenUrl, scope);
|
|
237
|
+
}
|
|
238
|
+
async validateAGSResponse(response, operation) {
|
|
62
239
|
if (!response.ok) {
|
|
63
240
|
const error = await response.json();
|
|
64
|
-
this.logger.error({ error, status: response.status, statusText: response.statusText },
|
|
65
|
-
throw new Error(`AGS
|
|
241
|
+
this.logger.error({ error, status: response.status, statusText: response.statusText }, `AGS ${operation} failed`);
|
|
242
|
+
throw new Error(`AGS ${operation} failed: ${response.statusText} ${error}`);
|
|
66
243
|
}
|
|
67
|
-
return response;
|
|
68
244
|
}
|
|
69
245
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BaseLogger } from 'pino';
|
|
2
|
+
import type { LTISession } from '../interfaces/ltiSession.js';
|
|
3
|
+
import type { LTIStorage } from '../interfaces/ltiStorage.js';
|
|
4
|
+
import type { TokenService } from './token.service.js';
|
|
5
|
+
/**
|
|
6
|
+
* Names and Role Provisioning Services (NRPS) implementation for LTI 1.3.
|
|
7
|
+
* Provides methods to retrieve course membership and user information from the platform.
|
|
8
|
+
*
|
|
9
|
+
* @see https://www.imsglobal.org/spec/lti-nrps/v2p0
|
|
10
|
+
*/
|
|
11
|
+
export declare class NRPSService {
|
|
12
|
+
private tokenService;
|
|
13
|
+
private storage;
|
|
14
|
+
private logger;
|
|
15
|
+
constructor(tokenService: TokenService, storage: LTIStorage, logger: BaseLogger);
|
|
16
|
+
/**
|
|
17
|
+
* Retrieves all members (users) in the current course/context from the platform.
|
|
18
|
+
* Returns raw response that should be parsed by the calling service.
|
|
19
|
+
*
|
|
20
|
+
* @param session - Active LTI session containing NRPS service endpoints
|
|
21
|
+
* @returns Raw HTTP response containing membership data
|
|
22
|
+
* @throws {Error} When NRPS is not available for this session or request fails
|
|
23
|
+
*/
|
|
24
|
+
getMembers(session: LTISession): Promise<Response>;
|
|
25
|
+
private getNRPSToken;
|
|
26
|
+
private validateNRPSResponse;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=nrps.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nrps.service.d.ts","sourceRoot":"","sources":["../../src/services/nrps.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAEvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAG9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;GAKG;AACH,qBAAa,WAAW;IAEpB,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,MAAM;gBAFN,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,UAAU;IAG5B;;;;;;;OAOG;IACG,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC;YAsB1C,YAAY;YAeZ,oBAAoB;CAanC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getValidLaunchConfig } from '../utils/launchConfigValidation.js';
|
|
2
|
+
/**
|
|
3
|
+
* Names and Role Provisioning Services (NRPS) implementation for LTI 1.3.
|
|
4
|
+
* Provides methods to retrieve course membership and user information from the platform.
|
|
5
|
+
*
|
|
6
|
+
* @see https://www.imsglobal.org/spec/lti-nrps/v2p0
|
|
7
|
+
*/
|
|
8
|
+
export class NRPSService {
|
|
9
|
+
tokenService;
|
|
10
|
+
storage;
|
|
11
|
+
logger;
|
|
12
|
+
constructor(tokenService, storage, logger) {
|
|
13
|
+
this.tokenService = tokenService;
|
|
14
|
+
this.storage = storage;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Retrieves all members (users) in the current course/context from the platform.
|
|
19
|
+
* Returns raw response that should be parsed by the calling service.
|
|
20
|
+
*
|
|
21
|
+
* @param session - Active LTI session containing NRPS service endpoints
|
|
22
|
+
* @returns Raw HTTP response containing membership data
|
|
23
|
+
* @throws {Error} When NRPS is not available for this session or request fails
|
|
24
|
+
*/
|
|
25
|
+
async getMembers(session) {
|
|
26
|
+
if (!session.services?.nrps?.membershipUrl) {
|
|
27
|
+
throw new Error('NRPS not available for this session');
|
|
28
|
+
}
|
|
29
|
+
const token = await this.getNRPSToken(session, 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly');
|
|
30
|
+
const response = await fetch(session.services.nrps.membershipUrl, {
|
|
31
|
+
method: 'GET',
|
|
32
|
+
headers: {
|
|
33
|
+
Authorization: `Bearer ${token}`,
|
|
34
|
+
Accept: 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
await this.validateNRPSResponse(response, 'get members');
|
|
38
|
+
return response;
|
|
39
|
+
}
|
|
40
|
+
async getNRPSToken(session, scope) {
|
|
41
|
+
const launchConfig = await getValidLaunchConfig(this.storage, session.platform.issuer, session.platform.clientId, session.platform.deploymentId);
|
|
42
|
+
return this.tokenService.getBearerToken(session.platform.clientId, launchConfig.tokenUrl, scope);
|
|
43
|
+
}
|
|
44
|
+
async validateNRPSResponse(response, operation) {
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const error = await response.json();
|
|
47
|
+
this.logger.error({ error, status: response.status, statusText: response.statusText }, `NRPS ${operation} failed`);
|
|
48
|
+
throw new Error(`NRPS ${operation} failed: ${response.statusText} ${error}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token.service.d.ts","sourceRoot":"","sources":["../../src/services/token.service.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,qBAAa,YAAY;IAQrB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,KAAK;IARf;;;;;OAKG;gBAEO,OAAO,EAAE,aAAa,EACtB,KAAK,SAAS;IAGxB;;;;;;OAMG;IACG,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBhF;;;;;;;OAOG;IACG,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"token.service.d.ts","sourceRoot":"","sources":["../../src/services/token.service.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,qBAAa,YAAY;IAQrB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,KAAK;IARf;;;;;OAKG;gBAEO,OAAO,EAAE,aAAa,EACtB,KAAK,SAAS;IAGxB;;;;;;OAMG;IACG,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBhF;;;;;;;OAOG;IACG,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC;CA6BnB"}
|
|
@@ -63,7 +63,8 @@ export class TokenService {
|
|
|
63
63
|
}),
|
|
64
64
|
});
|
|
65
65
|
if (!response.ok) {
|
|
66
|
-
|
|
66
|
+
const errorDetail = await response.json();
|
|
67
|
+
throw new Error(`Token request failed: ${response.status} ${response.statusText} ${errorDetail}`);
|
|
67
68
|
}
|
|
68
69
|
const tokenData = await response.json();
|
|
69
70
|
if (!tokenData.access_token) {
|
package/package.json
CHANGED
package/src/ltiTool.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRemoteJWKSet, decodeJwt, exportJWK, jwtVerify, SignJWT } from 'jose';
|
|
2
2
|
import type { Logger } from 'pino';
|
|
3
3
|
|
|
4
4
|
import type { JWKS } from './interfaces/jwks.js';
|
|
@@ -14,8 +14,22 @@ import {
|
|
|
14
14
|
SessionIdSchema,
|
|
15
15
|
VerifyLaunchParamsSchema,
|
|
16
16
|
} from './schemas/index.js';
|
|
17
|
-
import
|
|
17
|
+
import {
|
|
18
|
+
type CreateLineItem,
|
|
19
|
+
type LineItem,
|
|
20
|
+
type LineItems,
|
|
21
|
+
LineItemSchema,
|
|
22
|
+
LineItemsSchema,
|
|
23
|
+
type UpdateLineItem,
|
|
24
|
+
} from './schemas/lti13/ags/lineItem.schema.js';
|
|
25
|
+
import { type Results, ResultsSchema } from './schemas/lti13/ags/result.schema.js';
|
|
26
|
+
import { type ScoreSubmission } from './schemas/lti13/ags/scoreSubmission.schema.js';
|
|
27
|
+
import {
|
|
28
|
+
type Member,
|
|
29
|
+
NRPSContextMembershipResponseSchema,
|
|
30
|
+
} from './schemas/lti13/nrps/contextMembership.schema.js';
|
|
18
31
|
import { AGSService } from './services/ags.service.js';
|
|
32
|
+
import { NRPSService } from './services/nrps.service.js';
|
|
19
33
|
import { createSession } from './services/session.service.js';
|
|
20
34
|
import { TokenService } from './services/token.service.js';
|
|
21
35
|
import { getValidLaunchConfig } from './utils/launchConfigValidation.js';
|
|
@@ -49,6 +63,7 @@ export class LTITool {
|
|
|
49
63
|
private logger: Logger;
|
|
50
64
|
private tokenService: TokenService;
|
|
51
65
|
private agsService: AGSService;
|
|
66
|
+
private nrpsService: NRPSService;
|
|
52
67
|
|
|
53
68
|
/**
|
|
54
69
|
* Creates a new LTI Tool instance.
|
|
@@ -70,6 +85,11 @@ export class LTITool {
|
|
|
70
85
|
this.config.security?.keyId ?? 'main',
|
|
71
86
|
);
|
|
72
87
|
this.agsService = new AGSService(this.tokenService, this.config.storage, this.logger);
|
|
88
|
+
this.nrpsService = new NRPSService(
|
|
89
|
+
this.tokenService,
|
|
90
|
+
this.config.storage,
|
|
91
|
+
this.logger,
|
|
92
|
+
);
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
/**
|
|
@@ -259,17 +279,187 @@ export class LTITool {
|
|
|
259
279
|
*
|
|
260
280
|
* @param session - Active LTI session containing AGS service endpoints
|
|
261
281
|
* @param score - Score submission data including grade value and user ID
|
|
262
|
-
* @returns Result of the score submission
|
|
263
282
|
* @throws {Error} When AGS is not available or submission fails
|
|
264
283
|
*/
|
|
265
|
-
async submitScore(session: LTISession, score: ScoreSubmission): Promise<
|
|
284
|
+
async submitScore(session: LTISession, score: ScoreSubmission): Promise<void> {
|
|
266
285
|
if (!session) {
|
|
267
286
|
throw new Error('session is required');
|
|
268
287
|
}
|
|
269
288
|
if (!score) {
|
|
270
289
|
throw new Error('score is required');
|
|
271
290
|
}
|
|
272
|
-
|
|
291
|
+
|
|
292
|
+
await this.agsService.submitScore(session, score);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Retrieves all scores for a specific line item from the platform using Assignment and Grade Services (AGS).
|
|
297
|
+
*
|
|
298
|
+
* @param session - Active LTI session containing AGS service endpoints
|
|
299
|
+
* @returns Array of score submissions for the line item
|
|
300
|
+
* @throws {Error} When AGS is not available or request fails
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```typescript
|
|
304
|
+
* const scores = await ltiTool.getScores(session);
|
|
305
|
+
* console.log('All scores:', scores.map(s => `${s.userId}: ${s.scoreGiven}`));
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
async getScores(session: LTISession): Promise<Results> {
|
|
309
|
+
if (!session) {
|
|
310
|
+
throw new Error('session is required');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const response = await this.agsService.getScores(session);
|
|
314
|
+
const data = await response.json();
|
|
315
|
+
return ResultsSchema.parse(data);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Retrieves line items (gradebook columns) from the platform using Assignment and Grade Services (AGS).
|
|
320
|
+
*
|
|
321
|
+
* @param session - Active LTI session containing AGS service endpoints
|
|
322
|
+
* @returns Array of line items from the platform
|
|
323
|
+
* @throws {Error} When AGS is not available or request fails
|
|
324
|
+
*/
|
|
325
|
+
async listLineItems(session: LTISession): Promise<LineItems> {
|
|
326
|
+
if (!session) {
|
|
327
|
+
throw new Error('session is required');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const response = await this.agsService.listLineItems(session);
|
|
331
|
+
const data = await response.json();
|
|
332
|
+
return LineItemsSchema.parse(data);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Retrieves a specific line item (gradebook column) from the platform using Assignment and Grade Services (AGS).
|
|
337
|
+
*
|
|
338
|
+
* @param session - Active LTI session containing AGS service endpoints
|
|
339
|
+
* @returns Line item data from the platform
|
|
340
|
+
* @throws {Error} When AGS is not available or request fails
|
|
341
|
+
*/
|
|
342
|
+
async getLineItem(session: LTISession): Promise<LineItem> {
|
|
343
|
+
if (!session) {
|
|
344
|
+
throw new Error('session is required');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const response = await this.agsService.getLineItem(session);
|
|
348
|
+
const data = await response.json();
|
|
349
|
+
return LineItemSchema.parse(data);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Creates a new line item (gradebook column) on the platform using Assignment and Grade Services (AGS).
|
|
354
|
+
*
|
|
355
|
+
* @param session - Active LTI session containing AGS service endpoints
|
|
356
|
+
* @param createLineItem - Line item data including label, scoreMaximum, and optional metadata
|
|
357
|
+
* @returns Created line item with platform-generated ID and validated data
|
|
358
|
+
* @throws {Error} When AGS is not available, input validation fails, or creation fails
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```typescript
|
|
362
|
+
* const newLineItem = await ltiTool.createLineItem(session, {
|
|
363
|
+
* label: 'Quiz 1',
|
|
364
|
+
* scoreMaximum: 100,
|
|
365
|
+
* tag: 'quiz',
|
|
366
|
+
* resourceId: 'quiz-001'
|
|
367
|
+
* });
|
|
368
|
+
* console.log('Created line item:', newLineItem.id);
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
async createLineItem(
|
|
372
|
+
session: LTISession,
|
|
373
|
+
createLineItem: CreateLineItem,
|
|
374
|
+
): Promise<LineItem> {
|
|
375
|
+
if (!session) {
|
|
376
|
+
throw new Error('session is required');
|
|
377
|
+
}
|
|
378
|
+
if (!createLineItem) {
|
|
379
|
+
throw new Error('createLineItem is required');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const response = await this.agsService.createLineItem(session, createLineItem);
|
|
383
|
+
const data = await response.json();
|
|
384
|
+
return LineItemSchema.parse(data);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Updates an existing line item (gradebook column) on the platform using Assignment and Grade Services (AGS).
|
|
389
|
+
*
|
|
390
|
+
* @param session - Active LTI session containing AGS service endpoints
|
|
391
|
+
* @param updateLineItem - Updated line item data including all required fields
|
|
392
|
+
* @returns Updated line item with validated data from the platform
|
|
393
|
+
* @throws {Error} When AGS is not available, input validation fails, or update fails
|
|
394
|
+
*/
|
|
395
|
+
async updateLineItem(
|
|
396
|
+
session: LTISession,
|
|
397
|
+
updateLineItem: UpdateLineItem,
|
|
398
|
+
): Promise<LineItem> {
|
|
399
|
+
if (!session) {
|
|
400
|
+
throw new Error('session is required');
|
|
401
|
+
}
|
|
402
|
+
if (!updateLineItem) {
|
|
403
|
+
throw new Error('lineItem is required');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const response = await this.agsService.updateLineItem(session, updateLineItem);
|
|
407
|
+
const data = await response.json();
|
|
408
|
+
return LineItemSchema.parse(data);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Deletes a line item (gradebook column) from the platform using Assignment and Grade Services (AGS).
|
|
413
|
+
*
|
|
414
|
+
* @param session - Active LTI session containing AGS service endpoints
|
|
415
|
+
* @throws {Error} When AGS is not available or deletion fails
|
|
416
|
+
*/
|
|
417
|
+
async deleteLineItem(session: LTISession): Promise<void> {
|
|
418
|
+
if (!session) {
|
|
419
|
+
throw new Error('session is required');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
await this.agsService.deleteLineItem(session);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Retrieves course/context members using Names and Role Provisioning Services (NRPS).
|
|
427
|
+
*
|
|
428
|
+
* @param session - Active LTI session containing NRPS service endpoints
|
|
429
|
+
* @returns Array of members with clean camelCase properties
|
|
430
|
+
* @throws {Error} When NRPS is not available or request fails
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* ```typescript
|
|
434
|
+
* const members = await ltiTool.getMembers(session);
|
|
435
|
+
* const instructors = members.filter(m =>
|
|
436
|
+
* m.roles.some(role => role.includes('Instructor'))
|
|
437
|
+
* );
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
async getMembers(session: LTISession): Promise<Member[]> {
|
|
441
|
+
if (!session) {
|
|
442
|
+
throw new Error('session is required');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const response = await this.nrpsService.getMembers(session);
|
|
446
|
+
const data = await response.json();
|
|
447
|
+
console.log(data);
|
|
448
|
+
const validated = NRPSContextMembershipResponseSchema.parse(data);
|
|
449
|
+
|
|
450
|
+
// Transform to clean camelCase format
|
|
451
|
+
return validated.members.map((member) => ({
|
|
452
|
+
status: member.status,
|
|
453
|
+
name: member.name,
|
|
454
|
+
picture: member.picture,
|
|
455
|
+
givenName: member.given_name,
|
|
456
|
+
familyName: member.family_name,
|
|
457
|
+
middleName: member.middle_name,
|
|
458
|
+
email: member.email,
|
|
459
|
+
userId: member.user_id,
|
|
460
|
+
lisPersonSourcedId: member.lis_person_sourcedid,
|
|
461
|
+
roles: member.roles,
|
|
462
|
+
}));
|
|
273
463
|
}
|
|
274
464
|
|
|
275
465
|
// Client management
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema for LTI Assignment and Grade Services (AGS) Line Item.
|
|
5
|
+
* Represents a gradebook column/assignment according to LTI AGS v2.0 specification.
|
|
6
|
+
*
|
|
7
|
+
* @see https://www.imsglobal.org/spec/lti-ags/v2p0/#line-item-service
|
|
8
|
+
*/
|
|
9
|
+
export const LineItemSchema = z.object({
|
|
10
|
+
/** Unique identifier for the line item */
|
|
11
|
+
id: z.url(),
|
|
12
|
+
|
|
13
|
+
/** Maximum score possible for this line item */
|
|
14
|
+
scoreMaximum: z.number().min(0),
|
|
15
|
+
|
|
16
|
+
/** Human-readable label for the line item */
|
|
17
|
+
label: z.string(),
|
|
18
|
+
|
|
19
|
+
/** Optional resource identifier that this line item is associated with */
|
|
20
|
+
resourceId: z.string().optional(),
|
|
21
|
+
|
|
22
|
+
/** Optional resource link identifier */
|
|
23
|
+
resourceLinkId: z.string().optional(),
|
|
24
|
+
|
|
25
|
+
/** Optional tag to identify the line item */
|
|
26
|
+
tag: z.string().optional(),
|
|
27
|
+
|
|
28
|
+
/** Optional start date/time for the assignment */
|
|
29
|
+
startDateTime: z.iso.datetime().optional(),
|
|
30
|
+
|
|
31
|
+
/** Optional end date/time for the assignment */
|
|
32
|
+
endDateTime: z.iso.datetime().optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Schema for creating a new LTI Assignment and Grade Services (AGS) Line Item.
|
|
37
|
+
* Omits the 'id' field since it's generated by the platform upon creation.
|
|
38
|
+
*
|
|
39
|
+
* @see https://www.imsglobal.org/spec/lti-ags/v2p0/#line-item-service
|
|
40
|
+
*/
|
|
41
|
+
export const CreateLineItemSchema = LineItemSchema.omit({
|
|
42
|
+
id: true,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Schema for updating an existing LTI Assignment and Grade Services (AGS) Line Item.
|
|
47
|
+
* Omits 'id' and 'resourceLinkId' fields as they are immutable per LTI AGS specification.
|
|
48
|
+
* Tools MUST NOT change these values during updates.
|
|
49
|
+
*
|
|
50
|
+
* @see https://www.imsglobal.org/spec/lti-ags/v2p0/#line-item-service
|
|
51
|
+
*/
|
|
52
|
+
export const UpdateLineItemSchema = LineItemSchema.omit({
|
|
53
|
+
id: true,
|
|
54
|
+
resourceLinkId: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Schema for an array of line items returned from the line items service.
|
|
59
|
+
*/
|
|
60
|
+
export const LineItemsSchema = z.array(LineItemSchema);
|
|
61
|
+
|
|
62
|
+
// types
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Type representing a validated line item for LTI AGS.
|
|
66
|
+
* Represents a gradebook column or assignment.
|
|
67
|
+
*/
|
|
68
|
+
export type LineItem = z.infer<typeof LineItemSchema>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Type representing an array of line items.
|
|
72
|
+
*/
|
|
73
|
+
export type LineItems = z.infer<typeof LineItemsSchema>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Type representing data required to create a new line item for LTI AGS.
|
|
77
|
+
* Contains all LineItem fields except the platform-generated 'id'.
|
|
78
|
+
*/
|
|
79
|
+
export type CreateLineItem = z.infer<typeof CreateLineItemSchema>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Type representing data for updating an existing line item for LTI AGS.
|
|
83
|
+
* Contains all LineItem fields except immutable 'id' and 'resourceLinkId'.
|
|
84
|
+
*/
|
|
85
|
+
export type UpdateLineItem = z.infer<typeof UpdateLineItemSchema>;
|