@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,12 @@
1
+ import { z } from 'zod';
2
+
3
+ export const CoreLtiClaimsSchema = z.object({
4
+ 'https://purl.imsglobal.org/spec/lti/claim/message_type': z.union([
5
+ z.literal('LtiResourceLinkRequest'),
6
+ z.literal('LtiDeepLinkingRequest'),
7
+ ]),
8
+ 'https://purl.imsglobal.org/spec/lti/claim/version': z.literal('1.3.0'),
9
+ 'https://purl.imsglobal.org/spec/lti/claim/deployment_id': z.string(),
10
+ 'https://purl.imsglobal.org/spec/lti/claim/target_link_uri': z.string(),
11
+ 'https://purl.imsglobal.org/spec/lti/claim/roles': z.array(z.string()).optional(),
12
+ });
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+
3
+ export const ToolPlatformSchema = z
4
+ .object({
5
+ guid: z.string(),
6
+ name: z.string().optional(),
7
+ description: z.string().optional(),
8
+ url: z.string().optional(),
9
+ product_family_code: z.string().optional(),
10
+ contact_email: z.string().optional(),
11
+ version: z.string().optional(),
12
+ })
13
+ .optional();
14
+
15
+ export const LaunchPresentationSchema = z
16
+ .object({
17
+ target: z.string().optional(),
18
+ url: z.string().optional(),
19
+ locale: z.string().optional(),
20
+ })
21
+ .optional();
22
+
23
+ export const LisSchema = z
24
+ .object({
25
+ person_sourcedid: z.string().optional(),
26
+ })
27
+ .optional();
@@ -0,0 +1,8 @@
1
+ import { z } from 'zod';
2
+
3
+ export const PrivacyClaimsSchema = z.object({
4
+ given_name: z.string(),
5
+ family_name: z.string(),
6
+ name: z.string(),
7
+ email: z.string(),
8
+ });
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+
3
+ export const AgsEndpointSchema = z
4
+ .object({
5
+ scope: z.array(z.string()),
6
+ lineitem: z.string().optional(),
7
+ lineitems: z.string().optional(),
8
+ })
9
+ .optional();
10
+
11
+ export const NrpsServiceSchema = z
12
+ .object({
13
+ context_memberships_url: z.string(),
14
+ service_versions: z.array(z.string()).optional(),
15
+ })
16
+ .optional();
17
+
18
+ export const DeepLinkingSettingsSchema = z
19
+ .object({
20
+ deep_link_return_url: z.string(),
21
+ accept_types: z.array(z.string()),
22
+ accept_presentation_document_targets: z.array(z.string()),
23
+ accept_media_types: z.string().optional(),
24
+ accept_multiple: z.boolean().optional(),
25
+ auto_create: z.boolean().optional(),
26
+ data: z.string().optional(),
27
+ })
28
+ .optional();
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+
3
+ import { BaseJwtClaimsSchema } from './claims/baseJwtClaims.schema.js';
4
+ import { ContextSchema, ResourceLinkSchema } from './claims/contextClaims.schema.js';
5
+ import { CoreLtiClaimsSchema } from './claims/coreLtiClaims.schema.js';
6
+ import {
7
+ LaunchPresentationSchema,
8
+ LisSchema,
9
+ ToolPlatformSchema,
10
+ } from './claims/platformClaims.schema.js';
11
+ import { PrivacyClaimsSchema } from './claims/privacyClaims.schema.js';
12
+ import {
13
+ AgsEndpointSchema,
14
+ DeepLinkingSettingsSchema,
15
+ NrpsServiceSchema,
16
+ } from './claims/serviceClaims.schema.js';
17
+
18
+ export const LTI13JwtPayloadSchema = BaseJwtClaimsSchema.extend(PrivacyClaimsSchema.shape)
19
+ .extend(CoreLtiClaimsSchema.shape)
20
+ .extend({
21
+ 'https://purl.imsglobal.org/spec/lti/claim/resource_link': ResourceLinkSchema,
22
+ 'https://purl.imsglobal.org/spec/lti/claim/context': ContextSchema,
23
+ 'https://purl.imsglobal.org/spec/lti/claim/tool_platform': ToolPlatformSchema,
24
+ 'https://purl.imsglobal.org/spec/lti/claim/lis': LisSchema,
25
+ 'https://purl.imsglobal.org/spec/lti/claim/launch_presentation':
26
+ LaunchPresentationSchema,
27
+ 'https://purl.imsglobal.org/spec/lti/claim/custom': z
28
+ .record(z.string(), z.string())
29
+ .optional(),
30
+ 'https://purl.imsglobal.org/spec/lti-ags/claim/endpoint': AgsEndpointSchema,
31
+ 'https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice': NrpsServiceSchema,
32
+ 'https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings':
33
+ DeepLinkingSettingsSchema,
34
+ });
35
+
36
+ export type LTI13JwtPayload = z.infer<typeof LTI13JwtPayloadSchema>;
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+
3
+ export const LTI13LaunchSchema = z.object({
4
+ id_token: z.jwt(),
5
+ state: z.string(),
6
+ });
7
+
8
+ /**
9
+ * Schema for verifyLaunch method parameters - uses consistent camelCase naming
10
+ * for method parameters while LTI13LaunchSchema uses spec-compliant snake_case
11
+ */
12
+ export const VerifyLaunchParamsSchema = z.object({
13
+ idToken: z.string().min(1, 'idToken is required'),
14
+ state: z.string().min(1, 'state is required'),
15
+ });
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+
3
+ export const LTI13LoginSchema = z.object({
4
+ iss: z.string().min(1),
5
+ login_hint: z.string().min(1),
6
+ target_link_uri: z.url(),
7
+ client_id: z.string().min(1),
8
+ lti_deployment_id: z.string().min(1),
9
+ lti_message_hint: z.string().optional(),
10
+ });
11
+
12
+ /**
13
+ * Schema for handleLogin method parameters - extends LTI13LoginSchema
14
+ * with the additional launchUrl parameter needed for method calls
15
+ */
16
+ export const HandleLoginParamsSchema = LTI13LoginSchema.extend({
17
+ launchUrl: z.union([z.url(), z.instanceof(URL)]),
18
+ });
@@ -0,0 +1,92 @@
1
+ import type { BaseLogger } from 'pino';
2
+
3
+ import type { LTISession } from '../interfaces/ltiSession.js';
4
+ import type { LTIStorage } from '../interfaces/ltiStorage.js';
5
+ import type { ScoreSubmission } from '../schemas/lti13/ags/scoreSubmission.schema.js';
6
+ import { getValidLaunchConfig } from '../utils/launchConfigValidation.js';
7
+
8
+ import type { TokenService } from './token.service.js';
9
+
10
+ /**
11
+ * Assignment and Grade Services (AGS) implementation for LTI 1.3.
12
+ * Provides methods to submit grades and scores back to the platform.
13
+ *
14
+ * @see https://www.imsglobal.org/spec/lti-ags/v2p0
15
+ */
16
+ export class AGSService {
17
+ constructor(
18
+ private tokenService: TokenService,
19
+ private storage: LTIStorage,
20
+ private logger: BaseLogger,
21
+ ) {}
22
+
23
+ /**
24
+ * Submits a grade score to the platform using LTI Assignment and Grade Services.
25
+ *
26
+ * @param session - Active LTI session containing AGS endpoint configuration
27
+ * @param score - Score submission data including grade value and metadata
28
+ * @returns Promise resolving to the HTTP response from the platform
29
+ * @throws {Error} When AGS is not available for the session or submission fails
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * await agsService.submitScore(session, {
34
+ * scoreGiven: 85,
35
+ * scoreMaximum: 100,
36
+ * comment: 'Great work!',
37
+ * activityProgress: 'Completed',
38
+ * gradingProgress: 'FullyGraded'
39
+ * });
40
+ * ```
41
+ */
42
+ async submitScore(session: LTISession, score: ScoreSubmission): Promise<Response> {
43
+ if (!session.services?.ags?.lineitem) {
44
+ throw new Error('AGS not available for this session');
45
+ }
46
+
47
+ // Get launch config to access token URL
48
+ const launchConfig = await getValidLaunchConfig(
49
+ this.storage,
50
+ session.platform.issuer,
51
+ session.platform.clientId,
52
+ session.platform.deploymentId,
53
+ );
54
+
55
+ const token = await this.tokenService.getBearerToken(
56
+ session.platform.clientId,
57
+ // Need to get token URL from platform storage
58
+ launchConfig.tokenUrl,
59
+ 'https://purl.imsglobal.org/spec/lti-ags/scope/score',
60
+ );
61
+
62
+ const scorePayload = {
63
+ userId: score.userId,
64
+ scoreGiven: score.scoreGiven,
65
+ scoreMaximum: score.scoreMaximum,
66
+ comment: score.comment,
67
+ timestamp: score.timestamp || new Date().toISOString(),
68
+ activityProgress: score.activityProgress,
69
+ gradingProgress: score.gradingProgress,
70
+ };
71
+
72
+ const response = await fetch(`${session.services.ags.lineitem}/scores`, {
73
+ method: 'POST',
74
+ headers: {
75
+ Authorization: `Bearer ${token}`,
76
+ 'Content-Type': 'application/vnd.ims.lis.v1.score+json',
77
+ },
78
+ body: JSON.stringify(scorePayload),
79
+ });
80
+
81
+ if (!response.ok) {
82
+ const error = await response.json();
83
+ this.logger.error(
84
+ { error, status: response.status, statusText: response.statusText },
85
+ 'AGS score submission failed',
86
+ );
87
+ throw new Error(`AGS score submission failed: ${response.statusText}`);
88
+ }
89
+
90
+ return response;
91
+ }
92
+ }
@@ -0,0 +1,115 @@
1
+ import type { LTISession } from '../interfaces/ltiSession.js';
2
+ import type { LTI13JwtPayload } from '../schemas/index.js';
3
+
4
+ /**
5
+ * Creates an LTI session object from a validated LTI 1.3 JWT payload.
6
+ * Extracts user information, context data, and available services into a structured session.
7
+ *
8
+ * @param lti13JwtPayload - Validated LTI 1.3 JWT payload from successful launch
9
+ * @returns Complete LTI session object with user, context, and service information
10
+ */
11
+ // oxlint-disable-next-line max-lines-per-function
12
+ export function createSession(lti13JwtPayload: LTI13JwtPayload): LTISession {
13
+ const roles = lti13JwtPayload['https://purl.imsglobal.org/spec/lti/claim/roles'] || [];
14
+ const context = lti13JwtPayload['https://purl.imsglobal.org/spec/lti/claim/context'];
15
+ const platform =
16
+ lti13JwtPayload['https://purl.imsglobal.org/spec/lti/claim/tool_platform'];
17
+ const resourceLink =
18
+ lti13JwtPayload['https://purl.imsglobal.org/spec/lti/claim/resource_link'];
19
+ const customClaims =
20
+ lti13JwtPayload['https://purl.imsglobal.org/spec/lti/claim/custom'] || {};
21
+ const agsEndpoint =
22
+ lti13JwtPayload['https://purl.imsglobal.org/spec/lti-ags/claim/endpoint'];
23
+ const nrpsService =
24
+ lti13JwtPayload['https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice'];
25
+ const deepLinkingSettings =
26
+ lti13JwtPayload['https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings'];
27
+
28
+ const isInstructor = roles.some((role) => role.includes('Instructor'));
29
+ const isStudent = roles.some((role) => role.includes('Learner'));
30
+ const isAdmin = roles.some((role) => role.includes('Administrator'));
31
+
32
+ const services: Record<string, unknown> = {};
33
+ if (agsEndpoint) {
34
+ services.ags = {
35
+ lineitem: agsEndpoint.lineitem,
36
+ lineitems: agsEndpoint.lineitems,
37
+ scopes: agsEndpoint.scope || [],
38
+ };
39
+ }
40
+ if (nrpsService) {
41
+ services.nrps = {
42
+ membershipUrl: nrpsService.context_memberships_url,
43
+ versions: nrpsService.service_versions || [],
44
+ };
45
+ }
46
+ if (deepLinkingSettings) {
47
+ services.deepLinking = {
48
+ returnUrl: deepLinkingSettings.deep_link_return_url,
49
+ acceptTypes: deepLinkingSettings.accept_types || [],
50
+ acceptPresentationDocumentTargets:
51
+ deepLinkingSettings.accept_presentation_document_targets || [],
52
+ acceptMediaTypes: deepLinkingSettings.accept_media_types,
53
+ autoCreate: deepLinkingSettings.auto_create,
54
+ data: deepLinkingSettings.data,
55
+ };
56
+ }
57
+
58
+ // Extract simplified roles
59
+ const simplifiedRoles: string[] = [];
60
+ for (const role of roles) {
61
+ if (role.includes('Instructor')) simplifiedRoles.push('instructor');
62
+ if (role.includes('Learner')) simplifiedRoles.push('student');
63
+ if (role.includes('Administrator')) simplifiedRoles.push('admin');
64
+ if (role.includes('ContentDeveloper')) simplifiedRoles.push('content-developer');
65
+ if (role.includes('Member')) simplifiedRoles.push('member');
66
+ }
67
+
68
+ // Remove duplicates
69
+ const uniqueRoles = [...new Set(simplifiedRoles)];
70
+
71
+ return {
72
+ jwtPayload: lti13JwtPayload,
73
+ id: crypto.randomUUID(),
74
+ user: {
75
+ id: lti13JwtPayload.sub,
76
+ name: lti13JwtPayload.name,
77
+ email: lti13JwtPayload.email,
78
+ familyName: lti13JwtPayload.family_name,
79
+ givenName: lti13JwtPayload.given_name,
80
+ roles: uniqueRoles,
81
+ },
82
+ context: {
83
+ id: context?.id || '',
84
+ label: context?.label || context?.id || '',
85
+ title: context?.title || '',
86
+ },
87
+ platform: {
88
+ issuer: lti13JwtPayload.iss,
89
+ clientId: Array.isArray(lti13JwtPayload.aud)
90
+ ? lti13JwtPayload.aud[0]
91
+ : lti13JwtPayload.aud,
92
+ deploymentId:
93
+ lti13JwtPayload['https://purl.imsglobal.org/spec/lti/claim/deployment_id'],
94
+ name: platform?.name || lti13JwtPayload.iss,
95
+ },
96
+ launch: {
97
+ target:
98
+ lti13JwtPayload['https://purl.imsglobal.org/spec/lti/claim/target_link_uri'],
99
+ },
100
+ resourceLink: resourceLink
101
+ ? {
102
+ id: resourceLink.id,
103
+ title: resourceLink.title,
104
+ }
105
+ : undefined,
106
+ customParameters: customClaims,
107
+ services: Object.keys(services).length > 0 ? services : undefined,
108
+ isAdmin,
109
+ isInstructor,
110
+ isStudent,
111
+ isAssignmentAndGradesAvailable: !!agsEndpoint,
112
+ isDeepLinkingAvailable: !!deepLinkingSettings,
113
+ isNameAndRolesAvailable: !!nrpsService,
114
+ };
115
+ }
@@ -0,0 +1,84 @@
1
+ import { SignJWT } from 'jose';
2
+
3
+ /**
4
+ * Service for handling OAuth2 client credentials flow and JWT client assertions.
5
+ * Used for obtaining bearer tokens to access LTI Advantage services (AGS, NRPS, etc.).
6
+ *
7
+ * Implements RFC 7523 (JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants)
8
+ * as required by LTI 1.3 security framework.
9
+ */
10
+ export class TokenService {
11
+ /**
12
+ * Creates a new TokenService instance.
13
+ *
14
+ * @param keyPair - RSA key pair for signing client assertion JWTs (must be RS256 compatible)
15
+ * @param keyId - Key identifier for JWT header, should match JWKS key ID (defaults to 'main')
16
+ */
17
+ constructor(
18
+ private keyPair: CryptoKeyPair,
19
+ private keyId = 'main',
20
+ ) {}
21
+
22
+ /**
23
+ * Creates a JWT client assertion for OAuth2 client credentials flow.
24
+ *
25
+ * @param clientId - OAuth2 client identifier
26
+ * @param tokenUrl - Platform's token endpoint URL
27
+ * @returns Signed JWT client assertion
28
+ */
29
+ async createClientAssertion(clientId: string, tokenUrl: string): Promise<string> {
30
+ return await new SignJWT({
31
+ iss: clientId,
32
+ sub: clientId,
33
+ aud: tokenUrl,
34
+ iat: Math.floor(Date.now() / 1000),
35
+ exp: Math.floor(Date.now() / 1000) + 300,
36
+ jti: crypto.randomUUID(),
37
+ })
38
+ .setProtectedHeader({
39
+ alg: 'RS256',
40
+ kid: this.keyId,
41
+ typ: 'JWT',
42
+ })
43
+ .sign(this.keyPair.privateKey);
44
+ }
45
+
46
+ /**
47
+ * Obtains an OAuth2 bearer token using client credentials flow with JWT assertion.
48
+ *
49
+ * @param clientId - OAuth2 client identifier
50
+ * @param tokenUrl - Platform's token endpoint URL
51
+ * @param scope - Requested OAuth2 scope (e.g., AGS score scope)
52
+ * @returns Bearer access token for API calls
53
+ */
54
+ async getBearerToken(
55
+ clientId: string,
56
+ tokenUrl: string,
57
+ scope: string,
58
+ ): Promise<string> {
59
+ const assertion = await this.createClientAssertion(clientId, tokenUrl);
60
+
61
+ const response = await fetch(tokenUrl, {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
64
+ body: new URLSearchParams({
65
+ grant_type: 'client_credentials',
66
+ client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
67
+ client_assertion: assertion,
68
+ scope,
69
+ }),
70
+ });
71
+
72
+ if (!response.ok) {
73
+ throw new Error(`Token request failed: ${response.status} ${response.statusText}`);
74
+ }
75
+
76
+ const tokenData = await response.json();
77
+
78
+ if (!tokenData.access_token) {
79
+ throw new Error('Token response missing access_token');
80
+ }
81
+
82
+ return tokenData.access_token;
83
+ }
84
+ }
@@ -0,0 +1,16 @@
1
+ import type { LTILaunchConfig, LTIStorage } from '../interfaces/index.js';
2
+
3
+ export async function getValidLaunchConfig(
4
+ storage: LTIStorage,
5
+ iss: string,
6
+ clientId: string,
7
+ deploymentId: string,
8
+ ): Promise<LTILaunchConfig> {
9
+ const launchConfig = await storage.getLaunchConfig(iss, clientId, deploymentId);
10
+
11
+ if (!launchConfig) {
12
+ throw new Error('No valid launch config found');
13
+ }
14
+
15
+ return launchConfig;
16
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }