@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
@@ -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
- // Get launch config to access token URL
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 }, 'AGS score submission failed');
65
- throw new Error(`AGS score submission failed: ${response.statusText}`);
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;CA0BnB"}
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
- throw new Error(`Token request failed: ${response.status} ${response.statusText}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lti-tool/core",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "LTI 1.3 implementation for Node.js",
5
5
  "keywords": [
6
6
  "lti",
package/src/ltiTool.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { SignJWT, createRemoteJWKSet, decodeJwt, exportJWK, jwtVerify } from 'jose';
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 type { ScoreSubmission } from './schemas/lti13/ags/scoreSubmission.schema.js';
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<Response> {
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
- return await this.agsService.submitScore(session, score);
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
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import * as z from 'zod';
2
2
 
3
3
  import { DeploymentSchema } from './deployment.schema';
4
4
 
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import * as z from 'zod';
2
2
 
3
3
  /**
4
4
  * Common validation schemas used across the LTI tool
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import * as z from 'zod';
2
2
 
3
3
  export const DeploymentSchema = z.object({
4
4
  id: z.uuid().describe('Internal stable UUID for this deployment configuration'),
@@ -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>;