@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/ltiTool.d.ts +88 -3
  3. package/dist/ltiTool.d.ts.map +1 -1
  4. package/dist/ltiTool.js +157 -3
  5. package/dist/schemas/client.schema.d.ts +1 -1
  6. package/dist/schemas/client.schema.d.ts.map +1 -1
  7. package/dist/schemas/client.schema.js +1 -1
  8. package/dist/schemas/common.schema.d.ts +1 -1
  9. package/dist/schemas/common.schema.d.ts.map +1 -1
  10. package/dist/schemas/common.schema.js +1 -1
  11. package/dist/schemas/deployment.schema.d.ts +1 -1
  12. package/dist/schemas/deployment.schema.d.ts.map +1 -1
  13. package/dist/schemas/deployment.schema.js +1 -1
  14. package/dist/schemas/lti13/ags/lineItem.schema.d.ts +80 -0
  15. package/dist/schemas/lti13/ags/lineItem.schema.d.ts.map +1 -0
  16. package/dist/schemas/lti13/ags/lineItem.schema.js +49 -0
  17. package/dist/schemas/lti13/ags/result.schema.d.ts +65 -0
  18. package/dist/schemas/lti13/ags/result.schema.d.ts.map +1 -0
  19. package/dist/schemas/lti13/ags/result.schema.js +35 -0
  20. package/dist/schemas/lti13/ags/scoreSubmission.schema.d.ts +25 -4
  21. package/dist/schemas/lti13/ags/scoreSubmission.schema.d.ts.map +1 -1
  22. package/dist/schemas/lti13/ags/scoreSubmission.schema.js +2 -1
  23. package/dist/schemas/lti13/claims/baseJwtClaims.schema.d.ts +1 -1
  24. package/dist/schemas/lti13/claims/baseJwtClaims.schema.d.ts.map +1 -1
  25. package/dist/schemas/lti13/claims/baseJwtClaims.schema.js +1 -1
  26. package/dist/schemas/lti13/claims/contextClaims.schema.d.ts +1 -1
  27. package/dist/schemas/lti13/claims/contextClaims.schema.d.ts.map +1 -1
  28. package/dist/schemas/lti13/claims/contextClaims.schema.js +1 -1
  29. package/dist/schemas/lti13/claims/coreLtiClaims.schema.d.ts +1 -1
  30. package/dist/schemas/lti13/claims/coreLtiClaims.schema.d.ts.map +1 -1
  31. package/dist/schemas/lti13/claims/coreLtiClaims.schema.js +1 -1
  32. package/dist/schemas/lti13/claims/platformClaims.schema.d.ts +1 -1
  33. package/dist/schemas/lti13/claims/platformClaims.schema.d.ts.map +1 -1
  34. package/dist/schemas/lti13/claims/platformClaims.schema.js +1 -1
  35. package/dist/schemas/lti13/claims/privacyClaims.schema.d.ts +1 -1
  36. package/dist/schemas/lti13/claims/privacyClaims.schema.d.ts.map +1 -1
  37. package/dist/schemas/lti13/claims/privacyClaims.schema.js +1 -1
  38. package/dist/schemas/lti13/claims/serviceClaims.schema.d.ts +1 -1
  39. package/dist/schemas/lti13/claims/serviceClaims.schema.d.ts.map +1 -1
  40. package/dist/schemas/lti13/claims/serviceClaims.schema.js +1 -1
  41. package/dist/schemas/lti13/lti13JwtPayload.schema.d.ts +1 -1
  42. package/dist/schemas/lti13/lti13JwtPayload.schema.d.ts.map +1 -1
  43. package/dist/schemas/lti13/lti13JwtPayload.schema.js +1 -1
  44. package/dist/schemas/lti13/lti13Launch.schema.d.ts +1 -1
  45. package/dist/schemas/lti13/lti13Launch.schema.d.ts.map +1 -1
  46. package/dist/schemas/lti13/lti13Launch.schema.js +1 -1
  47. package/dist/schemas/lti13/lti13Login.schema.d.ts +1 -1
  48. package/dist/schemas/lti13/lti13Login.schema.d.ts.map +1 -1
  49. package/dist/schemas/lti13/lti13Login.schema.js +1 -1
  50. package/dist/schemas/lti13/nrps/contextMembership.schema.d.ts +65 -0
  51. package/dist/schemas/lti13/nrps/contextMembership.schema.d.ts.map +1 -0
  52. package/dist/schemas/lti13/nrps/contextMembership.schema.js +47 -0
  53. package/dist/services/ags.service.d.ts +86 -0
  54. package/dist/services/ags.service.d.ts.map +1 -1
  55. package/dist/services/ags.service.js +184 -8
  56. package/dist/services/nrps.service.d.ts +28 -0
  57. package/dist/services/nrps.service.d.ts.map +1 -0
  58. package/dist/services/nrps.service.js +51 -0
  59. package/dist/services/token.service.d.ts.map +1 -1
  60. package/dist/services/token.service.js +2 -1
  61. package/package.json +1 -1
  62. package/src/ltiTool.ts +195 -5
  63. package/src/schemas/client.schema.ts +1 -1
  64. package/src/schemas/common.schema.ts +1 -1
  65. package/src/schemas/deployment.schema.ts +1 -1
  66. package/src/schemas/lti13/ags/lineItem.schema.ts +85 -0
  67. package/src/schemas/lti13/ags/result.schema.ts +55 -0
  68. package/src/schemas/lti13/ags/scoreSubmission.schema.ts +3 -1
  69. package/src/schemas/lti13/claims/baseJwtClaims.schema.ts +1 -1
  70. package/src/schemas/lti13/claims/contextClaims.schema.ts +1 -1
  71. package/src/schemas/lti13/claims/coreLtiClaims.schema.ts +1 -1
  72. package/src/schemas/lti13/claims/platformClaims.schema.ts +1 -1
  73. package/src/schemas/lti13/claims/privacyClaims.schema.ts +1 -1
  74. package/src/schemas/lti13/claims/serviceClaims.schema.ts +1 -1
  75. package/src/schemas/lti13/lti13JwtPayload.schema.ts +1 -1
  76. package/src/schemas/lti13/lti13Launch.schema.ts +1 -1
  77. package/src/schemas/lti13/lti13Login.schema.ts +1 -1
  78. package/src/schemas/lti13/nrps/contextMembership.schema.ts +55 -0
  79. package/src/services/ags.service.ts +253 -16
  80. package/src/services/nrps.service.ts +80 -0
  81. package/src/services/token.service.ts +4 -1
