@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.
- package/CHANGELOG.md +15 -0
- package/README.md +60 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/interfaces/index.d.ts +7 -0
- package/dist/interfaces/index.d.ts.map +1 -0
- package/dist/interfaces/index.js +1 -0
- package/dist/interfaces/jwks.d.ts +18 -0
- package/dist/interfaces/jwks.d.ts.map +1 -0
- package/dist/interfaces/jwks.js +1 -0
- package/dist/interfaces/ltiClient.d.ts +24 -0
- package/dist/interfaces/ltiClient.d.ts.map +1 -0
- package/dist/interfaces/ltiClient.js +1 -0
- package/dist/interfaces/ltiConfig.d.ts +26 -0
- package/dist/interfaces/ltiConfig.d.ts.map +1 -0
- package/dist/interfaces/ltiConfig.js +1 -0
- package/dist/interfaces/ltiDeployment.d.ts +15 -0
- package/dist/interfaces/ltiDeployment.d.ts.map +1 -0
- package/dist/interfaces/ltiDeployment.js +1 -0
- package/dist/interfaces/ltiLaunchConfig.d.ts +19 -0
- package/dist/interfaces/ltiLaunchConfig.d.ts.map +1 -0
- package/dist/interfaces/ltiLaunchConfig.js +1 -0
- package/dist/interfaces/ltiSession.d.ts +110 -0
- package/dist/interfaces/ltiSession.d.ts.map +1 -0
- package/dist/interfaces/ltiSession.js +1 -0
- package/dist/interfaces/ltiStorage.d.ts +122 -0
- package/dist/interfaces/ltiStorage.d.ts.map +1 -0
- package/dist/interfaces/ltiStorage.js +1 -0
- package/dist/ltiTool.d.ts +184 -0
- package/dist/ltiTool.d.ts.map +1 -0
- package/dist/ltiTool.js +305 -0
- package/dist/schemas/client.schema.d.ts +33 -0
- package/dist/schemas/client.schema.d.ts.map +1 -0
- package/dist/schemas/client.schema.js +14 -0
- package/dist/schemas/common.schema.d.ts +6 -0
- package/dist/schemas/common.schema.d.ts.map +1 -0
- package/dist/schemas/common.schema.js +5 -0
- package/dist/schemas/deployment.schema.d.ts +8 -0
- package/dist/schemas/deployment.schema.d.ts.map +1 -0
- package/dist/schemas/deployment.schema.js +11 -0
- package/dist/schemas/index.d.ts +5 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +4 -0
- package/dist/schemas/lti13/ags/scoreSubmission.schema.d.ts +34 -0
- package/dist/schemas/lti13/ags/scoreSubmission.schema.d.ts.map +1 -0
- package/dist/schemas/lti13/ags/scoreSubmission.schema.js +41 -0
- package/dist/schemas/lti13/claims/baseJwtClaims.schema.d.ts +11 -0
- package/dist/schemas/lti13/claims/baseJwtClaims.schema.d.ts.map +1 -0
- package/dist/schemas/lti13/claims/baseJwtClaims.schema.js +10 -0
- package/dist/schemas/lti13/claims/contextClaims.schema.d.ts +11 -0
- package/dist/schemas/lti13/claims/contextClaims.schema.d.ts.map +1 -0
- package/dist/schemas/lti13/claims/contextClaims.schema.js +14 -0
- package/dist/schemas/lti13/claims/coreLtiClaims.schema.d.ts +9 -0
- package/dist/schemas/lti13/claims/coreLtiClaims.schema.d.ts.map +1 -0
- package/dist/schemas/lti13/claims/coreLtiClaims.schema.js +11 -0
- package/dist/schemas/lti13/claims/platformClaims.schema.d.ts +19 -0
- package/dist/schemas/lti13/claims/platformClaims.schema.d.ts.map +1 -0
- package/dist/schemas/lti13/claims/platformClaims.schema.js +24 -0
- package/dist/schemas/lti13/claims/privacyClaims.schema.d.ts +8 -0
- package/dist/schemas/lti13/claims/privacyClaims.schema.d.ts.map +1 -0
- package/dist/schemas/lti13/claims/privacyClaims.schema.js +7 -0
- package/dist/schemas/lti13/claims/serviceClaims.schema.d.ts +20 -0
- package/dist/schemas/lti13/claims/serviceClaims.schema.d.ts.map +1 -0
- package/dist/schemas/lti13/claims/serviceClaims.schema.js +25 -0
- package/dist/schemas/lti13/lti13JwtPayload.schema.d.ts +66 -0
- package/dist/schemas/lti13/lti13JwtPayload.schema.d.ts.map +1 -0
- package/dist/schemas/lti13/lti13JwtPayload.schema.js +22 -0
- package/dist/schemas/lti13/lti13Launch.schema.d.ts +14 -0
- package/dist/schemas/lti13/lti13Launch.schema.d.ts.map +1 -0
- package/dist/schemas/lti13/lti13Launch.schema.js +13 -0
- package/dist/schemas/lti13/lti13Login.schema.d.ts +23 -0
- package/dist/schemas/lti13/lti13Login.schema.d.ts.map +1 -0
- package/dist/schemas/lti13/lti13Login.schema.js +16 -0
- package/dist/services/ags.service.d.ts +38 -0
- package/dist/services/ags.service.d.ts.map +1 -0
- package/dist/services/ags.service.js +69 -0
- package/dist/services/session.service.d.ts +11 -0
- package/dist/services/session.service.d.ts.map +1 -0
- package/dist/services/session.service.js +103 -0
- package/dist/services/token.service.d.ts +36 -0
- package/dist/services/token.service.d.ts.map +1 -0
- package/dist/services/token.service.js +74 -0
- package/dist/utils/launchConfigValidation.d.ts +3 -0
- package/dist/utils/launchConfigValidation.d.ts.map +1 -0
- package/dist/utils/launchConfigValidation.js +7 -0
- package/package.json +53 -0
- package/src/index.ts +3 -0
- package/src/interfaces/index.ts +6 -0
- package/src/interfaces/jwks.ts +20 -0
- package/src/interfaces/ltiClient.ts +24 -0
- package/src/interfaces/ltiConfig.ts +31 -0
- package/src/interfaces/ltiDeployment.ts +17 -0
- package/src/interfaces/ltiLaunchConfig.ts +23 -0
- package/src/interfaces/ltiSession.ts +119 -0
- package/src/interfaces/ltiStorage.ts +161 -0
- package/src/ltiTool.ts +394 -0
- package/src/schemas/client.schema.ts +17 -0
- package/src/schemas/common.schema.ts +7 -0
- package/src/schemas/deployment.schema.ts +12 -0
- package/src/schemas/index.ts +10 -0
- package/src/schemas/lti13/ags/scoreSubmission.schema.ts +54 -0
- package/src/schemas/lti13/claims/baseJwtClaims.schema.ts +11 -0
- package/src/schemas/lti13/claims/contextClaims.schema.ts +16 -0
- package/src/schemas/lti13/claims/coreLtiClaims.schema.ts +12 -0
- package/src/schemas/lti13/claims/platformClaims.schema.ts +27 -0
- package/src/schemas/lti13/claims/privacyClaims.schema.ts +8 -0
- package/src/schemas/lti13/claims/serviceClaims.schema.ts +28 -0
- package/src/schemas/lti13/lti13JwtPayload.schema.ts +36 -0
- package/src/schemas/lti13/lti13Launch.schema.ts +15 -0
- package/src/schemas/lti13/lti13Login.schema.ts +18 -0
- package/src/services/ags.service.ts +92 -0
- package/src/services/session.service.ts +115 -0
- package/src/services/token.service.ts +84 -0
- package/src/utils/launchConfigValidation.ts +16 -0
- 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,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
|
+
}
|