@lti-tool/core 0.13.1 → 0.14.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/dist/ltiTool.js CHANGED
@@ -10,6 +10,7 @@ import { DynamicRegistrationService } from './services/dynamicRegistration.servi
10
10
  import { NRPSService } from './services/nrps.service.js';
11
11
  import { createSession } from './services/session.service.js';
12
12
  import { TokenService } from './services/token.service.js';
13
+ import { formatError } from './utils/errorFormatting.js';
13
14
  import { getValidLaunchConfig } from './utils/launchConfigValidation.js';
14
15
  /**
15
16
  * Main LTI 1.3 Tool implementation providing secure authentication, launch verification,
@@ -82,38 +83,29 @@ export class LTITool {
82
83
  * @throws {Error} When platform configuration is not found
83
84
  */
84
85
  async handleLogin(params) {
85
- const validatedParams = HandleLoginParamsSchema.parse(params);
86
- const nonce = crypto.randomUUID();
87
- // Store nonce with expiration for replay attack prevention
88
- const nonceExpirationSeconds = this.config.security?.nonceExpirationSeconds ?? 600;
89
- const nonceExpiresAt = new Date(Date.now() + nonceExpirationSeconds * 1000);
90
- await this.config.storage.storeNonce(nonce, nonceExpiresAt);
91
- const state = await new SignJWT({
92
- nonce,
93
- iss: validatedParams.iss,
94
- client_id: validatedParams.client_id,
95
- target_link_uri: validatedParams.target_link_uri,
96
- exp: Math.floor(Date.now() / 1000) +
97
- (this.config.security?.stateExpirationSeconds ?? 600),
98
- })
99
- .setProtectedHeader({ alg: 'HS256' })
100
- .sign(this.config.stateSecret);
101
- const launchConfig = await getValidLaunchConfig(this.config.storage, validatedParams.iss, validatedParams.client_id, validatedParams.lti_deployment_id);
102
- const authUrl = new URL(launchConfig.authUrl);
103
- authUrl.searchParams.set('scope', 'openid');
104
- authUrl.searchParams.set('response_type', 'id_token');
105
- authUrl.searchParams.set('response_mode', 'form_post');
106
- authUrl.searchParams.set('prompt', 'none');
107
- authUrl.searchParams.set('client_id', validatedParams.client_id);
108
- authUrl.searchParams.set('redirect_uri', validatedParams.launchUrl.toString());
109
- authUrl.searchParams.set('login_hint', validatedParams.login_hint);
110
- authUrl.searchParams.set('state', state);
111
- authUrl.searchParams.set('nonce', nonce);
112
- authUrl.searchParams.set('lti_deployment_id', validatedParams.lti_deployment_id);
113
- if (validatedParams.lti_message_hint) {
114
- authUrl.searchParams.set('lti_message_hint', validatedParams.lti_message_hint);
115
- }
116
- return authUrl.toString();
86
+ try {
87
+ const validatedParams = HandleLoginParamsSchema.parse(params);
88
+ const nonce = crypto.randomUUID();
89
+ // Store nonce with expiration for replay attack prevention
90
+ const nonceExpirationSeconds = this.config.security?.nonceExpirationSeconds ?? 600;
91
+ const nonceExpiresAt = new Date(Date.now() + nonceExpirationSeconds * 1000);
92
+ await this.config.storage.storeNonce(nonce, nonceExpiresAt);
93
+ const state = await new SignJWT({
94
+ nonce,
95
+ iss: validatedParams.iss,
96
+ client_id: validatedParams.client_id,
97
+ target_link_uri: validatedParams.target_link_uri,
98
+ exp: Math.floor(Date.now() / 1000) +
99
+ (this.config.security?.stateExpirationSeconds ?? 600),
100
+ })
101
+ .setProtectedHeader({ alg: 'HS256' })
102
+ .sign(this.config.stateSecret);
103
+ const launchConfig = await getValidLaunchConfig(this.config.storage, validatedParams.iss, validatedParams.client_id, validatedParams.lti_deployment_id);
104
+ return buildAuthUrl(launchConfig, validatedParams, state, nonce);
105
+ }
106
+ catch (error) {
107
+ throw new Error(`[LTI] Login initiation failed for issuer '${params.iss}', client '${params.client_id}': ${formatError(error)}`);
108
+ }
117
109
  }
118
110
  /**
119
111
  * Verifies and validates an LTI 1.3 launch by checking JWT signatures, nonces, and claims.
@@ -131,39 +123,44 @@ export class LTITool {
131
123
  * @throws {Error} When verification fails for security reasons
132
124
  */
133
125
  async verifyLaunch(idToken, state) {
134
- const validatedParams = VerifyLaunchParamsSchema.parse({ idToken, state });
135
- // 1. UNVERIFIED - get issuer
136
- const unverified = decodeJwt(validatedParams.idToken);
137
- if (!unverified.iss) {
138
- throw new Error('No issuer in token');
139
- }
140
- // 2. get the launchConfig so we can get the remote JWKS from our data store
141
- const launchConfig = await getValidLaunchConfig(this.config.storage, unverified.iss, unverified.aud, unverified['https://purl.imsglobal.org/spec/lti/claim/deployment_id']);
142
- // 3. Verify LMS JWT
143
- let jwks = this.jwksCache.get(launchConfig.jwksUrl);
144
- if (!jwks) {
145
- jwks = createRemoteJWKSet(new URL(launchConfig.jwksUrl));
146
- this.jwksCache.set(launchConfig.jwksUrl, jwks);
147
- }
148
- const { payload } = await jwtVerify(validatedParams.idToken, jwks);
149
- // 4. Verify our state JWT
150
- const { payload: stateData } = await jwtVerify(validatedParams.state, this.config.stateSecret);
151
- // 5. Parse and validate LMS JWT
152
- const validated = LTI13JwtPayloadSchema.parse(payload);
153
- // 6. Verify client id matches (audience claim)
154
- if (validated.aud !== launchConfig.clientId) {
155
- throw new Error(`Invalid client_id: expected ${launchConfig.clientId}, got ${validated.aud}`);
156
- }
157
- // 7. Verify nonce matches
158
- if (stateData.nonce !== validated.nonce) {
159
- throw new Error('Nonce mismatch');
160
- }
161
- // 8. Check nonce hasn't been used before (prevent replay attacks)
162
- const isValidNonce = await this.config.storage.validateNonce(validated.nonce);
163
- if (!isValidNonce) {
164
- throw new Error('Nonce has already been used or expired');
165
- }
166
- return validated;
126
+ try {
127
+ const validatedParams = VerifyLaunchParamsSchema.parse({ idToken, state });
128
+ // 1. UNVERIFIED - get issuer
129
+ const unverified = decodeJwt(validatedParams.idToken);
130
+ if (!unverified.iss) {
131
+ throw new Error('No issuer in token');
132
+ }
133
+ // 2. get the launchConfig so we can get the remote JWKS from our data store
134
+ const launchConfig = await getValidLaunchConfig(this.config.storage, unverified.iss, unverified.aud, unverified['https://purl.imsglobal.org/spec/lti/claim/deployment_id']);
135
+ // 3. Verify LMS JWT
136
+ let jwks = this.jwksCache.get(launchConfig.jwksUrl);
137
+ if (!jwks) {
138
+ jwks = createRemoteJWKSet(new URL(launchConfig.jwksUrl));
139
+ this.jwksCache.set(launchConfig.jwksUrl, jwks);
140
+ }
141
+ const { payload } = await jwtVerify(validatedParams.idToken, jwks);
142
+ // 4. Verify our state JWT
143
+ const { payload: stateData } = await jwtVerify(validatedParams.state, this.config.stateSecret);
144
+ // 5. Parse and validate LMS JWT
145
+ const validated = LTI13JwtPayloadSchema.parse(payload);
146
+ // 6. Verify client id matches (audience claim)
147
+ if (validated.aud !== launchConfig.clientId) {
148
+ throw new Error(`Invalid client_id: expected ${launchConfig.clientId}, got ${validated.aud}`);
149
+ }
150
+ // 7. Verify nonce matches
151
+ if (stateData.nonce !== validated.nonce) {
152
+ throw new Error('Nonce mismatch');
153
+ }
154
+ // 8. Check nonce hasn't been used before (prevent replay attacks)
155
+ const isValidNonce = await this.config.storage.validateNonce(validated.nonce);
156
+ if (!isValidNonce) {
157
+ throw new Error('Nonce has already been used or expired');
158
+ }
159
+ return validated;
160
+ }
161
+ catch (error) {
162
+ throw new Error(`[LTI] Launch verification failed: ${formatError(error)}`);
163
+ }
167
164
  }