@@ -0,0 +1,55 @@
1
+ import * as z from 'zod';
2
+
3
+ /**
4
+ * Schema for LTI Assignment and Grade Services (AGS) Result.
5
+ * Results contain richer metadata than scores, including user info and timestamps.
6
+ *
7
+ * @see https://www.imsglobal.org/spec/lti-ags/v2p0/#result-service
8
+ */
9
+ export const ResultSchema = z.object({
10
+ /** Unique identifier for the result */
11
+ id: z.string(),
12
+
13
+ /** Score of the result, represented as a URL */
14
+ scoreOf: z.url(),
15
+
16
+ /** URL identifying the Line Item to which this result belongs. */
17
+ userId: z.string(),
18
+
19
+ /** The score given to the user */
20
+ resultScore: z.number().optional(),
21
+
22
+ /** Maximum possible score */
23
+ resultMaximum: z.number().optional(),
24
+
25
+ /** Comment associated with the result */
26
+ comment: z.string().optional(),
27
+
28
+ /** Timestamp when the result was recorded */
29
+ timestamp: z.iso.datetime({ offset: true }).optional(),
30
+
31
+ /** Activity progress status */
32
+ activityProgress: z
33
+ .enum(['Initialized', 'Started', 'InProgress', 'Submitted', 'Completed'])
34
+ .optional(),
35
+
36
+ /** Grading progress status */
37
+ gradingProgress: z
38
+ .enum(['FullyGraded', 'Pending', 'PendingManual', 'Failed', 'NotReady'])
39
+ .optional(),
40
+ });
41
+
42
+ /**
43
+ * Schema for an array of results returned from the results service.
44
+ */
45
+ export const ResultsSchema = z.array(ResultSchema);
46
+
47
+ /**
48
+ * Type representing a validated result for LTI AGS.
49
+ */
50
+ export type Result = z.infer<typeof ResultSchema>;
51
+
52
+ /**
53
+ * Type representing an array of results.
54
+ */
55
+ export type Results = z.infer<typeof ResultsSchema>;
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import * as z from 'zod';
2
2
 
