@lti-tool/core 0.9.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 (116) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +60 -0
  3. package/dist/index.d.ts +4 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +3 -0
  6. package/dist/interfaces/index.d.ts +7 -0
  7. package/dist/interfaces/index.d.ts.map +1 -0
  8. package/dist/interfaces/index.js +1 -0
  9. package/dist/interfaces/jwks.d.ts +18 -0
  10. package/dist/interfaces/jwks.d.ts.map +1 -0
  11. package/dist/interfaces/jwks.js +1 -0
  12. package/dist/interfaces/ltiClient.d.ts +24 -0
  13. package/dist/interfaces/ltiClient.d.ts.map +1 -0
  14. package/dist/interfaces/ltiClient.js +1 -0
  15. package/dist/interfaces/ltiConfig.d.ts +26 -0
  16. package/dist/interfaces/ltiConfig.d.ts.map +1 -0
  17. package/dist/interfaces/ltiConfig.js +1 -0
  18. package/dist/interfaces/ltiDeployment.d.ts +15 -0
  19. package/dist/interfaces/ltiDeployment.d.ts.map +1 -0
  20. package/dist/interfaces/ltiDeployment.js +1 -0
  21. package/dist/interfaces/ltiLaunchConfig.d.ts +19 -0
  22. package/dist/interfaces/ltiLaunchConfig.d.ts.map +1 -0
  23. package/dist/interfaces/ltiLaunchConfig.js +1 -0
  24. package/dist/interfaces/ltiSession.d.ts +110 -0
  25. package/dist/interfaces/ltiSession.d.ts.map +1 -0
  26. package/dist/interfaces/ltiSession.js +1 -0
  27. package/dist/interfaces/ltiStorage.d.ts +122 -0
  28. package/dist/interfaces/ltiStorage.d.ts.map +1 -0
  29. package/dist/interfaces/ltiStorage.js +1 -0
  30. package/dist/ltiTool.d.ts +184 -0
  31. package/dist/ltiTool.d.ts.map +1 -0
  32. package/dist/ltiTool.js +305 -0
  33. package/dist/schemas/client.schema.d.ts +33 -0
  34. package/dist/schemas/client.schema.d.ts.map +1 -0
  35. package/dist/schemas/client.schema.js +14 -0
  36. package/dist/schemas/common.schema.d.ts +6 -0
  37. package/dist/schemas/common.schema.d.ts.map +1 -0
  38. package/dist/schemas/common.schema.js +5 -0
  39. package/dist/schemas/deployment.schema.d.ts +8 -0
  40. package/dist/schemas/deployment.schema.d.ts.map +1 -0
  41. package/dist/schemas/deployment.schema.js +11 -0
  42. package/dist/schemas/index.d.ts +5 -0
  43. package/dist/schemas/index.d.ts.map +1 -0
  44. package/dist/schemas/index.js +4 -0
  45. package/dist/schemas/lti13/ags/scoreSubmission.schema.d.ts +34 -0
  46. package/dist/schemas/lti13/ags/scoreSubmission.schema.d.ts.map +1 -0
  47. package/dist/schemas/lti13/ags/scoreSubmission.schema.js +41 -0
  48. package/dist/schemas/lti13/claims/baseJwtClaims.schema.d.ts +11 -0
  49. package/dist/schemas/lti13/claims/baseJwtClaims.schema.d.ts.map +1 -0
  50. package/dist/schemas/lti13/claims/baseJwtClaims.schema.js +10 -0
  51. package/dist/schemas/lti13/claims/contextClaims.schema.d.ts +11 -0
  52. package/dist/schemas/lti13/claims/contextClaims.schema.d.ts.map +1 -0
  53. package/dist/schemas/lti13/claims/contextClaims.schema.js +14 -0
  54. package/dist/schemas/lti13/claims/coreLtiClaims.schema.d.ts +9 -0
  55. package/dist/schemas/lti13/claims/coreLtiClaims.schema.d.ts.map +1 -0
  56. package/dist/schemas/lti13/claims/coreLtiClaims.schema.js +11 -0
  57. package/dist/schemas/lti13/claims/platformClaims.schema.d.ts +19 -0
  58. package/dist/schemas/lti13/claims/platformClaims.schema.d.ts.map +1 -0
  59. package/dist/schemas/lti13/claims/platformClaims.schema.js +24 -0
  60. package/dist/schemas/lti13/claims/privacyClaims.schema.d.ts +8 -0
  61. package/dist/schemas/lti13/claims/privacyClaims.schema.d.ts.map +1 -0
  62. package/dist/schemas/lti13/claims/privacyClaims.schema.js +7 -0
  63. package/dist/schemas/lti13/claims/serviceClaims.schema.d.ts +20 -0
  64. package/dist/schemas/lti13/claims/serviceClaims.schema.d.ts.map +1 -0
  65. package/dist/schemas/lti13/claims/serviceClaims.schema.js +25 -0
  66. package/dist/schemas/lti13/lti13JwtPayload.schema.d.ts +66 -0
  67. package/dist/schemas/lti13/lti13JwtPayload.schema.d.ts.map +1 -0
  68. package/dist/schemas/lti13/lti13JwtPayload.schema.js +22 -0
  69. package/dist/schemas/lti13/lti13Launch.schema.d.ts +14 -0
  70. package/dist/schemas/lti13/lti13Launch.schema.d.ts.map +1 -0
  71. package/dist/schemas/lti13/lti13Launch.schema.js +13 -0
  72. package/dist/schemas/lti13/lti13Login.schema.d.ts +23 -0
  73. package/dist/schemas/lti13/lti13Login.schema.d.ts.map +1 -0
  74. package/dist/schemas/lti13/lti13Login.schema.js +16 -0
  75. package/dist/services/ags.service.d.ts +38 -0
  76. package/dist/services/ags.service.d.ts.map +1 -0
  77. package/dist/services/ags.service.js +69 -0
  78. package/dist/services/session.service.d.ts +11 -0
  79. package/dist/services/session.service.d.ts.map +1 -0
  80. package/dist/services/session.service.js +103 -0
  81. package/dist/services/token.service.d.ts +36 -0
  82. package/dist/services/token.service.d.ts.map +1 -0
  83. package/dist/services/token.service.js +74 -0
  84. package/dist/utils/launchConfigValidation.d.ts +3 -0
  85. package/dist/utils/launchConfigValidation.d.ts.map +1 -0
  86. package/dist/utils/launchConfigValidation.js +7 -0
  87. package/package.json +53 -0
  88. package/src/index.ts +3 -0
  89. package/src/interfaces/index.ts +6 -0
  90. package/src/interfaces/jwks.ts +20 -0
  91. package/src/interfaces/ltiClient.ts +24 -0
  92. package/src/interfaces/ltiConfig.ts +31 -0
  93. package/src/interfaces/ltiDeployment.ts +17 -0
  94. package/src/interfaces/ltiLaunchConfig.ts +23 -0
  95. package/src/interfaces/ltiSession.ts +119 -0
  96. package/src/interfaces/ltiStorage.ts +161 -0
  97. package/src/ltiTool.ts +394 -0
  98. package/src/schemas/client.schema.ts +17 -0
  99. package/src/schemas/common.schema.ts +7 -0
  100. package/src/schemas/deployment.schema.ts +12 -0
  101. package/src/schemas/index.ts +10 -0
  102. package/src/schemas/lti13/ags/scoreSubmission.schema.ts +54 -0
  103. package/src/schemas/lti13/claims/baseJwtClaims.schema.ts +11 -0
  104. package/src/schemas/lti13/claims/contextClaims.schema.ts +16 -0
  105. package/src/schemas/lti13/claims/coreLtiClaims.schema.ts +12 -0
  106. package/src/schemas/lti13/claims/platformClaims.schema.ts +27 -0
  107. package/src/schemas/lti13/claims/privacyClaims.schema.ts +8 -0
  108. package/src/schemas/lti13/claims/serviceClaims.schema.ts +28 -0
  109. package/src/schemas/lti13/lti13JwtPayload.schema.ts +36 -0
  110. package/src/schemas/lti13/lti13Launch.schema.ts +15 -0
  111. package/src/schemas/lti13/lti13Login.schema.ts +18 -0
  112. package/src/services/ags.service.ts +92 -0
  113. package/src/services/session.service.ts +115 -0
  114. package/src/services/token.service.ts +84 -0
  115. package/src/utils/launchConfigValidation.ts +16 -0
  116. package/tsconfig.json +8 -0