168
165
  /**
169
166
  * Generates JSON Web Key Set (JWKS) containing the tool's public key for platform verification.
@@ -171,17 +168,22 @@ export class LTITool {
171
168
  * @returns JWKS object with the tool's public key for JWT signature verification
172
169
  */
173
170
  async getJWKS() {
174
- const publicJwk = await exportJWK(this.config.keyPair.publicKey);
175
- return {
176
- keys: [
177
- {
178
- ...publicJwk,
179
- use: 'sig',
180
- alg: 'RS256',
181
- kid: this.config.security?.keyId ?? 'main',
182
- },
183
- ],
184
- };
171
+ try {
172
+ const publicJwk = await exportJWK(this.config.keyPair.publicKey);
173
+ return {
174
+ keys: [
175
+ {
176
+ ...publicJwk,
177
+ use: 'sig',
178
+ alg: 'RS256',
179
+ kid: this.config.security?.keyId ?? 'main',
180
+ },
181
+ ],
182
+ };
183
+ }
184
+ catch (error) {
185
+ throw new Error(`[LTI] JWKS generation failed: ${formatError(error)}`);
186
+ }
185
187
  }
186
188
  /**
187
189
  * Creates and stores a new LTI session from validated JWT payload.
@@ -190,9 +192,14 @@ export class LTITool {
190
192
  * @returns Created session object with user, context, and service information
191
193
  */
192
194
  async createSession(lti13JwtPayload) {
193
- const session = createSession(lti13JwtPayload);
194
- await this.config.storage.addSession(session);
195
- return session;
195
+ try {
196
+ const session = createSession(lti13JwtPayload);
197
+ await this.config.storage.addSession(session);
198
+ return session;
199
+ }
200
+ catch (error) {
201
+ throw new Error(`[Session] Creation failed for user '${lti13JwtPayload.sub}': ${formatError(error)}`);
202
+ }
196
203
  }
197
204
  /**
198
205
  * Retrieves an existing LTI session by session ID.
@@ -201,8 +208,13 @@ export class LTITool {
201
208
  * @returns Session object if found, undefined otherwise
202
209
  */
203
210
  async getSession(sessionId) {
204
- const validatedSessionId = SessionIdSchema.parse(sessionId);
205
- return await this.config.storage.getSession(validatedSessionId);
211
+ try {
212
+ const validatedSessionId = SessionIdSchema.parse(sessionId);
213
+ return await this.config.storage.getSession(validatedSessionId);
214
+ }
215
+ catch (error) {
216
+ throw new Error(`[Session] Retrieval failed for ID '${sessionId}': ${formatError(error)}`);
217
+ }
206
218
  }
