coursecode 0.1.18 → 0.1.19

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.
@@ -68,7 +68,11 @@ export class Cmi5Driver extends HttpDriverBase {
68
68
 
69
69
  // Check for cmi5 dev API (stub player or standalone preview)
70
70
  // Search current window and parent frame (stub player injects on parent)
71
- const devApi = typeof window !== 'undefined' && (window.cmi5 || (window.parent !== window && window.parent.cmi5));
71
+ // Try/catch guards against DOMException in cross-origin iframes
72
+ let devApi = typeof window !== 'undefined' && window.cmi5;
73
+ if (!devApi && typeof window !== 'undefined' && window.parent !== window) {
74
+ try { devApi = window.parent.cmi5; } catch (_e) { /* cross-origin parent */ }
75
+ }
72
76
  if (devApi) {
73
77
  logger.info('[Cmi5Driver] Using cmi5 development API');
74
78
  this._mock = true;
@@ -82,12 +86,15 @@ export class Cmi5Driver extends HttpDriverBase {
82
86
 
83
87
  // Check for cmi5 launch parameters
84
88
  if (!this._hasLaunchParameters()) {
85
- logger.info('[Cmi5Driver] No cmi5 launch parameters. Using localStorage mock.');
86
- this._mock = true;
87
- this._loadMockState();
88
- this._isConnected = true;
89
- this._logMockStatement('initialized', { verb: 'initialized' });
90
- return true;
89
+ if (import.meta.env.DEV) {
90
+ logger.info('[Cmi5Driver] No cmi5 launch parameters. Using localStorage mock.');
91
+ this._mock = true;
92
+ this._loadMockState();
93
+ this._isConnected = true;
94
+ this._logMockStatement('initialized', { verb: 'initialized' });
95
+ return true;
96
+ }
97
+ throw new Error('[Cmi5Driver] No cmi5 launch parameters detected. Expected fetch, endpoint, actor, registration, and activityId URL parameters.');
91
98
  }
92
99
 
93
100
  // Production mode: dynamically import @xapi/cmi5
@@ -70,7 +70,11 @@ export class LtiDriver extends HttpDriverBase {
70
70
 
71
71
  // Check for LTI dev API (stub player)
72
72
  // Search current window and parent frame (stub player injects on parent)
73
- const devApi = typeof window !== 'undefined' && (window.lti || (window.parent !== window && window.parent.lti));
73
+ // Try/catch guards against DOMException in cross-origin iframes (LMS embeds)
74
+ let devApi = typeof window !== 'undefined' && window.lti;
75
+ if (!devApi && typeof window !== 'undefined' && window.parent !== window) {
76
+ try { devApi = window.parent.lti; } catch (_e) { /* cross-origin parent */ }
77
+ }
74
78
  if (devApi) {
75
79
  logger.info('[LtiDriver] Using LTI development API');
76
80
  this._mock = true;
@@ -84,12 +88,15 @@ export class LtiDriver extends HttpDriverBase {
84
88
 
85
89
  // Check for LTI launch parameters
86
90
  if (!this._hasLaunchParameters()) {
87
- logger.info('[LtiDriver] No LTI launch parameters. Using localStorage mock.');
88
- this._mock = true;
89
- this._loadMockState();
90
- this._isConnected = true;
91
- this._logMockStatement('initialized', { verb: 'initialized' });
92
- return true;
91
+ if (import.meta.env.DEV) {
92
+ logger.info('[LtiDriver] No LTI launch parameters. Using localStorage mock.');
93
+ this._mock = true;
94
+ this._loadMockState();
95
+ this._isConnected = true;
96
+ this._logMockStatement('initialized', { verb: 'initialized' });
97
+ return true;
98
+ }
99
+ throw new Error('[LtiDriver] No LTI launch context detected. Expected <meta name="lms-format" content="lti"> or id_token/state URL parameters.');
93
100
  }
94
101
 
95
102
  // Production mode: validate JWT and extract claims
@@ -248,6 +255,11 @@ export class LtiDriver extends HttpDriverBase {
248
255
  _hasLaunchParameters() {
249
256
  if (typeof window === 'undefined') return false;
250
257
 
258
+ // Cloud-hosted: engine handles OIDC server-side, signals via meta tag
259
+ const formatMeta = document.querySelector('meta[name="lms-format"]');
260
+ if (formatMeta?.content === 'lti') return true;
261
+
262
+ // Self-hosted: JWT params in URL from OIDC redirect
251
263
  const params = new URLSearchParams(window.location.search);
252
264
  return Boolean(
253
265
  params.get('id_token') ||
@@ -257,15 +269,27 @@ export class LtiDriver extends HttpDriverBase {
257
269
  }
258
270
 
259
271
  async _processLaunch() {
260
- const { jwtVerify, createRemoteJWKSet } = await import('jose');
261
-
262
272
  const params = new URLSearchParams(window.location.search);
263
273
  const idToken = params.get('id_token');
264
274
 
275
+ // Cloud-hosted path: OIDC handled server-side, no JWT in URL
265
276
  if (!idToken) {
266
- throw new Error('No id_token found in launch parameters');
277
+ this._claims = this._resolveCloudClaims();
278
+ this._stateEndpoint = this._resolveStateEndpoint();
279
+
280
+ const agsUrl = this._resolveCloudAgsEndpoint();
281
+ if (agsUrl) {
282
+ this._agsLineItemUrl = agsUrl;
283
+ logger.debug('[LtiDriver] Cloud AGS endpoint:', agsUrl);
284
+ }
285
+
286
+ logger.debug('[LtiDriver] Cloud-hosted launch. User:', this._claims?.sub || 'unknown');
287
+ return;
267
288
  }
268
289
 
290
+ // Self-hosted path: validate JWT from URL params
291
+ const { jwtVerify, createRemoteJWKSet } = await import('jose');
292
+
269
293
  const [headerB64] = idToken.split('.');
270
294
  const header = JSON.parse(atob(headerB64));
271
295
 
@@ -332,6 +356,37 @@ export class LtiDriver extends HttpDriverBase {
332
356
  return '/api/lti/state';
333
357
  }
334
358
 
359
+ /**
360
+ * Resolves LTI claims from cloud-injected meta tags or config object.
361
+ * Used when OIDC is handled server-side (no JWT in URL).
362
+ */
363
+ _resolveCloudClaims() {
364
+ const meta = document.querySelector('meta[name="cc-lti-claims"]');
365
+ if (meta?.content) {
366
+ try {
367
+ return JSON.parse(meta.content);
368
+ } catch (e) {
369
+ logger.warn('[LtiDriver] Failed to parse cc-lti-claims meta tag:', e.message);
370
+ }
371
+ }
372
+
373
+ if (window.__LTI_CONFIG__?.claims) return window.__LTI_CONFIG__.claims;
374
+
375
+ throw new Error('[LtiDriver] Cloud LTI launch detected but no claims provided. Expected <meta name="cc-lti-claims"> or window.__LTI_CONFIG__.claims.');
376
+ }
377
+
378
+ /**
379
+ * Resolves AGS lineitem URL from cloud-injected meta tags or config object.
380
+ */
381
+ _resolveCloudAgsEndpoint() {
382
+ const meta = document.querySelector('meta[name="cc-lti-ags"]');
383
+ if (meta?.content) return meta.content;
384
+
385
+ if (window.__LTI_CONFIG__?.agsEndpoint) return window.__LTI_CONFIG__.agsEndpoint;
386
+
387
+ return null;
388
+ }
389
+
335
390
  _getStateKey() {
336
391
  if (this._claims) {
337
392
  const resourceLink = this._claims['https://purl.imsglobal.org/spec/lti/claim/resource_link']?.id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coursecode",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Multi-format course authoring framework with CLI tools (SCORM 2004, SCORM 1.2, cmi5, LTI 1.3)",
5
5
  "type": "module",
6
6
  "bin": {