3
3
  /**
4
4
  * Schema for submitting grades via LTI Assignment and Grade Services (AGS).
@@ -47,6 +47,8 @@ export const ScoreSubmissionSchema = z.object({
47
47
  .default('FullyGraded'),
48
48
  });
49
49
 
50
+ export const ScoreSubmissionsSchema = z.array(ScoreSubmissionSchema);
51
+
50
52
  /**
51
53
  * Type representing a validated score submission for LTI AGS.
52
54
  * Contains grade data and metadata to be sent to the platform.
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import * as z from 'zod';
2
2
 
3
3
  export const BaseJwtClaimsSchema = z.object({
4
4
  iss: z.string(),
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import * as z from 'zod';
2
2
 
3
3
  export const ResourceLinkSchema = z
4
4
  .object({
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import * as z from 'zod';
2
2
 
3
3
  export const CoreLtiClaimsSchema = z.object({
4
4
  'https://purl.imsglobal.org/spec/lti/claim/message_type': z.union([
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import * as z from 'zod';
2
2
 
3
3
  export const ToolPlatformSchema = z
4
4
  .object({
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import * as z from 'zod';
2
2
 
3
3
  export const PrivacyClaimsSchema = z.object({
4
4
  given_name: z.string(),
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import * as z from 'zod';
2
2
 
3
3
  export const AgsEndpointSchema = z
4
4
  .object({
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import * as z from 'zod';
2
2
 
3
3
  import { BaseJwtClaimsSchema } from './claims/baseJwtClaims.schema.js';
4
4
  import { ContextSchema, ResourceLinkSchema } from './claims/contextClaims.schema.js';
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import * as z from 'zod';
2
2
 
3
3
  export const LTI13LaunchSchema = z.object({
4
4
  id_token: z.jwt(),
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import * as z from 'zod';
2
2
 
3
3
  export const LTI13LoginSchema = z.object({
4
4
  iss: z.string().min(1),
@@ -0,0 +1,55 @@
1
+ import * as z from 'zod';
2
+
3
+ /**
4
+ * Schema for individual member in NRPS response
5
+ */
6
+ export const NRPSMemberResponseSchema = z.object({
7
+ status: z.string(),
8
+ name: z.string(),
9
+ picture: z.url().optional(),
10
+ given_name: z.string().optional(),
11
+ family_name: z.string().optional(),
12
+ middle_name: z.string().optional(),
13
+ email: z.string().optional(), // Platforms don't force email regexp conformance
14
+ user_id: z.string(),
15
+ lis_person_sourcedid: z.string().optional(),
16
+ roles: z.array(z.string()),
17
+ });
18
+
19
+ /**
20
+ * Schema for context information in NRPS response
21
+ */
22
+ export const NRPSContextResponseSchema = z.object({
23
+ id: z.string(),
24
+ label: z.string(),
25
+ title: z.string(),
26
+ });
27
+
28
+ /**
29
+ * Schema for full NRPS context membership response
30
+ */
31
+ export const NRPSContextMembershipResponseSchema = z.object({
32
+ id: z.url(),
33
+ context: NRPSContextResponseSchema,
34
+ members: z.array(NRPSMemberResponseSchema),
35
+ });
36
+
37
+ /**
38
+ * Clean public API schemas (camelCase for JS/TS consumers)
39
+ */
40
+ export const MemberSchema = z.object({
41
+ status: z.string(),
42
+ name: z.string(),
43
+ picture: z.url().optional(),
44
+ givenName: z.string().optional(),
45
+ familyName: z.string().optional(),
46
+ middleName: z.string().optional(),
47
+ email: z.string().optional(), // Platforms don't force email regexp conformance
48
+ userId: z.string(),
49
+ lisPersonSourcedId: z.string().optional(),
50
+ roles: z.array(z.string()),
51
+ });
52
+
53
+ // Export clean types for public API
54
+ export type Member = z.infer<typeof MemberSchema>;
55
+ export type Context = z.infer<typeof NRPSContextResponseSchema>;
@@ -2,6 +2,10 @@ import type { BaseLogger } from 'pino';
2
2
 
3
3
  import type { LTISession } from '../interfaces/ltiSession.js';
4
4
  import type { LTIStorage } from '../interfaces/ltiStorage.js';
5
+ import type {
6
+ CreateLineItem,
7
+ UpdateLineItem,
8
+ } from '../schemas/lti13/ags/lineItem.schema.js';
5
9
  import type { ScoreSubmission } from '../schemas/lti13/ags/scoreSubmission.schema.js';
6
10
  import { getValidLaunchConfig } from '../utils/launchConfigValidation.js';
7
11
 
@@ -44,18 +48,8 @@ export class AGSService {
44
48
  throw new Error('AGS not available for this session');
45
49
  }
46
50
 