207
219
  /**
208
220
  * Submits a grade score to the platform using Assignment and Grade Services (AGS).
@@ -218,7 +230,12 @@ export class LTITool {
218
230
  if (!score) {
219
231
  throw new Error('score is required');
220
232
  }
221
- await this.agsService.submitScore(session, score);
233
+ try {
234
+ await this.agsService.submitScore(session, score);
235
+ }
236
+ catch (error) {
237
+ throw new Error(`[AGS] Score submission failed for user '${score.userId}': ${formatError(error)}`);
238
+ }
222
239
  }
223
240
  /**
224
241
  * Retrieves all scores for a specific line item from the platform using Assignment and Grade Services (AGS).
@@ -237,9 +254,14 @@ export class LTITool {
237
254
  if (!session) {
238
255
  throw new Error('session is required');
239
256
  }
240
- const response = await this.agsService.getScores(session);
241
- const data = await response.json();
242
- return ResultsSchema.parse(data);
257
+ try {
258
+ const response = await this.agsService.getScores(session);
259
+ const data = await response.json();
260
+ return ResultsSchema.parse(data);
261
+ }
262
+ catch (error) {
263
+ throw new Error(`[AGS] Scores retrieval failed for session '${session.id}': ${formatError(error)}`);
264
+ }
243
265
  }
244
266
  /**
245
267
  * Retrieves line items (gradebook columns) from the platform using Assignment and Grade Services (AGS).
@@ -252,9 +274,14 @@ export class LTITool {
252
274
  if (!session) {
253
275
  throw new Error('session is required');
254
276
  }
255
- const response = await this.agsService.listLineItems(session);
256
- const data = await response.json();
257
- return LineItemsSchema.parse(data);
277
+ try {
278
+ const response = await this.agsService.listLineItems(session);
279
+ const data = await response.json();
280
+ return LineItemsSchema.parse(data);
281
+ }
282
+ catch (error) {
283
+ throw new Error(`[AGS] Line items listing failed for session '${session.id}': ${formatError(error)}`);
284
+ }
258
285
  }
259
286
  /**
260
287
  * Retrieves a specific line item (gradebook column) from the platform using Assignment and Grade Services (AGS).
@@ -267,9 +294,14 @@ export class LTITool {
267
294
  if (!session) {
268
295
  throw new Error('session is required');
269
296
  }
270
- const response = await this.agsService.getLineItem(session);
271
- const data = await response.json();
272
- return LineItemSchema.parse(data);
297
+ try {
298
+ const response = await this.agsService.getLineItem(session);
299
+ const data = await response.json();
300
+ return LineItemSchema.parse(data);
301
+ }
302
+ catch (error) {
303
+ throw new Error(`[AGS] Line item retrieval failed for session '${session.id}': ${formatError(error)}`);
304
+ }
273
305
  }
274
306
  /**
275
307
  * Creates a new line item (gradebook column) on the platform using Assignment and Grade Services (AGS).
@@ -297,9 +329,14 @@ export class LTITool {
297
329
  if (!createLineItem) {
298
330
  throw new Error('createLineItem is required');
299
331
  }
300
- const response = await this.agsService.createLineItem(session, createLineItem);
301
- const data = await response.json();
302
- return LineItemSchema.parse(data);
332
+ try {
333
+ const response = await this.agsService.createLineItem(session, createLineItem);
334
+ const data = await response.json();
335
+ return LineItemSchema.parse(data);
336
+ }
337
+ catch (error) {
338
+ throw new Error(`[AGS] Line item creation failed for '${createLineItem.label}': ${formatError(error)}`);
339
+ }
303
340
  }
304
341
  /**
305
342
  * Updates an existing line item (gradebook column) on the platform using Assignment and Grade Services (AGS).
@@ -316,9 +353,14 @@ export class LTITool {
316
353
  if (!updateLineItem) {
317
354
  throw new Error('lineItem is required');
318
355
  }
319
- const response = await this.agsService.updateLineItem(session, updateLineItem);
320
- const data = await response.json();
321
- return LineItemSchema.parse(data);
356
+ try {
357
+ const response = await this.agsService.updateLineItem(session, updateLineItem);
358
+ const data = await response.json();
359
+ return LineItemSchema.parse(data);
360
+ }
361
+ catch (error) {
362
+ throw new Error(`[AGS] Line item update failed for '${updateLineItem.label}': ${formatError(error)}`);
363
+ }
322
364
  }
323
365
  /**
324
366
  * Deletes a line item (gradebook column) from the platform using Assignment and Grade Services (AGS).
@@ -330,7 +372,12 @@ export class LTITool {
330
372
  if (!session) {
331
373
  throw new Error('session is required');
332
374
  }
333
- await this.agsService.deleteLineItem(session);
375
+ try {
376
+ await this.agsService.deleteLineItem(session);
377
+ }
378
+ catch (error) {
379
+ throw new Error(`[AGS] Line item deletion failed for session '${session.id}': ${formatError(error)}`);
380
+ }
334
381
  }
335
382
  /**
336
383
  * Retrieves course/context members using Names and Role Provisioning Services (NRPS).
@@ -351,22 +398,27 @@ export class LTITool {
351
398
  if (!session) {
352
399
  throw new Error('session is required');
353
400
  }
354
- const response = await this.nrpsService.getMembers(session);
355
- const data = await response.json();
356
- const validated = NRPSContextMembershipResponseSchema.parse(data);
357
- // Transform to clean camelCase format
358
- return validated.members.map((member) => ({
359
- status: member.status,
360
- name: member.name,
361
- picture: member.picture,
362
- givenName: member.given_name,
363
- familyName: member.family_name,
364
- middleName: member.middle_name,
365
- email: member.email,
366
- userId: member.user_id,
367
- lisPersonSourcedId: member.lis_person_sourcedid,
368
- roles: member.roles,
369
- }));
401
+ try {
402
+ const response = await this.nrpsService.getMembers(session);
403
+ const data = await response.json();
404
+ const validated = NRPSContextMembershipResponseSchema.parse(data);
405
+ // Transform to clean camelCase format
406
+ return validated.members.map((member) => ({
407
+ status: member.status,
408
+ name: member.name,
409
+ picture: member.picture,
410
+ givenName: member.given_name,
411
+ familyName: member.family_name,
412
+ middleName: member.middle_name,
413
+ email: member.email,
414
+ userId: member.user_id,
415
+ lisPersonSourcedId: member.lis_person_sourcedid,
416
+ roles: member.roles,
417
+ }));
418
+ }
419
+ catch (error) {
420
+ throw new Error(`[NRPS] Members retrieval failed for session '${session.id}': ${formatError(error)}`);
421
+ }
370
422
  }
371
423
  /**
372
424
  * Creates a Deep Linking response with selected content items.
@@ -396,7 +448,12 @@ export class LTITool {
396
448
  if (!contentItems) {
397
449
  throw new Error('contentItems is required');
398
450
  }
399
- return await this.deepLinkingService.createResponse(session, contentItems);
451
+ try {
452
+ return await this.deepLinkingService.createResponse(session, contentItems);
453
+ }
454
+ catch (error) {
455
+ throw new Error(`[Deep Linking] Response creation failed for session '${session.id}': ${formatError(error)}`);
456
+ }
400
457
  }
401
458
  /**
402
459
  * Fetches and validates the OpenID Connect configuration from an LTI platform during dynamic registration.
@@ -419,7 +476,12 @@ export class LTITool {
419
476
  if (!this.dynamicRegistrationService) {
420
477
  throw new Error('Dynamic registration service is not configured');
421
478
  }
422
- return await this.dynamicRegistrationService.fetchPlatformConfiguration(registrationRequest);
479
+ try {
480
+ return await this.dynamicRegistrationService.fetchPlatformConfiguration(registrationRequest);
481
+ }
482
+ catch (error) {
483
+ throw new Error(`[Dynamic Registration] Platform configuration fetch failed: ${formatError(error)}`);
484
+ }
423
485
  }
424
486
  /**
425
487
  * Initiates LTI 1.3 dynamic registration by fetching platform configuration and generating registration form.
@@ -434,7 +496,12 @@ export class LTITool {
434
496
  if (!this.dynamicRegistrationService) {
435
497
  throw new Error('Dynamic registration service is not configured');
436
498
  }
437
- return await this.dynamicRegistrationService.initiateDynamicRegistration(registrationRequest, requestPath);
499
+ try {
500
+ return await this.dynamicRegistrationService.initiateDynamicRegistration(registrationRequest, requestPath);
501
+ }
502
+ catch (error) {
503
+ throw new Error(`[Dynamic Registration] Initiation failed: ${formatError(error)}`);
504
+ }
438
505
  }
439
506
  /**
440
507
  * Completes LTI 1.3 dynamic registration by processing form submission and storing client configuration.
@@ -448,7 +515,12 @@ export class LTITool {
448
515
  if (!this.dynamicRegistrationService) {
449
516
  throw new Error('Dynamic registration service is not configured');
450
517
  }
451
- return await this.dynamicRegistrationService.completeDynamicRegistration(dynamicRegistrationForm);
518
+ try {
519
+ return await this.dynamicRegistrationService.completeDynamicRegistration(dynamicRegistrationForm);
520
+ }
521
+ catch (error) {
522
+ throw new Error(`[Dynamic Registration] Completion failed: ${formatError(error)}`);
523
+ }
452
524
  }
453
525
  // Client management
454
526
  /**
@@ -457,7 +529,12 @@ export class LTITool {
457
529
  * @returns Array of client configurations (without deployment details)
458
530
  */
