@lti-tool/core 0.11.0 → 0.12.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 (59) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/interfaces/index.d.ts +1 -0
  3. package/dist/interfaces/index.d.ts.map +1 -1
  4. package/dist/interfaces/ltiConfig.d.ts +23 -0
  5. package/dist/interfaces/ltiConfig.d.ts.map +1 -1
  6. package/dist/interfaces/ltiDynamicRegistrationSession.d.ts +14 -0
  7. package/dist/interfaces/ltiDynamicRegistrationSession.d.ts.map +1 -0
  8. package/dist/interfaces/ltiDynamicRegistrationSession.js +1 -0
  9. package/dist/interfaces/ltiStorage.d.ts +22 -0
  10. package/dist/interfaces/ltiStorage.d.ts.map +1 -1
  11. package/dist/ltiTool.d.ts +64 -1
  12. package/dist/ltiTool.d.ts.map +1 -1
  13. package/dist/ltiTool.js +87 -1
  14. package/dist/schemas/index.d.ts +5 -0
  15. package/dist/schemas/index.d.ts.map +1 -1
  16. package/dist/schemas/index.js +4 -0
  17. package/dist/schemas/lti13/dynamicRegistration/ltiDynamicRegistration.schema.d.ts +34 -0
  18. package/dist/schemas/lti13/dynamicRegistration/ltiDynamicRegistration.schema.d.ts.map +1 -0
  19. package/dist/schemas/lti13/dynamicRegistration/ltiDynamicRegistration.schema.js +28 -0
  20. package/dist/schemas/lti13/dynamicRegistration/ltiMessages.schema.d.ts +101 -0
  21. package/dist/schemas/lti13/dynamicRegistration/ltiMessages.schema.d.ts.map +1 -0
  22. package/dist/schemas/lti13/dynamicRegistration/ltiMessages.schema.js +51 -0
  23. package/dist/schemas/lti13/dynamicRegistration/openIDConfiguration.schema.d.ts +59 -0
  24. package/dist/schemas/lti13/dynamicRegistration/openIDConfiguration.schema.d.ts.map +1 -0
  25. package/dist/schemas/lti13/dynamicRegistration/openIDConfiguration.schema.js +53 -0
  26. package/dist/schemas/lti13/dynamicRegistration/registrationRequest.schema.d.ts +7 -0
  27. package/dist/schemas/lti13/dynamicRegistration/registrationRequest.schema.d.ts.map +1 -0
  28. package/dist/schemas/lti13/dynamicRegistration/registrationRequest.schema.js +5 -0
  29. package/dist/schemas/lti13/dynamicRegistration/registrationResponse.schema.d.ts +73 -0
  30. package/dist/schemas/lti13/dynamicRegistration/registrationResponse.schema.d.ts.map +1 -0
  31. package/dist/schemas/lti13/dynamicRegistration/registrationResponse.schema.js +61 -0
  32. package/dist/schemas/lti13/dynamicRegistration/toolRegistrationPayload.schema.d.ts +103 -0
  33. package/dist/schemas/lti13/dynamicRegistration/toolRegistrationPayload.schema.d.ts.map +1 -0
  34. package/dist/schemas/lti13/dynamicRegistration/toolRegistrationPayload.schema.js +49 -0
  35. package/dist/services/dynamicRegistration.service.d.ts +121 -0
  36. package/dist/services/dynamicRegistration.service.d.ts.map +1 -0
  37. package/dist/services/dynamicRegistration.service.js +312 -0
  38. package/dist/services/dynamicRegistrationHandlers/moodle.d.ts +48 -0
  39. package/dist/services/dynamicRegistrationHandlers/moodle.d.ts.map +1 -0
  40. package/dist/services/dynamicRegistrationHandlers/moodle.js +152 -0
  41. package/dist/utils/ltiPlatformCapabilities.d.ts +65 -0
  42. package/dist/utils/ltiPlatformCapabilities.d.ts.map +1 -0
  43. package/dist/utils/ltiPlatformCapabilities.js +73 -0
  44. package/package.json +3 -3
  45. package/src/interfaces/index.ts +1 -0
  46. package/src/interfaces/ltiConfig.ts +25 -0
  47. package/src/interfaces/ltiDynamicRegistrationSession.ts +14 -0
  48. package/src/interfaces/ltiStorage.ts +32 -0
  49. package/src/ltiTool.ts +122 -1
  50. package/src/schemas/index.ts +17 -0
  51. package/src/schemas/lti13/dynamicRegistration/ltiDynamicRegistration.schema.ts +31 -0
  52. package/src/schemas/lti13/dynamicRegistration/ltiMessages.schema.ts +59 -0
  53. package/src/schemas/lti13/dynamicRegistration/openIDConfiguration.schema.ts +60 -0
  54. package/src/schemas/lti13/dynamicRegistration/registrationRequest.schema.ts +8 -0
  55. package/src/schemas/lti13/dynamicRegistration/registrationResponse.schema.ts +67 -0
  56. package/src/schemas/lti13/dynamicRegistration/toolRegistrationPayload.schema.ts +55 -0
  57. package/src/services/dynamicRegistration.service.ts +406 -0
  58. package/src/services/dynamicRegistrationHandlers/moodle.ts +184 -0
  59. package/src/utils/ltiPlatformCapabilities.ts +86 -0