47
- // Get launch config to access token URL
48
- const launchConfig = await getValidLaunchConfig(
49
- this.storage,
50
- session.platform.issuer,
51
- session.platform.clientId,
52
- session.platform.deploymentId,
53
- );
54
-
55
- const token = await this.tokenService.getBearerToken(
56
- session.platform.clientId,
57
- // Need to get token URL from platform storage
58
- launchConfig.tokenUrl,
51
+ const token = await this.getAGSToken(
52
+ session,
59
53
  'https://purl.imsglobal.org/spec/lti-ags/scope/score',
60
54
  );
61
55
 
@@ -78,15 +72,258 @@ export class AGSService {
78
72
  body: JSON.stringify(scorePayload),
79
73
  });
80
74
 
75
+ await this.validateAGSResponse(response, 'score submission');
76
+ return response;
77
+ }
78
+
79
+ /**
80
+ * Retrieves all scores for a specific line item from the platform using Assignment and Grade Services.
81
+ *
82
+ * @param session - Active LTI session containing AGS line item endpoint configuration
83
+ * @returns Promise resolving to the HTTP response containing scores data for the line item
84
+ * @throws {Error} When AGS line item service is not available for the session or request fails
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const response = await agsService.getScores(session);
89
+ * const scores = await response.json();
90
+ * console.log('All scores for this line item:', scores);
91
+ * ```
92
+ */
93
+ async getScores(session: LTISession): Promise<Response> {
94
+ if (!session.services?.ags?.lineitem) {
95
+ throw new Error('AGS line item not available for this session');
96
+ }
97
+
98
+ const token = await this.getAGSToken(
99
+ session,
100
+ 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly',
101
+ );
102
+
103
+ // cleanse the results URL
104
+ // we cannot include a search / query param
105
+ const lineItemUrl = new URL(session.services.ags.lineitem);
106
+ lineItemUrl.search = '';
107
+ const resultsUrl = `${lineItemUrl.toString()}/results`;
108
+
109
+ const response = await fetch(resultsUrl, {
110
+ method: 'GET',
111
+ headers: {
112
+ Authorization: `Bearer ${token}`,
113
+ Accept: 'application/vnd.ims.lis.v2.resultcontainer+json',
114
+ },
115
+ });
116
+
117
+ await this.validateAGSResponse(response, 'get scores');
118
+ return response;
119
+ }
120
+
121
+ /**
122
+ * Retrieves line items (gradebook columns) from the platform using Assignment and Grade Services.
123
+ *
124
+ * @param session - Active LTI session containing AGS line items endpoint configuration
125
+ * @returns Promise resolving to the HTTP response containing line items data
126
+ * @throws {Error} When AGS line items service is not available for the session or request fails
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const response = await agsService.listLineItems(session);
131
+ * const lineItems = await response.json();
132
+ * console.log('Available gradebook columns:', lineItems);
133
+ * ```
134
+ */
135
+ async listLineItems(session: LTISession): Promise<Response> {
136
+ if (!session.services?.ags?.lineitems) {
137
+ throw new Error('AGS list line items not available for this session');
138
+ }
139
+
140
+ const token = await this.getAGSToken(
141
+ session,
142
+ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly',
143
+ );
144
+
145
+ const response = await fetch(`${session.services.ags.lineitems}`, {
146
+ method: 'GET',
147
+ headers: {
148
+ Authorization: `Bearer ${token}`,
149
+ Accept: 'application/vnd.ims.lis.v2.lineitemcontainer+json',
150
+ },
151
+ });
152
+
153
+ await this.validateAGSResponse(response, 'list line items');
154
+ return response;
155
+ }
156
+
157
+ /**
158
+ * Retrieves a specific line item (gradebook column) from the platform using Assignment and Grade Services.
159
+ *
160
+ * @param session - Active LTI session containing AGS line item endpoint configuration
161
+ * @returns Promise resolving to the HTTP response containing the line item data
162
+ * @throws {Error} When AGS line item service is not available for the session or request fails
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * const response = await agsService.getLineItem(session);
167
+ * const lineItem = await response.json();
168
+ * console.log('Line item details:', lineItem);
169
+ * ```
170
+ */
171
+ async getLineItem(session: LTISession): Promise<Response> {
172
+ if (!session.services?.ags?.lineitem) {
173
+ throw new Error('AGS line item not available for this session');
174
+ }
175
+
176
+ const token = await this.getAGSToken(
177
+ session,
178
+ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly',
179
+ );
180
+
181
+ const response = await fetch(`${session.services.ags.lineitem}`, {
182
+ method: 'GET',
183
+ headers: {
184
+ Authorization: `Bearer ${token}`,
185
+ Accept: 'application/vnd.ims.lis.v2.lineitem+json',
186
+ },
187
+ });
188
+
189
+ await this.validateAGSResponse(response, 'get line item');
190
+ return response;
191
+ }
192
+
193
+ /**
194
+ * Creates a new line item (gradebook column) on the platform using Assignment and Grade Services.
195
+ *
196
+ * @param session - Active LTI session containing AGS line items endpoint configuration
197
+ * @param createLineItem - Line item data including label, scoreMaximum, and optional metadata
198
+ * @returns Promise resolving to the HTTP response containing the created line item with generated ID
199
+ * @throws {Error} When AGS line item creation service is not available for the session or creation fails
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * const response = await agsService.createLineItem(session, {
204
+ * label: 'Quiz 1',
205
+ * scoreMaximum: 100,
206
+ * tag: 'quiz',
207
+ * resourceId: 'quiz-001'
208
+ * });
209
+ * const newLineItem = await response.json();
210
+ * console.log('Created line item:', newLineItem.id);
211
+ * ```
212
+ */
213
+ async createLineItem(
214
+ session: LTISession,
215
+ createLineItem: CreateLineItem,
216
+ ): Promise<Response> {
217
+ if (!session.services?.ags?.lineitems) {
218
+ throw new Error('AGS create line items not available for this session');
219
+ }
220
+
221
+ const token = await this.getAGSToken(
222
+ session,
223
+ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem',
224
+ );
225
+
226
+ const response = await fetch(`${session.services.ags.lineitems}`, {
227
+ method: 'POST',
228
+ headers: {
229
+ Authorization: `Bearer ${token}`,
230
+ 'Content-Type': 'application/vnd.ims.lis.v2.lineitem+json',
231
+ },
232
+ body: JSON.stringify(createLineItem),
233
+ });
234
+
235
+ await this.validateAGSResponse(response, 'create line item');
236
+ return response;
237
+ }
238
+
239
+ /**
240
+ * Updates an existing line item (gradebook column) on the platform using Assignment and Grade Services.
241
+ *
242
+ * @param session - Active LTI session containing AGS line item endpoint configuration
243
+ * @param updateLineItem - Updated line item data including all required fields
244
+ * @returns Promise resolving to the HTTP response containing the updated line item
245
+ * @throws {Error} When AGS line item service is not available for the session or update fails
246
+ */
247
+ async updateLineItem(
248
+ session: LTISession,
249
+ updateLineItem: UpdateLineItem,
250
+ ): Promise<Response> {
251
+ if (!session.services?.ags?.lineitem) {
252
+ throw new Error('AGS line item not available for this session');
253
+ }
254
+
255
+ const token = await this.getAGSToken(
256
+ session,
257
+ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem',
258
+ );
259
+
260
+ const response = await fetch(session.services.ags.lineitem, {
261
+ method: 'PUT',
262
+ headers: {
263
+ Authorization: `Bearer ${token}`,
264
+ 'Content-Type': 'application/vnd.ims.lis.v2.lineitem+json',
265
+ },
266
+ body: JSON.stringify(updateLineItem),
267
+ });
268
+
269
+ await this.validateAGSResponse(response, 'update line item');
270
+ return response;
271
+ }
272
+
273
+ /**
274
+ * Deletes a line item (gradebook column) from the platform using Assignment and Grade Services.
275
+ *
276
+ * @param session - Active LTI session containing AGS line item endpoint configuration
277
+ * @returns Promise resolving to the HTTP response (typically 204 No Content on success)
278
+ * @throws {Error} When AGS line item service is not available for the session or deletion fails
279
+ */
280
+ async deleteLineItem(session: LTISession): Promise<Response> {
281
+ if (!session.services?.ags?.lineitem) {
282
+ throw new Error('AGS line item not available for this session');
283
+ }
284
+
285
+ const token = await this.getAGSToken(
286
+ session,
287
+ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem',
288
+ );
289
+
290
+ const response = await fetch(session.services.ags.lineitem, {
291
+ method: 'DELETE',
292
+ headers: {
293
+ Authorization: `Bearer ${token}`,
294
+ },
295
+ });
296
+
297
+ await this.validateAGSResponse(response, 'delete line item');
298
+ return response;
299
+ }
300
+
301
+ private async getAGSToken(session: LTISession, scope: string): Promise<string> {
302
+ const launchConfig = await getValidLaunchConfig(
303
+ this.storage,
304
+ session.platform.issuer,
305
+ session.platform.clientId,
306
+ session.platform.deploymentId,
307
+ );
308
+
309
+ return this.tokenService.getBearerToken(
310
+ session.platform.clientId,
311
+ launchConfig.tokenUrl,
312
+ scope,
313
+ );
314
+ }
315
+
316
+ private async validateAGSResponse(
317
+ response: Response,
318
+ operation: string,
319
+ ): Promise<void> {
81
320
  if (!response.ok) {
82
321
  const error = await response.json();
83
322
  this.logger.error(
84
323
  { error, status: response.status, statusText: response.statusText },
85
- 'AGS score submission failed',
324
+ `AGS ${operation} failed`,
86
325
  );
87
- throw new Error(`AGS score submission failed: ${response.statusText}`);
326
+ throw new Error(`AGS ${operation} failed: ${response.statusText} ${error}`);
88
327
  }
89
-
90
- return response;
91
328
  }