459
531
  async listClients() {
460
- return await this.config.storage.listClients();
532
+ try {
533
+ return await this.config.storage.listClients();
534
+ }
535
+ catch (error) {
536
+ throw new Error(`[Client] Listing failed: ${formatError(error)}`);
537
+ }
461
538
  }
462
539
  /**
463
540
  * Updates an existing client configuration.
@@ -466,8 +543,13 @@ export class LTITool {
466
543
  * @param client - Partial client object with fields to update
467
544
  */
468
545
  async updateClient(clientId, client) {
469
- const validated = UpdateClientSchema.parse(client);
470
- return await this.config.storage.updateClient(clientId, validated);
546
+ try {
547
+ const validated = UpdateClientSchema.parse(client);
548
+ return await this.config.storage.updateClient(clientId, validated);
549
+ }
550
+ catch (error) {
551
+ throw new Error(`[Client] Update failed for ID '${clientId}': ${formatError(error)}`);
552
+ }
471
553
  }
472
554
  /**
473
555
  * Retrieves a specific client configuration by ID.
@@ -476,7 +558,12 @@ export class LTITool {
476
558
  * @returns Client configuration if found, undefined otherwise
477
559
  */
478
560
  async getClientById(clientId) {
479
- return await this.config.storage.getClientById(clientId);
561
+ try {
562
+ return await this.config.storage.getClientById(clientId);
563
+ }
564
+ catch (error) {
565
+ throw new Error(`[Client] Retrieval failed for ID '${clientId}': ${formatError(error)}`);
566
+ }
480
567
  }
