@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.
@@ -28,7 +28,15 @@ const LTIDeepLinkingMessageSchema = z.object({
28
28
  target_link_uri: z.url().optional(),
29
29
  label: z.string().optional(),
30
30
  placements: z
31
- .array(z.enum(['ContentArea', 'RichTextEditor', 'CourseNavigation']))
31
+ .array(
32
+ z.enum([
33
+ 'editor_button',
34
+ 'assignment_selection',
35
+ 'link_selection',
36
+ 'module_index_menu_modal',
37
+ 'module_menu_modal',
38
+ ]),
39
+ )
32
40
  .optional(),
33
41
  supported_types: z
34
42
  .array(z.enum(['ltiResourceLink', 'file', 'html', 'link', 'image']))
@@ -189,9 +189,16 @@ export class DynamicRegistrationService {
189
189
  throw new Error('Invalid or expired session');
190
190
  }
191
191
 
192
+ // Extract platform family from session
193
+ const platformFamily =
194
+ session.openIdConfiguration[
195
+ 'https://purl.imsglobal.org/spec/lti-platform-configuration'
196
+ ].product_family_code;
197
+
192
198
  // 1. build payload
193
199
  const toolRegistrationPayload = this.buildRegistrationPayload(
194
200
  dynamicRegistrationForm.services ?? [],
201
+ platformFamily,
195
202
  );
196
203
 
197
204
  // 2. Post request to Moodle
@@ -249,29 +256,88 @@ export class DynamicRegistrationService {
249
256
  return session;
250
257
  }
251
258
 
259
+ /**
260
+ * Builds Canvas-specific deep linking messages for the 5 common placements.
261
+ * Creates separate messages for editor, module menu, assignments, modules page, and link selection.
262
+ *
263
+ * @param deepLinkingUri - URI where deep linking requests should be sent
264
+ * @param toolName - Display name of the tool
265
+ * @returns Array of Canvas deep linking message configurations
266
+ */
267
+ private buildCanvasDeepLinkingMessages(
268
+ deepLinkingUri: string,
269
+ toolName: string,
270
+ ): LTIMessage[] {
271
+ return [
272
+ {
273
+ type: 'LtiDeepLinkingRequest' as const,
274
+ target_link_uri: deepLinkingUri,
275
+ label: toolName,
276
+ placements: ['editor_button' as const],
277
+ supported_types: ['ltiResourceLink' as const],
278
+ },
279
+ {
280
+ type: 'LtiDeepLinkingRequest' as const,
281
+ target_link_uri: deepLinkingUri,
282
+ label: toolName,
283
+ placements: ['module_menu_modal' as const],
284
+ supported_types: ['ltiResourceLink' as const],
285
+ },
286
+ {
287
+ type: 'LtiDeepLinkingRequest' as const,
288
+ target_link_uri: deepLinkingUri,
289
+ label: toolName,
290
+ placements: ['assignment_selection' as const],
291
+ supported_types: ['ltiResourceLink' as const],
292
+ },
293
+ {
294
+ type: 'LtiDeepLinkingRequest' as const,
295
+ target_link_uri: deepLinkingUri,
296
+ label: toolName,
297
+ placements: ['module_index_menu_modal' as const],
298
+ supported_types: ['ltiResourceLink' as const],
299
+ },
300
+ {
301
+ type: 'LtiDeepLinkingRequest' as const,
302
+ target_link_uri: deepLinkingUri,
303
+ label: toolName,
304
+ placements: ['link_selection' as const],
305
+ supported_types: ['ltiResourceLink' as const],
306
+ },
307
+ ];
308
+ }
309
+
252
310
  /**
253
311
  * Builds array of LTI message types based on selected services during registration.
254
312
  * Always includes ResourceLinkRequest, conditionally adds DeepLinkingRequest.
255
313
  *
256
314
  * @param selectedServices - Array of service names selected by administrator
257
315
  * @param deepLinkingUri - URI where deep linking requests should be sent
316
+ * @param platformFamily - Platform family code (e.g., 'canvas', 'moodle')
317
+ * @param toolName - Display name of the tool
258
318
  * @returns Array of LTI message configurations for the registration payload
259
319
  */
260
320
  private buildMessages(
261
321
  selectedServices: string[],
262
322
  deepLinkingUri: string,
323
+ platformFamily: string,
324
+ toolName: string,
263
325
  ): LTIMessage[] {
264
- const messages = [];
326
+ const messages: LTIMessage[] = [];
265
327
  messages.push({ type: 'LtiResourceLinkRequest' as const });
266
328
 
267
329
  if (selectedServices?.includes('deep_linking')) {
268
- messages.push({
269
- type: 'LtiDeepLinkingRequest' as const,
270
- target_link_uri: deepLinkingUri,
271
- label: 'Content Selection',
272
- placements: ['ContentArea' as const], // Focus on content area only
273
- supported_types: ['ltiResourceLink' as const], // Standard content selection
274
- });
330
+ if (platformFamily.toLowerCase() === 'canvas') {
331
+ messages.push(...this.buildCanvasDeepLinkingMessages(deepLinkingUri, toolName));
332
+ } else {
333
+ messages.push({
334
+ type: 'LtiDeepLinkingRequest' as const,
335
+ target_link_uri: deepLinkingUri,
336
+ label: toolName,
337
+ placements: ['editor_button' as const],
338
+ supported_types: ['ltiResourceLink' as const],
339
+ });
340
+ }
275
341
  }
276
342
 
277
343
  return messages;
@@ -309,9 +375,13 @@ export class DynamicRegistrationService {
309
375
  * Combines tool configuration, selected services, and OAuth parameters into LTI 1.3 registration format.
310
376
  *
311
377
  * @param selectedServices - Array of service names selected by administrator
378
+ * @param platformFamily - Platform family code (e.g., 'canvas', 'moodle')
312
379
  * @returns Complete registration payload ready for platform submission
313
380
  */
314
- private buildRegistrationPayload(selectedServices: string[]): ToolRegistrationPayload {
381
+ private buildRegistrationPayload(
382
+ selectedServices: string[],
383
+ platformFamily: string,
384
+ ): ToolRegistrationPayload {
315
385
  const config = this.dynamicRegistrationConfig;
316
386
 
317
387
  const deepLinkingUri = config.deepLinkingUri || `${config.url}/lti/deep-linking`;
@@ -319,7 +389,12 @@ export class DynamicRegistrationService {
319
389
  const launchUri = config.launchUri || `${config.url}/lti/launch`;
320
390
  const loginUri = config.loginUri || `${config.url}/lti/login`;
321
391
 
322
- const messages = this.buildMessages(selectedServices, deepLinkingUri);
392
+ const messages = this.buildMessages(
393
+ selectedServices,
394
+ deepLinkingUri,
395
+ platformFamily,
396
+ config.name,
397
+ );
323
398
  const scopes = this.buildScopes(selectedServices);
324
399
 
325
400
  const toolRegistrationPayload: ToolRegistrationPayload = {
@@ -173,7 +173,7 @@ export async function postRegistrationToMoodle(
173
173
  if (!response.ok) {
174
174
  const errorText = await response.json();
175
175
  logger.error({ errorText }, 'lti dynamic registration error');
176
- throw new Error(errorText);
176
+ throw new Error(JSON.stringify(errorText));
177
177
  }
178
178
 
179
179
  const data = await response.json();
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Formats an unknown error into a readable string message.
3
+ * Handles Error objects, strings, and other types safely.
4
+ *
5
+ * @param error - The error to format (can be Error, string, or any other type)
6
+ * @returns Formatted error message string
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * try {
11
+ * await riskyOperation();
12
+ * } catch (error) {
13
+ * throw new Error(`Operation failed: ${formatError(error)}`);
14
+ * }
15
+ * ```
16
+ */
17
+ export function formatError(error: unknown): string {
18
+ if (error instanceof Error) {
19
+ return error.message;
20
+ }
21
+ return String(error);
22
+ }