92
329
  }
@@ -0,0 +1,80 @@
1
+ import type { BaseLogger } from 'pino';
2
+
3
+ import type { LTISession } from '../interfaces/ltiSession.js';
4
+ import type { LTIStorage } from '../interfaces/ltiStorage.js';
5
+ import { getValidLaunchConfig } from '../utils/launchConfigValidation.js';
6
+
7
+ import type { TokenService } from './token.service.js';
8
+
9
+ /**
10
+ * Names and Role Provisioning Services (NRPS) implementation for LTI 1.3.
11
+ * Provides methods to retrieve course membership and user information from the platform.
12
+ *
13
+ * @see https://www.imsglobal.org/spec/lti-nrps/v2p0
14
+ */
15
+ export class NRPSService {
16
+ constructor(
17
+ private tokenService: TokenService,
18
+ private storage: LTIStorage,
19
+ private logger: BaseLogger,
20
+ ) {}
21
+
22
+ /**
23
+ * Retrieves all members (users) in the current course/context from the platform.
24
+ * Returns raw response that should be parsed by the calling service.
25
+ *
26
+ * @param session - Active LTI session containing NRPS service endpoints
27
+ * @returns Raw HTTP response containing membership data
28
+ * @throws {Error} When NRPS is not available for this session or request fails
29
+ */
30
+ async getMembers(session: LTISession): Promise<Response> {
31
+ if (!session.services?.nrps?.membershipUrl) {
32
+ throw new Error('NRPS not available for this session');
33
+ }
34
+
35
+ const token = await this.getNRPSToken(
36
+ session,
37
+ 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly',
38
+ );
39
+
40
+ const response = await fetch(session.services.nrps.membershipUrl, {
41
+ method: 'GET',
42
+ headers: {
43
+ Authorization: `Bearer ${token}`,
44
+ Accept: 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json',
45
+ },
46
+ });
47
+
48
+ await this.validateNRPSResponse(response, 'get members');
49
+ return response;
50
+ }
51
+
52
+ private async getNRPSToken(session: LTISession, scope: string): Promise<string> {
53
+ const launchConfig = await getValidLaunchConfig(
54
+ this.storage,
55
+ session.platform.issuer,
56
+ session.platform.clientId,
57
+ session.platform.deploymentId,
58
+ );
59
+
60
+ return this.tokenService.getBearerToken(
61
+ session.platform.clientId,
62
+ launchConfig.tokenUrl,
63
+ scope,
64
+ );
65
+ }
66
+
67
+ private async validateNRPSResponse(
68
+ response: Response,
69
+ operation: string,
70
+ ): Promise<void> {
71
+ if (!response.ok) {
72
+ const error = await response.json();
73
+ this.logger.error(
74
+ { error, status: response.status, statusText: response.statusText },
75
+ `NRPS ${operation} failed`,
76
+ );
77
+ throw new Error(`NRPS ${operation} failed: ${response.statusText} ${error}`);
78
+ }
79
+ }
80
+ }
@@ -70,7 +70,10 @@ export class TokenService {
70
70
  });
71
71
 
72
72
  if (!response.ok) {
73
- throw new Error(`Token request failed: ${response.status} ${response.statusText}`);
73
+ const errorDetail = await response.json();
74
+ throw new Error(
75
+ `Token request failed: ${response.status} ${response.statusText} ${errorDetail}`,
76
+ );
74
77
  }
75
78
 
76
79
  const tokenData = await response.json();