@@ -0,0 +1,161 @@
1
+ import type { LTIClient } from './ltiClient.js';
2
+ import type { LTIDeployment } from './ltiDeployment.js';
3
+ import type { LTILaunchConfig } from './ltiLaunchConfig.js';
4
+ import type { LTISession } from './ltiSession.js';
5
+
6
+ /**
7
+ * Storage interface for persisting LTI Client configurations, user sessions, and security nonces.
8
+ * Implement this interface to use different storage backends (memory, database, Redis, etc.).
9
+ */
10
+ export interface LTIStorage {
11
+ // Client management
12
+
13
+ /**
14
+ * Retrieves all clients configured in the system.
15
+ *
16
+ * @returns Array of all client configurations
17
+ */
18
+ listClients(): Promise<Omit<LTIClient, 'deployments'>[]>;
19
+
20
+ /**
21
+ * Retrieves client configuration by its unique id.
22
+ *
23
+ * @param clientId - Unique client identifier
24
+ * @returns Client configuration if found, undefined otherwise
25
+ */
26
+ getClientById(clientId: string): Promise<LTIClient | undefined>;
27
+
28
+ /**
29
+ * Adds a new client configuration to storage.
30
+ *
31
+ * @param client - Partial client configuration object
32
+ */
33
+ addClient(client: Omit<LTIClient, 'id' | 'deployments'>): Promise<string>;
34
+
35
+ /**
36
+ * Updates an existing client configuration.
37
+ *
38
+ * @param clientId - Unique client identifier
39
+ * @param client - Partial client object with fields to update
40
+ */
41
+ updateClient(
42
+ clientId: string,
43
+ client: Partial<Omit<LTIClient, 'id' | 'deployments'>>,
44
+ ): Promise<void>;
45
+
46
+ /**
47
+ * Removes a client configuration from storage.
48
+ *
49
+ * @param clientId - Unique client identifier
50
+ */
51
+ deleteClient(clientId: string): Promise<void>;
52
+
53
+ // Deployment management
54
+
55
+ /**
56
+ * Lists all deployments for a specific client.
57
+ *
58
+ * @param clientId - Client identifier
59
+ * @returns Array of deployment configurations
60
+ */
61
+ listDeployments(clientId: string): Promise<LTIDeployment[]>;
62
+
63
+ /**
64
+ * Retrieves deployment configuration by client ID and deployment ID (admin use).
65
+ *
66
+ * @param clientId - Unique client identifier
67
+ * @param deploymentId - Deployment identifier
68
+ * @returns Deployment configuration if found, undefined otherwise
69
+ */
70
+ getDeployment(
71
+ clientId: string,
72
+ deploymentId: string,
73
+ ): Promise<LTIDeployment | undefined>;
74
+
75
+ /**
76
+ * Adds a new deployment to an existing client.
77
+ *
78
+ * @param clientId - Client identifier
79
+ * @param deployment - Deployment configuration to add
80
+ */
81
+ addDeployment(clientId: string, deployment: Omit<LTIDeployment, 'id'>): Promise<string>;
82
+
83
+ /**
84
+ * Updates an existing deployment configuration.
85
+ * @param clientId - Client identifier
86
+ * @param deploymentId - Deployment identifier to update
87
+ * @param deployment - Partial deployment object with fields to update
88
+ */
89
+ updateDeployment(
90
+ clientId: string,
91
+ deploymentId: string,
92
+ deployment: Partial<LTIDeployment>,
93
+ ): Promise<void>;
94
+
95
+ /**
96
+ * Removes a deployment from a Client.
97
+ *
98
+ * @param clientId - Client identifier
99
+ * @param deploymentId - Deployment identifier to remove
100
+ */
101
+ deleteDeployment(clientId: string, deploymentId: string): Promise<void>;
102
+
103
+ // Session management
104
+
105
+ /**
106
+ * Retrieves an active user session by session ID.
107
+ *
108
+ * @param sessionId - Unique session identifier (typically a UUID)
109
+ * @returns Session object if found and valid, undefined otherwise
110
+ */
111
+ getSession(sessionId: string): Promise<LTISession | undefined>;
112
+
113
+ /**
114
+ * Stores a new user session after successful LTI launch.
115
+ *
116
+ * @param session - Complete session object with user, context, and service data
117
+ * @returns The session ID for reference
118
+ */
119
+ addSession(session: LTISession): Promise<string>;
120
+
121
+ // Nonce validation (prevent replay attacks)
122
+
123
+ /**
124
+ * Stores a nonce with expiration time for replay attack prevention.
125
+ *
126
+ * @param nonce - Unique nonce value (typically a UUID)
127
+ * @param expiresAt - When this nonce should be considered expired
128
+ */
129
+ storeNonce(nonce: string, expiresAt: Date): Promise<void>;
130
+
131
+ /**
132
+ * Validates a nonce and marks it as used to prevent replay attacks.
133
+ *
134
+ * @param nonce - Nonce value to validate
135
+ * @returns true if nonce is valid and unused, false if already used or expired
136
+ */
137
+ validateNonce(nonce: string): Promise<boolean>;
138
+
139
+ // Launch configuration management
140
+
141
+ /**
142
+ * Retrieves launch configuration for LTI authentication flow.
143
+ *
144
+ * @param iss - Platform issuer URL (identifies the LMS)
145
+ * @param clientId - OAuth2 client identifier for this tool
146
+ * @param deploymentId - Deployment identifier within the platform
147
+ * @returns Launch configuration if found, undefined otherwise
148
+ */
149
+ getLaunchConfig(
150
+ iss: string,
151
+ clientId: string,
152
+ deploymentId: string,
153
+ ): Promise<LTILaunchConfig | undefined>;
154
+
155
+ /**
156
+ * Stores launch configuration for platform authentication.
157
+ *
158
+ * @param launchConfig - Complete launch configuration with auth URLs and keys
159
+ */
160
+ saveLaunchConfig(launchConfig: LTILaunchConfig): Promise<void>;
161
+ }
package/src/ltiTool.ts ADDED
@@ -0,0 +1,394 @@
1
+ import { SignJWT, createRemoteJWKSet, decodeJwt, exportJWK, jwtVerify } from 'jose';
2
+ import type { Logger } from 'pino';
3
+
4
+ import type { JWKS } from './interfaces/jwks.js';
5
+ import type { LTIClient } from './interfaces/ltiClient.js';
6
+ import type { LTIConfig } from './interfaces/ltiConfig.js';
7
+ import type { LTIDeployment } from './interfaces/ltiDeployment.js';
8
+ import type { LTISession } from './interfaces/ltiSession.js';
9
+ import { AddClientSchema, UpdateClientSchema } from './schemas/client.schema.js';
10
+ import {
11
+ HandleLoginParamsSchema,
12
+ type LTI13JwtPayload,
13
+ LTI13JwtPayloadSchema,
14
+ SessionIdSchema,
15
+ VerifyLaunchParamsSchema,
16
+ } from './schemas/index.js';
17
+ import type { ScoreSubmission } from './schemas/lti13/ags/scoreSubmission.schema.js';
18
+ import { AGSService } from './services/ags.service.js';
19
+ import { createSession } from './services/session.service.js';
20
+ import { TokenService } from './services/token.service.js';
21
+ import { getValidLaunchConfig } from './utils/launchConfigValidation.js';
22
+
23
+ /**
24
+ * Main LTI 1.3 Tool implementation providing secure authentication, launch verification,
25
+ * and LTI Advantage services integration.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const ltiTool = new LTITool({
30
+ * stateSecret: new TextEncoder().encode('your-secret'),
31
+ * keyPair: await generateKeyPair('RS256'),
32
+ * storage: new MemoryStorage()
33
+ * });
34
+ *
35
+ * // Handle login initiation
36
+ * const authUrl = await ltiTool.handleLogin({
37
+ * client_id: 'your-client-id',
38
+ * iss: 'https://platform.example.com',
39
+ * launchUrl: 'https://yourtool.com/lti/launch',
40
+ * login_hint: 'user123',
41
+ * target_link_uri: 'https://yourtool.com/content',
42
+ * lti_deployment_id: 'deployment123'
43
+ * });
44
+ * ```
45
+ */
46
+ export class LTITool {
47
+ /** Cache for JWKS remote key sets to improve performance */
48
+ private jwksCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
49
+ private logger: Logger;
50
+ private tokenService: TokenService;
51
+ private agsService: AGSService;
52
+
53
+ /**
54
+ * Creates a new LTI Tool instance.
55
+ *
56
+ * @param config - Configuration object containing secrets, keys, and storage adapter
57
+ */
58
+ constructor(private config: LTIConfig) {
59
+ this.logger =
60
+ config.logger ??
61
+ ({
62
+ debug: () => {},
63
+ info: () => {},
64
+ warn: () => {},
65
+ error: () => {},
66
+ } as unknown as Logger);
67
+
68
+ this.tokenService = new TokenService(
69
+ this.config.keyPair,
70
+ this.config.security?.keyId ?? 'main',
71
+ );
72
+ this.agsService = new AGSService(this.tokenService, this.config.storage, this.logger);
73
+ }
74
+
75
+ /**
76
+ * Handles LTI 1.3 login initiation by generating state/nonce and redirecting to platform auth.
77
+ *
78
+ * @param params - Login parameters from the platform
79
+ * @param params.client_id - OAuth2 client identifier for this tool
80
+ * @param params.iss - Platform issuer URL (identifies the LMS)
81
+ * @param params.launchUrl - URL where platform will POST the id_token after auth
82
+ * @param params.login_hint - Platform-specific user identifier hint
83
+ * @param params.target_link_uri - Final destination URL after successful launch
84
+ * @param params.lti_deployment_id - Deployment identifier within the platform
85
+ * @param params.lti_message_hint - Optional platform-specific message context
86
+ * @returns Authorization URL to redirect user to for authentication
87
+ * @throws {Error} When platform configuration is not found
88
+ */
89
+ async handleLogin(params: {
90
+ client_id: string;
91
+ iss: string;
92
+ launchUrl: URL | string;
93
+ login_hint: string;
94
+ target_link_uri: string;
95
+ lti_deployment_id: string;
96
+ lti_message_hint?: string;
97
+ }): Promise<string> {
98
+ const validatedParams = HandleLoginParamsSchema.parse(params);
99
+
100
+ const nonce = crypto.randomUUID();
101
+
102
+ // Store nonce with expiration for replay attack prevention
103
+ const nonceExpirationSeconds = this.config.security?.nonceExpirationSeconds ?? 600;
104
+ const nonceExpiresAt = new Date(Date.now() + nonceExpirationSeconds * 1000);
105
+ await this.config.storage.storeNonce(nonce, nonceExpiresAt);
106
+
107
+ const state = await new SignJWT({
108
+ nonce,
109
+ iss: validatedParams.iss,
110
+ client_id: validatedParams.client_id,
111
+ target_link_uri: validatedParams.target_link_uri,
112
+ exp:
113
+ Math.floor(Date.now() / 1000) +
114
+ (this.config.security?.stateExpirationSeconds ?? 600),
115
+ })
116
+ .setProtectedHeader({ alg: 'HS256' })
117
+ .sign(this.config.stateSecret);
118
+
119
+ const launchConfig = await getValidLaunchConfig(
120
+ this.config.storage,
121
+ validatedParams.iss,
122
+ validatedParams.client_id,
123
+ validatedParams.lti_deployment_id,
124
+ );
125
+
126
+ const authUrl = new URL(launchConfig.authUrl);
127
+ authUrl.searchParams.set('scope', 'openid');
128
+ authUrl.searchParams.set('response_type', 'id_token');
129
+ authUrl.searchParams.set('response_mode', 'form_post');
130
+ authUrl.searchParams.set('prompt', 'none');
131
+ authUrl.searchParams.set('client_id', validatedParams.client_id);
132
+ authUrl.searchParams.set('redirect_uri', validatedParams.launchUrl.toString());
133
+ authUrl.searchParams.set('login_hint', validatedParams.login_hint);
134
+ authUrl.searchParams.set('state', state);
135
+ authUrl.searchParams.set('nonce', nonce);
136
+ authUrl.searchParams.set('lti_deployment_id', validatedParams.lti_deployment_id);
137
+
138
+ if (validatedParams.lti_message_hint) {
139
+ authUrl.searchParams.set('lti_message_hint', validatedParams.lti_message_hint);
140
+ }
141
+
142
+ return authUrl.toString();
143
+ }
144
+
145
+ /**
146
+ * Verifies and validates an LTI 1.3 launch by checking JWT signatures, nonces, and claims.
147
+ *
148
+ * Performs comprehensive security validation including:
149
+ * - JWT signature verification using platform's JWKS
150
+ * - State JWT verification to prevent CSRF
151
+ * - Nonce validation to prevent replay attacks
152
+ * - Client ID and deployment ID verification
153
+ * - LTI 1.3 claim structure validation
154
+ *
155
+ * @param idToken - JWT id_token received from platform after authentication
156
+ * @param state - State JWT that was generated during login initiation
157
+ * @returns Validated and parsed LTI 1.3 JWT payload
158
+ * @throws {Error} When verification fails for security reasons
159
+ */
160
+ async verifyLaunch(idToken: string, state: string): Promise<LTI13JwtPayload> {
161
+ const validatedParams = VerifyLaunchParamsSchema.parse({ idToken, state });
162
+
163
+ // 1. UNVERIFIED - get issuer
164
+ const unverified = decodeJwt(validatedParams.idToken);
165
+ if (!unverified.iss) {
166
+ throw new Error('No issuer in token');
167
+ }
168
+
169
+ // 2. get the launchConfig so we can get the remote JWKS from our data store
170
+ const launchConfig = await getValidLaunchConfig(
171
+ this.config.storage,
172
+ unverified.iss,
173
+ unverified.aud as string,
174
+ unverified['https://purl.imsglobal.org/spec/lti/claim/deployment_id'] as string,
175
+ );
176
+
177
+ // 3. Verify LMS JWT
178
+ let jwks = this.jwksCache.get(launchConfig.jwksUrl);
179
+ if (!jwks) {
180
+ jwks = createRemoteJWKSet(new URL(launchConfig.jwksUrl));
181
+ this.jwksCache.set(launchConfig.jwksUrl, jwks);
182
+ }
183
+ const { payload } = await jwtVerify(validatedParams.idToken, jwks);
184
+
185
+ // 4. Verify our state JWT
186
+ const { payload: stateData } = await jwtVerify(
187
+ validatedParams.state,
188
+ this.config.stateSecret,
189
+ );
190
+
191
+ // 5. Parse and validate LMS JWT
192
+ const validated = LTI13JwtPayloadSchema.parse(payload);
193
+
194
+ // 6. Verify client id matches (audience claim)
195
+ if (validated.aud !== launchConfig.clientId) {
196
+ throw new Error(
197
+ `Invalid client_id: expected ${launchConfig.clientId}, got ${validated.aud}`,
198
+ );
199
+ }
200
+
201
+ // 7. Verify nonce matches
202
+ if (stateData.nonce !== validated.nonce) {
203
+ throw new Error('Nonce mismatch');
204
+ }
205
+
206
+ // 8. Check nonce hasn't been used before (prevent replay attacks)
207
+ const isValidNonce = await this.config.storage.validateNonce(validated.nonce);
208
+ if (!isValidNonce) {
209
+ throw new Error('Nonce has already been used or expired');
210
+ }
211
+
212
+ return validated;
213
+ }
214
+
215
+ /**
216
+ * Generates JSON Web Key Set (JWKS) containing the tool's public key for platform verification.
217
+ *
218
+ * @returns JWKS object with the tool's public key for JWT signature verification
219
+ */
220
+ async getJWKS(): Promise<JWKS> {
221
+ const publicJwk = await exportJWK(this.config.keyPair.publicKey);
222
+ return {
223
+ keys: [
224
+ {
225
+ ...publicJwk,
226
+ use: 'sig',
227
+ alg: 'RS256',
228
+ kid: this.config.security?.keyId ?? 'main',
229
+ },
230
+ ],
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Creates and stores a new LTI session from validated JWT payload.
236
+ *
237
+ * @param lti13JwtPayload - Validated LTI 1.3 JWT payload from successful launch
238
+ * @returns Created session object with user, context, and service information
239
+ */
240
+ async createSession(lti13JwtPayload: LTI13JwtPayload): Promise<LTISession> {
241
+ const session = createSession(lti13JwtPayload);
242
+ await this.config.storage.addSession(session);
243
+ return session;
244
+ }
245
+
246
+ /**
247
+ * Retrieves an existing LTI session by session ID.
248
+ *
249
+ * @param sessionId - Unique session identifier
250
+ * @returns Session object if found, undefined otherwise
251
+ */
252
+ async getSession(sessionId: string): Promise<LTISession | undefined> {
253
+ const validatedSessionId = SessionIdSchema.parse(sessionId);
254
+ return await this.config.storage.getSession(validatedSessionId);
255
+ }
256
+
257
+ /**
258
+ * Submits a grade score to the platform using Assignment and Grade Services (AGS).
259
+ *
260
+ * @param session - Active LTI session containing AGS service endpoints
261
+ * @param score - Score submission data including grade value and user ID
262
+ * @returns Result of the score submission
263
+ * @throws {Error} When AGS is not available or submission fails
264
+ */
265
+ async submitScore(session: LTISession, score: ScoreSubmission): Promise<Response> {
266
+ if (!session) {
267
+ throw new Error('session is required');
268
+ }
269
+ if (!score) {
270
+ throw new Error('score is required');
271
+ }
272
+ return await this.agsService.submitScore(session, score);
273
+ }
274
+
275
+ // Client management
276
+
277
+ /**
278
+ * Retrieves all configured LTI client platforms.
279
+ *
280
+ * @returns Array of client configurations (without deployment details)
281
+ */
282
+ async listClients(): Promise<Omit<LTIClient, 'deployments'>[]> {
283
+ return await this.config.storage.listClients();
284
+ }
285
+
286
+ /**
287
+ * Updates an existing client configuration.
288
+ *
289
+ * @param clientId - Unique client identifier
290
+ * @param client - Partial client object with fields to update
291
+ */
292
+ async updateClient(
293
+ clientId: string,
294
+ client: Partial<Omit<LTIClient, 'id' | 'deployments'>>,
295
+ ): Promise<void> {
296
+ const validated = UpdateClientSchema.parse(client);
297
+ return await this.config.storage.updateClient(clientId, validated);
298
+ }
299
+
300
+ /**
301
+ * Retrieves a specific client configuration by ID.
302
+ *
303
+ * @param clientId - Unique client identifier
304
+ * @returns Client configuration if found, undefined otherwise
305
+ */
306
+ async getClientById(clientId: string): Promise<LTIClient | undefined> {
307
+ return await this.config.storage.getClientById(clientId);
308
+ }
309
+
310
+ /**
311
+ * Adds a new LTI client platform configuration.
312
+ *
313
+ * @param client - Client configuration (ID will be auto-generated)
314
+ * @returns The generated client ID
315
+ */
316
+ async addClient(client: Omit<LTIClient, 'id' | 'deployments'>): Promise<string> {
317
+ const validated = AddClientSchema.parse(client);
318
+ return await this.config.storage.addClient(validated);
319
+ }
320
+
321
+ /**
322
+ * Removes a client configuration and all its deployments.
323
+ *
324
+ * @param clientId - Unique client identifier
325
+ */
326
+ async deleteClient(clientId: string): Promise<void> {
327
+ return await this.config.storage.deleteClient(clientId);
328
+ }
329
+
330
+ // Deployment management
331
+
332
+ /**
333
+ * Lists all deployments for a specific client platform.
334
+ *
335
+ * @param clientId - Client identifier
336
+ * @returns Array of deployment configurations for the client
337
+ */
338
+ async listDeployments(clientId: string): Promise<LTIDeployment[]> {
339
+ return await this.config.storage.listDeployments(clientId);
340
+ }
341
+
342
+ /**
343
+ * Retrieves a specific deployment configuration.
344
+ *
345
+ * @param clientId - Client identifier
346
+ * @param deploymentId - Deployment identifier
347
+ * @returns Deployment configuration if found, undefined otherwise
348
+ */
349
+ async getDeployment(
350
+ clientId: string,
351
+ deploymentId: string,
352
+ ): Promise<LTIDeployment | undefined> {
353
+ return await this.config.storage.getDeployment(clientId, deploymentId);
354
+ }
355
+
356
+ /**
357
+ * Adds a new deployment to an existing client.
358
+ *
359
+ * @param clientId - Client identifier
360
+ * @param deployment - Deployment configuration to add
361
+ * @returns The generated deployment ID
362
+ */
363
+ async addDeployment(
364
+ clientId: string,
365
+ deployment: Omit<LTIDeployment, 'id'>,
366
+ ): Promise<string> {
367
+ return await this.config.storage.addDeployment(clientId, deployment);
368
+ }
369
+
370
+ /**
371
+ * Updates an existing deployment configuration.
372
+ *
373
+ * @param clientId - Client identifier
374
+ * @param deploymentId - Deployment identifier
375
+ * @param deployment - Partial deployment object with fields to update
376
+ */
377
+ async updateDeployment(
378
+ clientId: string,
379
+ deploymentId: string,
380
+ deployment: Partial<LTIDeployment>,
381
+ ): Promise<void> {
382
+ return await this.config.storage.updateDeployment(clientId, deploymentId, deployment);
383
+ }
384
+
385
+ /**
386
+ * Removes a deployment from a client.
387
+ *
388
+ * @param clientId - Client identifier
389
+ * @param deploymentId - Deployment identifier to remove
390
+ */
391
+ async deleteDeployment(clientId: string, deploymentId: string): Promise<void> {
392
+ return await this.config.storage.deleteDeployment(clientId, deploymentId);
393
+ }
394
+ }
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+
3
+ import { DeploymentSchema } from './deployment.schema';
4
+
5
+ export const ClientSchema = z.object({
6
+ id: z.uuid().describe('Internal stable UUID for the client'),
7
+ name: z.string().min(1).describe('human-readable name for the platform'),
8
+ iss: z.url().describe('Platform issuer (unique identifier)'),
9
+ clientId: z.string().min(1).describe("Your app's client ID on this platform"),
10
+ authUrl: z.url().describe("Platform's auth endpoint"),
11
+ tokenUrl: z.url().describe("Platform's token endpoint"),
12
+ jwksUrl: z.url().describe("Platform's JWKS endpoint"),
13
+ deployments: z.array(DeploymentSchema),
14
+ });
15
+
16
+ export const AddClientSchema = ClientSchema.omit({ id: true, deployments: true });
17
+ export const UpdateClientSchema = ClientSchema.omit({ id: true, deployments: true });
@@ -0,0 +1,7 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Common validation schemas used across the LTI tool
5
+ */
6
+
7
+ export const SessionIdSchema = z.string().min(1, 'sessionId is required');
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+
3
+ export const DeploymentSchema = z.object({
4
+ id: z.uuid().describe('Internal stable UUID for this deployment configuration'),
5
+ deploymentId: z.string().min(1).describe('LMS-provided deployment identifier'),
6
+ name: z
7
+ .string()
8
+ .min(1)
9
+ .optional()
10
+ .describe('Optional human-readable name for the deployment'),
11
+ description: z.string().optional().describe('Optional description of the deployment'),
12
+ });
@@ -0,0 +1,10 @@
1
+ export { SessionIdSchema } from './common.schema.js';
2
+ export {
3
+ LTI13JwtPayloadSchema,
4
+ type LTI13JwtPayload,
5
+ } from './lti13/lti13JwtPayload.schema.js';
6
+ export {
7
+ LTI13LaunchSchema,
8
+ VerifyLaunchParamsSchema,
9
+ } from './lti13/lti13Launch.schema.js';
10
+ export { HandleLoginParamsSchema, LTI13LoginSchema } from './lti13/lti13Login.schema.js';
@@ -0,0 +1,54 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Schema for submitting grades via LTI Assignment and Grade Services (AGS).
5
+ * Validates score data according to LTI AGS v2.0 specification.
6
+ *
7
+ * @see https://www.imsglobal.org/spec/lti-ags/v2p0/#score-publish-service
8
+ */
9
+ export const ScoreSubmissionSchema = z.object({
10
+ /** Points awarded to the student (must be non-negative) */
11
+ scoreGiven: z.number().min(0),
12
+
13
+ /** Maximum possible points for this assignment (must be non-negative) */
14
+ scoreMaximum: z.number().min(0),
15
+
16
+ /** Optional feedback comment to display to the student */
17
+ comment: z.string().optional(),
18
+
19
+ /** User ID to submit score for (optional - defaults to session user if not provided) */
20
+ userId: z.string().optional(),
21
+
22
+ /** Timestamp when score was generated (optional - defaults to current time) */
23
+ timestamp: z.iso.datetime().optional(),
24
+
25
+ /**
26
+ * Student's progress on the activity itself.
27
+ * - Initialized: Student has started but not made progress
28
+ * - Started: Student has begun working
29
+ * - InProgress: Student is actively working
30
+ * - Submitted: Student has submitted work for review
31
+ * - Completed: Student has finished the activity
32
+ */
33
+ activityProgress: z
34
+ .enum(['Initialized', 'Started', 'InProgress', 'Submitted', 'Completed'])
35
+ .default('Completed'),
36
+
37
+ /**
38
+ * Instructor's progress on grading the submission.
39
+ * - NotReady: Submission not ready for grading
40
+ * - Failed: Grading failed due to error
41
+ * - Pending: Awaiting automatic grading
42
+ * - PendingManual: Awaiting manual grading
43
+ * - FullyGraded: Grading is complete
44
+ */
45
+ gradingProgress: z
46
+ .enum(['NotReady', 'Failed', 'Pending', 'PendingManual', 'FullyGraded'])
47
+ .default('FullyGraded'),
48
+ });
49
+
50
+ /**
51
+ * Type representing a validated score submission for LTI AGS.
52
+ * Contains grade data and metadata to be sent to the platform.
53
+ */
54
+ export type ScoreSubmission = z.infer<typeof ScoreSubmissionSchema>;
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod';
2
+
3
+ export const BaseJwtClaimsSchema = z.object({
4
+ iss: z.string(),
5
+ sub: z.string(),
6
+ aud: z.union([z.string(), z.array(z.string())]),
7
+ exp: z.number(),
8
+ iat: z.number(),
9
+ nbf: z.number().optional(),
10
+ nonce: z.string(),
11
+ });
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+
3
+ export const ResourceLinkSchema = z
4
+ .object({
5
+ id: z.string(),
6
+ title: z.string().optional(),
7
+ })
8
+ .optional();
9
+
10
+ export const ContextSchema = z
11
+ .object({
12
+ id: z.string(),
13
+ label: z.string().optional(),
14
+ title: z.string().optional(),
15
+ })
16
+ .optional();