481
568
  /**
482
569
  * Adds a new LTI client platform configuration.
@@ -485,8 +572,13 @@ export class LTITool {
485
572
  * @returns The generated client ID
486
573
  */
487
574
  async addClient(client) {
488
- const validated = AddClientSchema.parse(client);
489
- return await this.config.storage.addClient(validated);
575
+ try {
576
+ const validated = AddClientSchema.parse(client);
577
+ return await this.config.storage.addClient(validated);
578
+ }
579
+ catch (error) {
580
+ throw new Error(`[Client] Creation failed for issuer '${client.iss}': ${formatError(error)}`);
581
+ }
490
582
  }
491
583
  /**
492
584
  * Removes a client configuration and all its deployments.
@@ -494,7 +586,12 @@ export class LTITool {
494
586
  * @param clientId - Unique client identifier
495
587
  */
496
588
  async deleteClient(clientId) {
497
- return await this.config.storage.deleteClient(clientId);
589
+ try {
590
+ return await this.config.storage.deleteClient(clientId);
591
+ }
592
+ catch (error) {
593
+ throw new Error(`[Client] Deletion failed for ID '${clientId}': ${formatError(error)}`);
594
+ }
498
595
  }
499
596
  // Deployment management
500
597
  /**
@@ -504,7 +601,12 @@ export class LTITool {
504
601
  * @returns Array of deployment configurations for the client
505
602
  */
506
603
  async listDeployments(clientId) {
507
- return await this.config.storage.listDeployments(clientId);
604
+ try {
605
+ return await this.config.storage.listDeployments(clientId);
606
+ }
607
+ catch (error) {
608
+ throw new Error(`[Deployment] Listing failed for client '${clientId}': ${formatError(error)}`);
609
+ }
508
610
  }
509
611
  /**
510
612
  * Retrieves a specific deployment configuration.
@@ -514,7 +616,12 @@ export class LTITool {
514
616
  * @returns Deployment configuration if found, undefined otherwise
515
617
  */
516
618
  async getDeployment(clientId, deploymentId) {
517
- return await this.config.storage.getDeployment(clientId, deploymentId);
619
+ try {
620
+ return await this.config.storage.getDeployment(clientId, deploymentId);
621
+ }
622
+ catch (error) {
623
+ throw new Error(`[Deployment] Retrieval failed for client '${clientId}', deployment '${deploymentId}': ${formatError(error)}`);
624
+ }
518
625
  }
519
626
  /**
520
627
  * Adds a new deployment to an existing client.
@@ -524,7 +631,12 @@ export class LTITool {
524
631
  * @returns The generated deployment ID
525
632
  */
526
633
  async addDeployment(clientId, deployment) {
527
- return await this.config.storage.addDeployment(clientId, deployment);
634
+ try {
635
+ return await this.config.storage.addDeployment(clientId, deployment);
636
+ }
637
+ catch (error) {
638
+ throw new Error(`[Deployment] Creation failed for client '${clientId}': ${formatError(error)}`);
639
+ }
528
640
  }
529
641
  /**
530
642
  * Updates an existing deployment configuration.
@@ -534,7 +646,12 @@ export class LTITool {
534
646
  * @param deployment - Partial deployment object with fields to update
535
647
  */
536
648
  async updateDeployment(clientId, deploymentId, deployment) {
537
- return await this.config.storage.updateDeployment(clientId, deploymentId, deployment);
649
+ try {
650
+ return await this.config.storage.updateDeployment(clientId, deploymentId, deployment);
651
+ }
652
+ catch (error) {
653
+ throw new Error(`Deployment update failed for client '${clientId}' and deployment '${deploymentId}': ${formatError(error)}`);
654
+ }
538
655
  }