@@ -0,0 +1,312 @@
1
+ import { openIDConfigurationSchema, } from '../schemas/lti13/dynamicRegistration/openIDConfiguration.schema.js';
2
+ import { handleMoodleDynamicRegistration, postRegistrationToMoodle, } from './dynamicRegistrationHandlers/moodle.js';
3
+ /**
4
+ * Service for handling LTI 1.3 dynamic registration workflows.
5
+ *
6
+ * Provides a complete implementation of the LTI 1.3 Dynamic Registration specification,
7
+ * enabling tools to automatically register with LTI platforms without manual configuration.
8
+ * Handles the full registration lifecycle from initiation to completion with security validation.
9
+ *
10
+ * ## Key Features
11
+ * - **Platform Discovery**: Fetches and validates OpenID Connect configuration from LTI platforms
12
+ * - **Security Validation**: Enforces hostname matching and session-based CSRF protection
13
+ * - **Vendor Support**: Provides platform-specific registration forms (Moodle, with extensibility for Canvas, Sakai, etc.)
14
+ * - **Service Selection**: Allows administrators to choose which LTI Advantage services to enable (AGS, NRPS, Deep Linking)
15
+ * - **Automatic Storage**: Persists client and deployment configurations for future launches
16
+ *
17
+ * ## Registration Flow
18
+ * 1. **Initiation**: Platform redirects to tool with registration request
19
+ * 2. **Discovery**: Tool fetches platform's OpenID Connect configuration
20
+ * 3. **Form Generation**: Tool presents service selection form to administrator
21
+ * 4. **Registration**: Tool submits registration payload to platform
22
+ * 5. **Storage**: Tool stores received client credentials and deployment information
23
+ *
24
+ * ## Security Features
25
+ * - Session-based registration with 15-minute expiration
26
+ * - CSRF protection via secure session tokens
27
+ * - Hostname validation between OIDC endpoint and issuer
28
+ * - One-time session consumption to prevent replay attacks
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const service = new DynamicRegistrationService(
33
+ * storage,
34
+ * dynamicRegistrationConfig,
35
+ * logger
36
+ * );
37
+ *
38
+ * // Initiate registration
39
+ * const formHtml = await service.initiateDynamicRegistration(request, '/lti/register');
40
+ *
41
+ * // Complete registration
42
+ * const successHtml = await service.completeDynamicRegistration(formData);
43
+ * ```
44
+ *
45
+ * @see https://www.imsglobal.org/spec/lti-dr/v1p0 LTI 1.3 Dynamic Registration specification
46
+ */
47
+ export class DynamicRegistrationService {
48
+ storage;
49
+ dynamicRegistrationConfig;
50
+ logger;
51
+ constructor(storage, dynamicRegistrationConfig, logger) {
52
+ this.storage = storage;
53
+ this.dynamicRegistrationConfig = dynamicRegistrationConfig;
54
+ this.logger = logger;
55
+ }
56
+ /**
57
+ * Fetches and validates the OpenID Connect configuration from an LTI platform during dynamic registration.
58
+ * Validates that the OIDC endpoint and issuer have matching hostnames for security.
59
+ *
60
+ * @param registrationRequest - Registration request containing openid_configuration URL and optional registration_token
61
+ * @returns Validated OpenID configuration with platform endpoints and supported features
62
+ * @throws {Error} When the configuration fetch fails, validation fails, or hostname mismatch detected
63
+ */
64
+ async fetchPlatformConfiguration(registrationRequest) {
65
+ const { openid_configuration, registration_token } = registrationRequest;
66
+ const response = await fetch(openid_configuration, {
67
+ method: 'GET',
68
+ headers: {
69
+ // only include registration token if it was provided
70
+ ...(registration_token && { Authorization: `Bearer ${registration_token}` }),
71
+ Accept: 'application/json',
72
+ },
73
+ });
74
+ await this.validateDynamicRegistrationResponse(response, 'validateRegistrationRequest');
75
+ const data = await response.json();
76
+ const openIdConfiguration = openIDConfigurationSchema.parse(data);
77
+ this.logger.debug({ openIdConfiguration });
78
+ // validate that the endpoint and issuer have the same hostname
79
+ const oidcEndpoint = new URL(openid_configuration);
80
+ const { issuer } = openIdConfiguration;
81
+ const issuerEndpoint = new URL(issuer);
82
+ if (oidcEndpoint.hostname !== issuerEndpoint.hostname) {
83
+ const errorMessage = `OIDC endpoint and issuer in OIDC payload do not match, cannot continue. OIDC endpoint: ${oidcEndpoint} issuer endpoint: ${issuerEndpoint}`;
84
+ this.logger.error(errorMessage);
85
+ throw new Error(errorMessage);
86
+ }
87
+ // good to continue!
88
+ return openIdConfiguration;
89
+ }
90
+ /**
91
+ * Initiates LTI 1.3 dynamic registration by fetching platform configuration and generating registration form.
92
+ * Creates a temporary session and returns vendor-specific HTML form for service selection.
93
+ *
94
+ * @param registrationRequest - Registration request containing openid_configuration URL and optional registration_token
95
+ * @param requestPath - Current request path used to build form action URLs
96
+ * @returns HTML form for service selection and registration completion
97
+ * @throws {Error} When platform configuration fetch fails or session creation fails
98
+ */
99
+ async initiateDynamicRegistration(registrationRequest, requestPath) {
100
+ // 1. Validate request
101
+ const openIdConfiguration = await this.fetchPlatformConfiguration(registrationRequest);
102
+ // 2. generate and store session
103
+ const sessionToken = crypto.randomUUID();
104
+ await this.storage.setRegistrationSession(sessionToken, {
105
+ openIdConfiguration,
106
+ registrationToken: registrationRequest.registration_token,
107
+ expiresAt: Date.now() + 15 * 60 * 1000, // 15 minutes
108
+ });
109
+ // 3. build form based on LMS vendor
110
+ const { product_family_code } = openIdConfiguration['https://purl.imsglobal.org/spec/lti-platform-configuration'];
111
+ let html;
112
+ switch (product_family_code.toLowerCase()) {
113
+ case 'moodle':
114
+ html = handleMoodleDynamicRegistration(openIdConfiguration, requestPath, sessionToken);
115
+ break;
116
+ default:
117
+ html = handleMoodleDynamicRegistration(openIdConfiguration, requestPath, sessionToken);
118
+ break;
119
+ }
120
+ return html;
121
+ }
122
+ /**
123
+ * Completes LTI 1.3 dynamic registration by processing form submission and storing client configuration.
124
+ * Validates session, registers with platform, stores client/deployment data, and returns success page.
125
+ *
126
+ * @param dynamicRegistrationForm - Validated form data containing selected services and session token
127
+ * @returns HTML success page with registration details and close button
128
+ * @throws {Error} When session is invalid, registration fails, or storage operations fail
129
+ */
130
+ async completeDynamicRegistration(dynamicRegistrationForm) {
131
+ // 1. Verify session token
132
+ const session = await this.verifyRegistrationSession(dynamicRegistrationForm.sessionToken);
133
+ if (!session) {
134
+ throw new Error('Invalid or expired session');
135
+ }
136
+ // 1. build payload
137
+ const toolRegistrationPayload = this.buildRegistrationPayload(dynamicRegistrationForm.services ?? []);
138
+ // 2. Post request to Moodle
139
+ const registrationResponse = await postRegistrationToMoodle(session.openIdConfiguration.registration_endpoint, toolRegistrationPayload, this.logger, session.registrationToken);
140
+ // 3. save to storage
141
+ const clientId = await this.storage.addClient({
142
+ name: registrationResponse.client_name,
143
+ clientId: registrationResponse.client_id,
144
+ iss: session.openIdConfiguration.issuer,
145
+ jwksUrl: session.openIdConfiguration.jwks_uri,
146
+ authUrl: session.openIdConfiguration.authorization_endpoint,
147
+ tokenUrl: session.openIdConfiguration.token_endpoint,
148
+ });
149
+ const ltiToolConfig = registrationResponse['https://purl.imsglobal.org/spec/lti-tool-configuration'];
150
+ if (ltiToolConfig.deployment_id) {
151
+ await this.storage.addDeployment(clientId, {
152
+ deploymentId: ltiToolConfig.deployment_id,
153
+ name: 'Default Deployment via dynamic registration provided deployment id',
154
+ });
155
+ }
156
+ else {
157
+ // platform doesn't use a deployment id (canvas for example) - create default
158
+ await this.storage.addDeployment(clientId, {
159
+ deploymentId: 'default',
160
+ name: 'Default Deployment (Canvas-style)',
161
+ });
162
+ }
163
+ // 4. return success
164
+ const successHtml = this.getRegistrationSuccessHtml(registrationResponse);
165
+ return successHtml;
166
+ }
167
+ /**
168
+ * Verifies and consumes a registration session token for security validation.
169
+ * Retrieves the session data and immediately deletes it to prevent replay attacks.
170
+ *
171
+ * @param sessionToken - UUID session token from the registration form
172
+ * @returns Session data if valid and not expired, undefined otherwise
173
+ */
174
+ async verifyRegistrationSession(sessionToken) {
175
+ const session = await this.storage.getRegistrationSession(sessionToken);
176
+ if (session) {
177
+ await this.storage.deleteRegistrationSession(sessionToken);
178
+ }
179
+ return session;
180
+ }
181
+ /**
182
+ * Builds array of LTI message types based on selected services during registration.
183
+ * Always includes ResourceLinkRequest, conditionally adds DeepLinkingRequest.
184
+ *
185
+ * @param selectedServices - Array of service names selected by administrator
186
+ * @param deepLinkingUri - URI where deep linking requests should be sent
187
+ * @returns Array of LTI message configurations for the registration payload
188
+ */
189
+ buildMessages(selectedServices, deepLinkingUri) {
190
+ const messages = [];
191
+ messages.push({ type: 'LtiResourceLinkRequest' });
192
+ if (selectedServices?.includes('deep_linking')) {
193
+ messages.push({
194
+ type: 'LtiDeepLinkingRequest',
195
+ target_link_uri: deepLinkingUri,
196
+ label: 'Content Selection',
197
+ placements: ['ContentArea'], // Focus on content area only
198
+ supported_types: ['ltiResourceLink'], // Standard content selection
199
+ });
200
+ }
201
+ return messages;
202
+ }
203
+ /**
204
+ * Builds array of OAuth scopes based on selected LTI services during registration.
205
+ * Maps service selections to their corresponding LTI Advantage scope URIs.
206
+ *
207
+ * @param selectedServices - Array of service names selected by administrator ('ags', 'nrps', etc.)
208
+ * @returns Array of OAuth scope URIs to request from the platform
209
+ */
210
+ buildScopes(selectedServices) {
211
+ const scopes = [];
212
+ if (selectedServices?.includes('ags')) {
213
+ scopes.push('https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', 'https://purl.imsglobal.org/spec/lti-ags/scope/score');
214
+ }
215
+ if (selectedServices?.includes('nrps')) {
216
+ scopes.push('https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly');
217
+ }
218
+ return scopes;
219
+ }
220
+ /**
221
+ * Constructs the complete tool registration payload for platform submission.
222
+ * Combines tool configuration, selected services, and OAuth parameters into LTI 1.3 registration format.
223
+ *
224
+ * @param selectedServices - Array of service names selected by administrator
225
+ * @returns Complete registration payload ready for platform submission
226
+ */
227
+ buildRegistrationPayload(selectedServices) {
228
+ const config = this.dynamicRegistrationConfig;
229
+ const deepLinkingUri = config.deepLinkingUri || `${config.url}/lti/deep-linking`;
230
+ const jwksUri = config.jwksUri || `${config.url}/lti/jwks`;
231
+ const launchUri = config.launchUri || `${config.url}/lti/launch`;
232
+ const loginUri = config.loginUri || `${config.url}/lti/login`;
233
+ const messages = this.buildMessages(selectedServices, deepLinkingUri);
234
+ const scopes = this.buildScopes(selectedServices);
235
+ const toolRegistrationPayload = {
236
+ application_type: 'web',
237
+ response_types: ['id_token'],
238
+ grant_types: ['implicit', 'client_credentials'],
239
+ initiate_login_uri: loginUri,
240
+ redirect_uris: [config.url, launchUri, ...(config.redirectUris || [])],
241
+ client_name: config.name,
242
+ jwks_uri: jwksUri,
243
+ logo_uri: config.logo,
244
+ scope: scopes.join(' '),
245
+ token_endpoint_auth_method: 'private_key_jwt',
246
+ 'https://purl.imsglobal.org/spec/lti-tool-configuration': {
247
+ domain: new URL(config.url).hostname,
248
+ description: config.description,
249
+ target_link_uri: config.url,
250
+ claims: ['iss', 'sub', 'name', 'email'],
251
+ messages,
252
+ },
253
+ };
254
+ return toolRegistrationPayload;
255
+ }
256
+ async validateDynamicRegistrationResponse(response, operation) {
257
+ if (!response.ok) {
258
+ const error = await response.json();
259
+ this.logger.error({ error, status: response.status, statusText: response.statusText }, `Dynamic Registration ${operation} failed`);
260
+ throw new Error(`Dynamic Registration ${operation} failed: ${response.statusText} ${error}`);
261
+ }
262
+ }
263
+ getRegistrationSuccessHtml(registrationResponse) {
264
+ return `
265
+ <!doctype html>
266
+ <html lang="en">
267
+ <head>
268
+ <meta charset="utf-8">
269
+ <meta name="viewport" content="width=device-width, initial-scale=1">
270
+ <title>Registration Complete</title>
271
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
272
+ </head>
273
+ <body class="container mt-4">
274
+ <div class="alert alert-success" role="alert">
275
+ <h4 class="alert-heading">Registration Successful!</h4>
276
+ <p>Your LTI tool has been successfully registered with the platform.</p>
277
+ <hr>
278
+ <p class="mb-0">You can now close this window and return to your LMS.</p>
279
+ </div>
280
+
281
+ <div class="card">
282
+ <div class="card-header">
283
+ <h5 class="card-title mb-0">Registration Details</h5>
284
+ </div>
285
+ <div class="card-body">
286
+ <dl class="row">
287
+ <dt class="col-sm-3">Tool Name:</dt>
288
+ <dd class="col-sm-9">${registrationResponse.client_name}</dd>
289
+ <dt class="col-sm-3">Client ID:</dt>
290
+ <dd class="col-sm-9"><code>${registrationResponse.client_id}</code></dd>
291
+ <dt class="col-sm-3">Deployment ID:</dt>
292
+ <dd class="col-sm-9"><code>${registrationResponse['https://purl.imsglobal.org/spec/lti-tool-configuration'].deployment_id || 'default'}</code></dd>
293
+ </dl>
294
+ </div>
295
+ </div>
296
+
297
+ <div class="mt-4 text-center">
298
+ <button type="button" class="btn btn-primary btn-lg" onclick="closeWindow()">
299
+ Close Window
300
+ </button>
301
+ </div>
302
+
303
+ <script>
304
+ function closeWindow() {
305
+ (window.opener || window.parent).postMessage({subject:'org.imsglobal.lti.close'}, '*');
306
+ }
307
+ </script>
308
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
309
+ </body>
310
+ </html>`;
311
+ }
312
+ }
@@ -0,0 +1,48 @@
1
+ import type { BaseLogger } from 'pino';
2
+ import { type OpenIDConfiguration, type RegistrationResponse } from '../../schemas';
3
+ /**
4
+ * Generates Moodle-specific dynamic registration form HTML with service selection options.
5
+ * Creates a Bootstrap 5 form that detects available LTI Advantage services from the platform
6
+ * configuration and presents them as selectable checkboxes to the administrator.
7
+ *
8
+ * @param openIdConfiguration - Platform's OpenID Connect configuration containing supported services
9
+ * @param currentPath - Current request path used to build the form submission URL
10
+ * @param sessionToken - Security token for CSRF protection and session validation
11
+ * @returns Complete HTML page with Bootstrap form for service selection
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const html = handleMoodleDynamicRegistration(
16
+ * platformConfig,
17
+ * '/lti/register',
18
+ * 'uuid-session-token'
19
+ * );
20
+ * // Returns HTML form with AGS, NRPS, and Deep Linking options if supported
21
+ * ```
22
+ */
23
+ export declare function handleMoodleDynamicRegistration(openIdConfiguration: OpenIDConfiguration, currentPath: string, sessionToken: string): string;
24
+ /**
25
+ * Submits tool registration payload to Moodle's dynamic registration endpoint.
26
+ * Handles the HTTP communication with proper authentication, error handling, and response validation.
27
+ * Validates the registration response against the LTI 1.3 specification schema.
28
+ *
29
+ * @param registrationEndpoint - Platform's registration endpoint URL from OpenID configuration
30
+ * @param registrationPayload - Complete tool registration payload with OAuth and LTI configuration
31
+ * @param logger - Pino logger instance for request/response logging and error tracking
32
+ * @param registrationToken - Optional bearer token for authenticated registration requests
33
+ * @returns Validated registration response containing client credentials and deployment information
34
+ * @throws {Error} When registration request fails or response validation fails
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const response = await postRegistrationToMoodle(
39
+ * 'https://moodle.edu/mod/lti/register.php',
40
+ * registrationPayload,
41
+ * logger,
42
+ * 'optional-bearer-token'
43
+ * );
44
+ * console.log('Registered with client ID:', response.client_id);
45
+ * ```
46
+ */
47
+ export declare function postRegistrationToMoodle(registrationEndpoint: string, registrationPayload: unknown, logger: BaseLogger, registrationToken?: string): Promise<RegistrationResponse>;
48
+ //# sourceMappingURL=moodle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moodle.d.ts","sourceRoot":"","sources":["../../../src/services/dynamicRegistrationHandlers/moodle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAEvC,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EAC1B,MAAM,eAAe,CAAC;AAQvB;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,wBAAgB,+BAA+B,CAC7C,mBAAmB,EAAE,mBAAmB,EACxC,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,MAAM,CA2FR;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,wBAAwB,CAC5C,oBAAoB,EAAE,MAAM,EAC5B,mBAAmB,EAAE,OAAO,EAC5B,MAAM,EAAE,UAAU,EAClB,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAuB/B"}
@@ -0,0 +1,152 @@
1
+ import { RegistrationResponseSchema, } from '../../schemas';
2
+ import { getAGSScopes, hasAGSSupport, hasDeepLinkingSupport, hasNRPSSupport, } from '../../utils/ltiPlatformCapabilities';
3
+ /**
4
+ * Generates Moodle-specific dynamic registration form HTML with service selection options.
5
+ * Creates a Bootstrap 5 form that detects available LTI Advantage services from the platform
6
+ * configuration and presents them as selectable checkboxes to the administrator.
7
+ *
8
+ * @param openIdConfiguration - Platform's OpenID Connect configuration containing supported services
9
+ * @param currentPath - Current request path used to build the form submission URL
10
+ * @param sessionToken - Security token for CSRF protection and session validation
11
+ * @returns Complete HTML page with Bootstrap form for service selection
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const html = handleMoodleDynamicRegistration(
16
+ * platformConfig,
17
+ * '/lti/register',
18
+ * 'uuid-session-token'
19
+ * );
20
+ * // Returns HTML form with AGS, NRPS, and Deep Linking options if supported
21
+ * ```
22
+ */
23
+ // oxlint-disable-next-line max-lines-per-function
24
+ export function handleMoodleDynamicRegistration(openIdConfiguration, currentPath, sessionToken) {
25
+ const hasAGS = hasAGSSupport(openIdConfiguration);
26
+ const hasNRPS = hasNRPSSupport(openIdConfiguration);
27
+ const hasDeepLinking = hasDeepLinkingSupport(openIdConfiguration);
28
+ const agsScopes = getAGSScopes(openIdConfiguration);
29
+ // Build complete action from current path
30
+ const completeAction = `${currentPath}/complete`;
31
+ return `
32
+ <!doctype html>
33
+ <html lang="en">
34
+ <head>
35
+ <meta charset="utf-8">
36
+ <meta name="viewport" content="width=device-width, initial-scale=1">
37
+ <title>Configure LTI Advantage Settings for Moodle</title>
38
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
39
+ </head>
40
+ <body class="container mt-4">
41
+ <form method="POST" action="${completeAction}">
42
+ <div class="mb-3">
43
+ <label class="form-label">Available Services</label>
44
+ ${hasAGS
45
+ ? `
46
+ <div class="form-check">
47
+ <input class="form-check-input" type="checkbox" name="services" value="ags" id="ags" checked>
48
+ <label class="form-check-label" for="ags">
49
+ <strong>Assignment and Grade Services (AGS)</strong>
50
+ <small class="text-muted d-block">Enables automatic grade passback from this tool to your gradebook</small>
51
+ </label>
52
+ <div class="mt-2 ms-4">
53
+ <small class="text-muted">OAuth Scopes that will be requested:</small>
54
+ <pre class="bg-light p-2 mt-1 small border rounded">${agsScopes.map((scope) => scope.replace('https://purl.imsglobal.org/spec/lti-ags/scope/', '')).join('\n')}</pre>
55
+ </div>
56
+ </div>
57
+ `
58
+ : ''}
59
+
60
+ ${hasNRPS
61
+ ? `
62
+ <div class="form-check">
63
+ <input class="form-check-input" type="checkbox" name="services" value="nrps" id="nrps" checked>
64
+ <label class="form-check-label" for="nrps">Names and Role Provisioning Services (NRPS)</label>
65
+ </div>
66
+ `
67
+ : ''}
68
+
69
+ ${hasDeepLinking
70
+ ? `
71
+ <div class="form-check">
72
+ <input class="form-check-input" type="checkbox" name="services" value="deep_linking" id="deep_linking" checked>
73
+ <label class="form-check-label" for="deep_linking">Deep Linking</label>
74
+ </div>
75
+ `
76
+ : ''}
77
+ </div>
78
+ <div class="mb-3">
79
+ <label class="form-label">Required Privacy Settings</label>
80
+ <div class="alert alert-info">
81
+ <small>These privacy settings must be enabled in your LMS for this tool to function properly.</small>
82
+ </div>
83
+
84
+ <div class="form-check">
85
+ <input class="form-check-input" type="checkbox" id="share_name" checked disabled>
86
+ <label class="form-check-label" for="share_name">
87
+ Share launcher's name
88
+ <small class="text-muted d-block">Required for user identification</small>
89
+ </label>
90
+ </div>
91
+
92
+ <div class="form-check">
93
+ <input class="form-check-input" type="checkbox" id="share_email" checked disabled>
94
+ <label class="form-check-label" for="share_email">
95
+ Share launcher's email
96
+ <small class="text-muted d-block">Required for user communication</small>
97
+ </label>
98
+ </div>
99
+ </div>
100
+
101
+ <input type="hidden" name="sessionToken" value="${sessionToken}">
102
+
103
+ <button type="submit" class="btn btn-primary">Register Tool</button>
104
+ </form>
105
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
106
+ </body>
107
+ </html>`;
108
+ }
109
+ /**
110
+ * Submits tool registration payload to Moodle's dynamic registration endpoint.
111
+ * Handles the HTTP communication with proper authentication, error handling, and response validation.
112
+ * Validates the registration response against the LTI 1.3 specification schema.
113
+ *
114
+ * @param registrationEndpoint - Platform's registration endpoint URL from OpenID configuration
115
+ * @param registrationPayload - Complete tool registration payload with OAuth and LTI configuration
116
+ * @param logger - Pino logger instance for request/response logging and error tracking
117
+ * @param registrationToken - Optional bearer token for authenticated registration requests
118
+ * @returns Validated registration response containing client credentials and deployment information
119
+ * @throws {Error} When registration request fails or response validation fails
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * const response = await postRegistrationToMoodle(
124
+ * 'https://moodle.edu/mod/lti/register.php',
125
+ * registrationPayload,
126
+ * logger,
127
+ * 'optional-bearer-token'
128
+ * );
129
+ * console.log('Registered with client ID:', response.client_id);
130
+ * ```
131
+ */
132
+ export async function postRegistrationToMoodle(registrationEndpoint, registrationPayload, logger, registrationToken) {
133
+ const headers = { 'Content-Type': 'application/json' };
134
+ if (registrationToken) {
135
+ headers['Authorization'] = `Bearer ${registrationToken}`;
136
+ }
137
+ const response = await fetch(registrationEndpoint, {
138
+ method: 'POST',
139
+ headers,
140
+ body: JSON.stringify(registrationPayload),
141
+ });
142
+ if (!response.ok) {
143
+ const errorText = await response.json();
144
+ logger.error({ errorText }, 'lti dynamic registration error');
145
+ throw new Error(errorText);
146
+ }
147
+ const data = await response.json();
148
+ logger.debug({ data }, 'Registration response');
149
+ const validated = RegistrationResponseSchema.parse(data);
150
+ logger.debug({ validated }, 'Registration response validated');
151
+ return validated;
152
+ }
@@ -0,0 +1,65 @@
1
+ import type { OpenIDConfiguration } from '../schemas';
2
+ /**
3
+ * Checks if an LTI platform supports Assignment and Grade Services (AGS).
4
+ * Examines the platform's OpenID configuration for AGS-related OAuth scopes.
5
+ *
6
+ * @param config - Platform's OpenID Connect configuration from discovery endpoint
7
+ * @returns True if the platform supports any AGS scopes, false otherwise
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * if (hasAGSSupport(platformConfig)) {
12
+ * // Show AGS checkbox in registration form
13
+ * // Enable grade passback functionality
14
+ * }
15
+ * ```
16
+ */
17
+ export declare function hasAGSSupport(config: OpenIDConfiguration): boolean;
18
+ /**
19
+ * Checks if an LTI platform supports Names and Role Provisioning Services (NRPS).
20
+ * Examines the platform's OpenID configuration for NRPS-related OAuth scopes.
21
+ *
22
+ * @param config - Platform's OpenID Connect configuration from discovery endpoint
23
+ * @returns True if the platform supports any NRPS scopes, false otherwise
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * if (hasNRPSSupport(platformConfig)) {
28
+ * // Show NRPS checkbox in registration form
29
+ * // Enable roster access functionality
30
+ * }
31
+ * ```
32
+ */
33
+ export declare function hasNRPSSupport(config: OpenIDConfiguration): boolean;
34
+ /**
35
+ * Checks if an LTI platform supports Deep Linking for content selection.
36
+ * Examines the platform's LTI configuration for supported message types.
37
+ *
38
+ * @param config - Platform's OpenID Connect configuration from discovery endpoint
39
+ * @returns True if the platform supports LtiDeepLinkingRequest messages, false otherwise
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * if (hasDeepLinkingSupport(platformConfig)) {
44
+ * // Show Deep Linking checkbox in registration form
45
+ * // Enable content selection functionality
46
+ * }
47
+ * ```
48
+ */
49
+ export declare function hasDeepLinkingSupport(config: OpenIDConfiguration): boolean;
50
+ /**
51
+ * Extracts all Assignment and Grade Services (AGS) scopes supported by the platform.
52
+ * Filters the platform's supported scopes to return only AGS-related scope URIs.
53
+ *
54
+ * @param config - Platform's OpenID Connect configuration from discovery endpoint
55
+ * @returns Array of AGS scope URIs supported by the platform (e.g., lineitem, score, result.readonly)
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const agsScopes = getAGSScopes(platformConfig);
60
+ * // Returns: ['https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', ...]
61
+ * console.log('Available AGS scopes:', agsScopes.join(', '));
62
+ * ```
63
+ */
64
+ export declare function getAGSScopes(config: OpenIDConfiguration): string[];
65
+ //# sourceMappingURL=ltiPlatformCapabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ltiPlatformCapabilities.d.ts","sourceRoot":"","sources":["../../src/utils/ltiPlatformCapabilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAIlE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAInE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAM1E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,EAAE,CAIlE"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Checks if an LTI platform supports Assignment and Grade Services (AGS).
3
+ * Examines the platform's OpenID configuration for AGS-related OAuth scopes.
4
+ *
5
+ * @param config - Platform's OpenID Connect configuration from discovery endpoint
6
+ * @returns True if the platform supports any AGS scopes, false otherwise
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * if (hasAGSSupport(platformConfig)) {
11
+ * // Show AGS checkbox in registration form
12
+ * // Enable grade passback functionality
13
+ * }
14
+ * ```
15
+ */
16
+ export function hasAGSSupport(config) {
17
+ return (config.scopes_supported?.some((scope) => scope.includes('lti-ags/scope')) ?? false);
18
+ }
19
+ /**
20
+ * Checks if an LTI platform supports Names and Role Provisioning Services (NRPS).
21
+ * Examines the platform's OpenID configuration for NRPS-related OAuth scopes.
22
+ *
23
+ * @param config - Platform's OpenID Connect configuration from discovery endpoint
24
+ * @returns True if the platform supports any NRPS scopes, false otherwise
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * if (hasNRPSSupport(platformConfig)) {
29
+ * // Show NRPS checkbox in registration form
30
+ * // Enable roster access functionality
31
+ * }
32
+ * ```
33
+ */
34
+ export function hasNRPSSupport(config) {
35
+ return (config.scopes_supported?.some((scope) => scope.includes('lti-nrps/scope')) ?? false);
36
+ }
37
+ /**
38
+ * Checks if an LTI platform supports Deep Linking for content selection.
39
+ * Examines the platform's LTI configuration for supported message types.
40
+ *
41
+ * @param config - Platform's OpenID Connect configuration from discovery endpoint
42
+ * @returns True if the platform supports LtiDeepLinkingRequest messages, false otherwise
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * if (hasDeepLinkingSupport(platformConfig)) {
47
+ * // Show Deep Linking checkbox in registration form
48
+ * // Enable content selection functionality
49
+ * }
50
+ * ```
51
+ */
52
+ export function hasDeepLinkingSupport(config) {
53
+ const ltiConfig = config['https://purl.imsglobal.org/spec/lti-platform-configuration'];
54
+ return (ltiConfig?.messages_supported?.some((msg) => msg.type === 'LtiDeepLinkingRequest') ??
55
+ false);
56
+ }
57
+ /**
58
+ * Extracts all Assignment and Grade Services (AGS) scopes supported by the platform.
59
+ * Filters the platform's supported scopes to return only AGS-related scope URIs.
60
+ *
61
+ * @param config - Platform's OpenID Connect configuration from discovery endpoint
62
+ * @returns Array of AGS scope URIs supported by the platform (e.g., lineitem, score, result.readonly)
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const agsScopes = getAGSScopes(platformConfig);
67
+ * // Returns: ['https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', ...]
68
+ * console.log('Available AGS scopes:', agsScopes.join(', '));
69
+ * ```
70
+ */
71
+ export function getAGSScopes(config) {
72
+ return (config.scopes_supported?.filter((scope) => scope.includes('lti-ags/scope')) ?? []);
73
+ }