539
656
  /**
540
657
  * Removes a deployment from a client.
@@ -543,7 +660,12 @@ export class LTITool {
543
660
  * @param deploymentId - Deployment identifier to remove
544
661
  */
545
662
  async deleteDeployment(clientId, deploymentId) {
546
- return await this.config.storage.deleteDeployment(clientId, deploymentId);
663
+ try {
664
+ return await this.config.storage.deleteDeployment(clientId, deploymentId);
665
+ }
666
+ catch (error) {
667
+ throw new Error(`[Deployment] Deletion failed for client '${clientId}', deployment '${deploymentId}': ${formatError(error)}`);
668
+ }
547
669
  }
548
670
  // Dynamic Registration Session Management
549
671
  /**
@@ -554,7 +676,12 @@ export class LTITool {
554
676
  * @param session - Registration session data including platform config and tokens
555
677
  */
556
678
  async setRegistrationSession(sessionId, session) {
557
- return await this.config.storage.setRegistrationSession(sessionId, session);
679
+ try {
680
+ return await this.config.storage.setRegistrationSession(sessionId, session);
681
+ }
682
+ catch (error) {
683
+ throw new Error(`[Dynamic Registration] Session storage failed for ID '${sessionId}': ${formatError(error)}`);
684
+ }
558
685
  }
559
686
  /**
560
687
  * Retrieves a registration session by its ID for validation during completion.
@@ -564,7 +691,12 @@ export class LTITool {
564
691
  * @returns Registration session if found and not expired, undefined otherwise
565
692
  */
566
693
  async getRegistrationSession(sessionId) {
567
- return await this.config.storage.getRegistrationSession(sessionId);
694
+ try {
695
+ return await this.config.storage.getRegistrationSession(sessionId);
696
+ }
697
+ catch (error) {
698
+ throw new Error(`[Dynamic Registration] Session retrieval failed for ID '${sessionId}': ${formatError(error)}`);
699
+ }
568
700
  }
569
701
  /**
570
702
  * Removes a registration session from storage after completion or expiration.
@@ -573,6 +705,37 @@ export class LTITool {
573
705
  * @param sessionId - Unique session identifier to delete
574
706
  */
575
707
  async deleteRegistrationSession(sessionId) {
576
- return await this.config.storage.deleteRegistrationSession(sessionId);
708
+ try {
709
+ return await this.config.storage.deleteRegistrationSession(sessionId);
710
+ }
711
+ catch (error) {
712
+ throw new Error(`[Dynamic Registration] Session deletion failed for ID '${sessionId}': ${formatError(error)}`);
713
+ }
714
+ }
715
+ }
716
+ /**
717
+ * Builds the authorization URL for LTI 1.3 OIDC authentication flow.
718
+ *
719
+ * @param launchConfig - Launch configuration containing auth endpoints
720
+ * @param validatedParams - Validated login parameters
721
+ * @param state - State JWT for CSRF protection
722
+ * @param nonce - Nonce for replay attack prevention
723
+ * @returns Complete authorization URL with all required parameters
724
+ */
725
+ function buildAuthUrl(launchConfig, validatedParams, state, nonce) {
726
+ const authUrl = new URL(launchConfig.authUrl);
727
+ authUrl.searchParams.set('scope', 'openid');
728
+ authUrl.searchParams.set('response_type', 'id_token');
729
+ authUrl.searchParams.set('response_mode', 'form_post');
730
+ authUrl.searchParams.set('prompt', 'none');
731
+ authUrl.searchParams.set('client_id', validatedParams.client_id);
732
+ authUrl.searchParams.set('redirect_uri', validatedParams.launchUrl.toString());
733
+ authUrl.searchParams.set('login_hint', validatedParams.login_hint);
734
+ authUrl.searchParams.set('state', state);
735
+ authUrl.searchParams.set('nonce', nonce);
736
+ authUrl.searchParams.set('lti_deployment_id', validatedParams.lti_deployment_id);
737
+ if (validatedParams.lti_message_hint) {
738
+ authUrl.searchParams.set('lti_message_hint', validatedParams.lti_message_hint);
577
739
  }
740
+ return authUrl.toString();
578
741
  }