coursecode 0.1.53 → 0.1.55

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 (38) hide show
  1. package/README.md +6 -3
  2. package/bin/cli.js +14 -1
  3. package/framework/docs/COURSE_AUTHORING_GUIDE.md +13 -7
  4. package/framework/docs/FRAMEWORK_GUIDE.md +23 -22
  5. package/framework/docs/USER_GUIDE.md +9 -3
  6. package/framework/index.html +1 -1
  7. package/framework/js/core/event-bus.js +21 -6
  8. package/framework/js/drivers/lti-driver.js +50 -94
  9. package/framework/js/drivers/proxy-driver.js +8 -5
  10. package/framework/js/main.js +3 -15
  11. package/framework/js/utilities/access-control.js +11 -45
  12. package/framework/js/utilities/data-reporter.js +16 -3
  13. package/framework/version.json +26 -50
  14. package/lib/build-packaging.d.ts +4 -0
  15. package/lib/build-packaging.js +39 -2
  16. package/lib/cloud.js +1 -1
  17. package/lib/course-writer.js +97 -32
  18. package/lib/create.js +71 -18
  19. package/lib/manifest/cmi5-manifest.js +19 -10
  20. package/lib/manifest/lti-tool-config.js +18 -5
  21. package/lib/manifest/scorm-12-manifest.js +11 -5
  22. package/lib/manifest/scorm-2004-manifest.js +16 -9
  23. package/lib/manifest/xml-utils.js +14 -0
  24. package/lib/preview-routes-api.js +16 -7
  25. package/lib/preview-server.js +49 -28
  26. package/lib/project-utils.js +34 -0
  27. package/lib/proxy-templates/proxy.html +7 -3
  28. package/lib/proxy-templates/scorm-bridge.js +15 -9
  29. package/lib/scaffold.js +33 -4
  30. package/lib/stub-player.js +19 -2
  31. package/lib/token.js +26 -29
  32. package/lib/upgrade.js +76 -2
  33. package/package.json +33 -26
  34. package/template/course/course-config.js +9 -7
  35. package/template/gitattributes +44 -0
  36. package/template/gitignore +38 -0
  37. package/template/package.json +13 -4
  38. package/template/vite.config.js +90 -27
@@ -44,11 +44,15 @@
44
44
  </div>
45
45
 
46
46
  <iframe id="course" style="display:none"></iframe>
47
-
47
+
48
+ <script src="pipwerks.js"></script>
49
+ <script>
50
+ // Injected as a JSON string by the build system before the bridge loads.
51
+ window.COURSECODE_PROXY_CONFIG = { courseUrl: {{EXTERNAL_URL_JSON}} };
52
+ </script>
48
53
  <script src="scorm-bridge.js"></script>
49
54
  <script>
50
- // Course URL is injected at build time
51
- const COURSE_URL = '{{EXTERNAL_URL}}';
55
+ const COURSE_URL = window.COURSECODE_PROXY_CONFIG.courseUrl;
52
56
 
53
57
  const iframe = document.getElementById('course');
54
58
  const loading = document.getElementById('loading');
@@ -22,16 +22,17 @@
22
22
  const iframe = document.getElementById('course');
23
23
  let initialized = false;
24
24
 
25
- // Derive expected course origin from build-time URL for postMessage validation
26
- // COURSE_URL is injected by the build system (e.g., 'https://cdn.example.com/my-course')
27
- let courseOrigin = '*'; // Fallback if URL parsing fails
25
+ // Derive the exact expected course origin from build-time configuration.
26
+ // Fail closed: wildcard origins would allow unrelated frames to operate the LMS API.
27
+ let courseOrigin;
28
28
  try {
29
- if (typeof COURSE_URL === 'string' && COURSE_URL && !COURSE_URL.startsWith('{{')) {
30
- courseOrigin = new URL(COURSE_URL).origin;
31
- console.log('SCORM Bridge: Expected course origin:', courseOrigin);
32
- }
29
+ const courseUrl = window.COURSECODE_PROXY_CONFIG?.courseUrl;
30
+ if (!courseUrl) throw new Error('Missing proxy course URL');
31
+ courseOrigin = new URL(courseUrl).origin;
32
+ console.log('SCORM Bridge: Expected course origin:', courseOrigin);
33
33
  } catch (e) {
34
- console.warn('SCORM Bridge: Could not parse COURSE_URL for origin validation:', e.message);
34
+ console.error('SCORM Bridge: Invalid course URL; bridge disabled:', e.message);
35
+ return;
35
36
  }
36
37
 
37
38
  // Disable pipwerks auto-handling - we manage status ourselves
@@ -49,8 +50,13 @@
49
50
  return;
50
51
  }
51
52
 
53
+ if (!iframe || event.source !== iframe.contentWindow) {
54
+ console.warn('SCORM Bridge: Rejected message not sent by the course iframe');
55
+ return;
56
+ }
57
+
52
58
  // Validate origin when we have a known course origin
53
- if (courseOrigin !== '*' && event.origin !== courseOrigin) {
59
+ if (event.origin !== courseOrigin) {
54
60
  console.warn('SCORM Bridge: Rejected message from unexpected origin:', event.origin);
55
61
  return;
56
62
  }
package/lib/scaffold.js CHANGED
@@ -34,13 +34,24 @@ const MINIMAL_CONFIG = `export const courseConfig = {
34
34
  };
35
35
  `;
36
36
 
37
+ const DEMO_ASSETS = [
38
+ 'docs/example_md_1.md',
39
+ 'docs/example_md_2.md',
40
+ 'docs/example_pdf_1_thumbnail.png',
41
+ 'docs/example_pdf_2.pdf',
42
+ 'images/course-architecture.svg',
43
+ 'images/logo.svg',
44
+ 'widgets/counter-demo.html',
45
+ 'widgets/gravity-painter.html'
46
+ ];
47
+
37
48
  /**
38
49
  * Minimal slide template
39
50
  */
40
51
  function slideTemplate(id) {
41
52
  const title = id.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
42
53
  return `export const slide = {
43
- render(root, context) {
54
+ render(_root, _context) {
44
55
  const container = document.createElement('div');
45
56
  container.innerHTML = \`
46
57
  <h1>${title}</h1>
@@ -83,9 +94,11 @@ const questions = [
83
94
  ];
84
95
 
85
96
  export const slide = {
86
- render(root) {
87
- const assessment = AssessmentManager.createAssessment(config, questions);
88
- return assessment.render(root);
97
+ render(_root, context = {}) {
98
+ const container = document.createElement('div');
99
+ const assessment = AssessmentManager.createAssessment({ ...config, questions });
100
+ assessment.render(container, context);
101
+ return container;
89
102
  }
90
103
  };
91
104
  `;
@@ -128,6 +141,22 @@ export function clean(options = {}) {
128
141
  }
129
142
  }
130
143
 
144
+ // Remove the known demo documents, images, and widgets without touching
145
+ // any author-created assets that may already exist in the project.
146
+ const assetsDir = path.join(coursePath, 'assets');
147
+ for (const relativePath of DEMO_ASSETS) {
148
+ const assetPath = path.join(assetsDir, relativePath);
149
+ if (fs.existsSync(assetPath)) {
150
+ fs.rmSync(assetPath, { force: true });
151
+ removed++;
152
+ }
153
+ }
154
+
155
+ // A freshly blank scaffold must not inherit the example narration cache.
156
+ if (options.blank) {
157
+ fs.rmSync(path.join(basePath, '.narration-cache.json'), { force: true });
158
+ }
159
+
131
160
  // Rewrite course-config.js to minimal starter
132
161
  const configPath = path.join(coursePath, 'course-config.js');
133
162
  if (fs.existsSync(configPath)) {
@@ -58,7 +58,7 @@ export { escapeHtml };
58
58
  * @returns {string} - Complete HTML for the player page
59
59
  */
60
60
  export function generateStubPlayer(config) {
61
- const { title, launchUrl, storageKey, passwordHash, isLive, liveReload, courseContent, startSlide, isDesktop, showHeader = true, skipGating = null, moduleBasePath = '/__stub-player' } = config;
61
+ const { title, launchUrl, storageKey, passwordHash, isLive, liveReload, courseContent, startSlide, isDesktop, showHeader = true, skipGating = null, moduleBasePath = '/__stub-player', previewToken = null } = config;
62
62
  const hasPassword = !!passwordHash;
63
63
  const hasContent = !!courseContent;
64
64
  const headerVisible = showHeader !== false;
@@ -115,8 +115,25 @@ ${stubPlayerStyles}
115
115
  showHeader: ${headerVisible},
116
116
  skipGating: ${typeof skipGating === 'boolean' ? skipGating : 'null'},
117
117
  isDesktop: ${isDesktop || false},
118
- isCI: ${!!process.env.CI}
118
+ isCI: ${!!process.env.CI},
119
+ previewToken: ${previewToken ? JSON.stringify(previewToken) : 'null'}
119
120
  };
121
+
122
+ // Protect source-mutating preview endpoints from cross-site requests. The
123
+ // token is random for each preview process and is never used by exports.
124
+ if (window.STUB_CONFIG.previewToken) {
125
+ const nativeFetch = window.fetch.bind(window);
126
+ window.fetch = function(input, init = {}) {
127
+ const requestUrl = typeof input === 'string' ? input : input?.url;
128
+ const requestMethod = String(init.method || input?.method || 'GET').toUpperCase();
129
+ if (requestUrl?.startsWith('/__') && !['GET', 'HEAD', 'OPTIONS'].includes(requestMethod)) {
130
+ const headers = new Headers(init.headers || input?.headers || {});
131
+ headers.set('X-CourseCode-Preview-Token', window.STUB_CONFIG.previewToken);
132
+ init = { ...init, headers };
133
+ }
134
+ return nativeFetch(input, init);
135
+ };
136
+ }
120
137
  </script>
121
138
  <script type="module" src="${moduleBasePath}/${isLive ? 'app' : 'app-viewer'}.js"></script>
122
139
 
package/lib/token.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Usage:
5
5
  * coursecode token # Generate random token
6
- * coursecode token --add client-name # Add client to course-config
6
+ * coursecode token --add client-name # Add client to gitignored access file
7
7
  */
8
8
 
9
9
  import crypto from 'crypto';
@@ -25,51 +25,48 @@ export function generateToken(length = 24) {
25
25
  export async function token(options = {}) {
26
26
  const newToken = generateToken();
27
27
 
28
- // If --add specified, add to course-config
28
+ // If --add specified, add to the non-published access file. Credentials
29
+ // must never live in course-config.js because that module is bundled into
30
+ // learner-facing JavaScript.
29
31
  if (options.add) {
30
32
  const clientId = options.add;
31
- const courseDir = path.join(process.cwd(), 'course');
32
- const configPath = path.join(courseDir, 'course-config.js');
33
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(clientId)) {
34
+ throw new Error('Client ID must use letters, numbers, dots, underscores, or hyphens');
35
+ }
36
+
37
+ const configPath = path.join(process.cwd(), 'course', 'course-config.js');
33
38
 
34
39
  if (!fs.existsSync(configPath)) {
35
40
  console.error('\n❌ No course-config.js found. Run from a CourseCode project directory.\n');
36
41
  process.exit(1);
37
42
  }
38
43
 
39
- let content = fs.readFileSync(configPath, 'utf-8');
44
+ const accessDir = path.join(process.cwd(), '.coursecode');
45
+ const accessPath = path.join(accessDir, 'access-control.json');
46
+ fs.mkdirSync(accessDir, { recursive: true });
40
47
 
41
- // Check if accessControl already exists
42
- if (content.includes('accessControl:')) {
43
- // Add to existing clients object
44
- const clientsMatch = content.match(/accessControl:\s*\{[\s\S]*?clients:\s*\{/);
45
- if (clientsMatch) {
46
- const insertPos = clientsMatch.index + clientsMatch[0].length;
47
- const newClient = `\n '${clientId}': { token: '${newToken}' },`;
48
- content = content.slice(0, insertPos) + newClient + content.slice(insertPos);
48
+ let accessConfig = { clients: {} };
49
+ if (fs.existsSync(accessPath)) {
50
+ try {
51
+ accessConfig = JSON.parse(fs.readFileSync(accessPath, 'utf-8'));
52
+ } catch (error) {
53
+ throw new Error(`Cannot parse ${accessPath}: ${error.message}`);
49
54
  }
50
- } else {
51
- // Add accessControl section before closing brace
52
- const accessControlBlock = `
53
- accessControl: {
54
- enabled: true,
55
- clients: {
56
- '${clientId}': { token: '${newToken}' }
57
55
  }
58
- },`;
59
- // Find the last closing brace of courseConfig
60
- const lastBrace = content.lastIndexOf('};');
61
- if (lastBrace !== -1) {
62
- content = content.slice(0, lastBrace) + accessControlBlock + '\n' + content.slice(lastBrace);
63
- }
56
+ if (!accessConfig.clients || typeof accessConfig.clients !== 'object') {
57
+ accessConfig.clients = {};
64
58
  }
65
-
66
- fs.writeFileSync(configPath, content, 'utf-8');
59
+ accessConfig.clients[clientId] = { token: newToken };
60
+ fs.writeFileSync(accessPath, JSON.stringify(accessConfig, null, 2) + '\n', { mode: 0o600 });
61
+ try { fs.chmodSync(accessPath, 0o600); } catch { /* Windows/filesystem may not support chmod */ }
67
62
 
68
63
  console.log(`
69
- ✅ Added client '${clientId}' to accessControl
64
+ ✅ Added client '${clientId}' to .coursecode/access-control.json
70
65
 
71
66
  Token: ${newToken}
72
67
 
68
+ Keep accessControl.enforcement = 'server' in course-config.js.
69
+ Your CDN/backend must validate this token before serving course files.
73
70
  Build will generate: ${clientId}_proxy.zip
74
71
  `);
75
72
  } else {
package/lib/upgrade.js CHANGED
@@ -113,6 +113,46 @@ export function addMissingRuntimeDependencies(projectPkg, templatePkg) {
113
113
  return added;
114
114
  }
115
115
 
116
+ const OBSOLETE_MANAGED_DEV_DEPENDENCIES = ['@vitejs/plugin-legacy'];
117
+
118
+ /**
119
+ * Synchronize the build tooling that accompanies a replaced template config.
120
+ * This only runs with `coursecode upgrade --configs`, keeping the default
121
+ * framework-only upgrade from changing a project's custom build stack.
122
+ */
123
+ export function syncManagedBuildTooling(projectPkg, templatePkg) {
124
+ const projectDevDeps = { ...(projectPkg.devDependencies || {}) };
125
+ const templateDevDeps = templatePkg.devDependencies || {};
126
+ const updated = [];
127
+ const removed = [];
128
+
129
+ for (const [name, version] of Object.entries(templateDevDeps)) {
130
+ if (projectDevDeps[name] !== version) {
131
+ projectDevDeps[name] = version;
132
+ updated.push(`${name}@${version}`);
133
+ }
134
+ }
135
+
136
+ for (const name of OBSOLETE_MANAGED_DEV_DEPENDENCIES) {
137
+ if (projectDevDeps[name]) {
138
+ delete projectDevDeps[name];
139
+ removed.push(name);
140
+ }
141
+ }
142
+
143
+ projectPkg.devDependencies = Object.fromEntries(
144
+ Object.entries(projectDevDeps).sort(([a], [b]) => a.localeCompare(b))
145
+ );
146
+
147
+ const nodeEngine = templatePkg.engines?.node;
148
+ const engineUpdated = Boolean(nodeEngine && projectPkg.engines?.node !== nodeEngine);
149
+ if (engineUpdated) {
150
+ projectPkg.engines = { ...(projectPkg.engines || {}), node: nodeEngine };
151
+ }
152
+
153
+ return { updated, removed, engineUpdated };
154
+ }
155
+
116
156
  export async function upgrade(options = {}) {
117
157
  const cwd = process.cwd();
118
158
  const rcPath = path.join(cwd, '.coursecoderc.json');
@@ -175,7 +215,8 @@ export async function upgrade(options = {}) {
175
215
  if (options.configs) {
176
216
  dryRunMsg += `
177
217
  - vite.config.js (replace, backup original)
178
- - eslint.config.js (replace, backup original)`;
218
+ - eslint.config.js (replace, backup original)
219
+ - package.json (sync managed Vite/ESLint tooling and remove plugin-legacy)`;
179
220
  }
180
221
 
181
222
  dryRunMsg += `
@@ -266,10 +307,18 @@ export async function upgrade(options = {}) {
266
307
  // the project's existing version ranges.
267
308
  const pkgPath = path.join(cwd, 'package.json');
268
309
  let addedRuntimeDeps = [];
310
+ let toolingChanges = null;
269
311
  if (fs.existsSync(pkgPath)) {
270
312
  const projectPkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
271
313
  addedRuntimeDeps = addMissingRuntimeDependencies(projectPkg, templatePkg);
272
- if (addedRuntimeDeps.length > 0) {
314
+ if (options.configs) {
315
+ toolingChanges = syncManagedBuildTooling(projectPkg, templatePkg);
316
+ }
317
+ const packageChanged = addedRuntimeDeps.length > 0 ||
318
+ toolingChanges?.updated.length > 0 ||
319
+ toolingChanges?.removed.length > 0 ||
320
+ toolingChanges?.engineUpdated;
321
+ if (packageChanged) {
273
322
  fs.writeFileSync(pkgPath, JSON.stringify(projectPkg, null, 2) + '\n');
274
323
  }
275
324
  }
@@ -290,6 +339,31 @@ export async function upgrade(options = {}) {
290
339
  Run npm install to update node_modules and your lockfile.`;
291
340
  }
292
341
 
342
+ if (toolingChanges && (
343
+ toolingChanges.updated.length > 0 ||
344
+ toolingChanges.removed.length > 0 ||
345
+ toolingChanges.engineUpdated
346
+ )) {
347
+ successMsg += `
348
+
349
+ Managed build tooling synchronized for the new config:`;
350
+ if (toolingChanges.updated.length > 0) {
351
+ successMsg += `
352
+ - Updated: ${toolingChanges.updated.join(', ')}`;
353
+ }
354
+ if (toolingChanges.removed.length > 0) {
355
+ successMsg += `
356
+ - Removed: ${toolingChanges.removed.join(', ')}`;
357
+ }
358
+ if (toolingChanges.engineUpdated) {
359
+ successMsg += `
360
+ - Updated Node engine requirement to ${templatePkg.engines.node}`;
361
+ }
362
+ successMsg += `
363
+
364
+ Run npm install to update node_modules and your lockfile.`;
365
+ }
366
+
293
367
  if (configUpdates.length > 0) {
294
368
  successMsg += `
295
369
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coursecode",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
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": {
@@ -14,7 +14,6 @@
14
14
  "!framework/dist/**",
15
15
  "schemas",
16
16
  "template",
17
- "!template/.gitattributes",
18
17
  "!template/**/.gitkeep",
19
18
  "README.md",
20
19
  "THIRD_PARTY_NOTICES.md"
@@ -51,26 +50,28 @@
51
50
  "lint:responsive:structure": "node scripts/check-responsive-structure-scoping.mjs",
52
51
  "lint:fix": "eslint . --fix",
53
52
  "smoke:responsive": "node scripts/responsive-visual-smoke.mjs",
53
+ "smoke:packed-create": "node scripts/smoke-packed-create.mjs",
54
54
  "build": "npx vite build --config vite.framework-dev.config.js",
55
55
  "build:dev": "npx vite build --config vite.framework-dev.config.js",
56
56
  "build:scorm2004": "LMS_FORMAT=scorm2004 npx vite build --config vite.framework-dev.config.js",
57
57
  "build:scorm12": "LMS_FORMAT=scorm1.2 npx vite build --config vite.framework-dev.config.js",
58
58
  "build:cmi5": "LMS_FORMAT=cmi5 npx vite build --config vite.framework-dev.config.js",
59
59
  "build:lti": "LMS_FORMAT=lti npx vite build --config vite.framework-dev.config.js",
60
- "package": "npm run lint && PACKAGE=true npx vite build --config vite.framework-dev.config.js",
61
- "package:scorm2004": "npm run lint && LMS_FORMAT=scorm2004 PACKAGE=true npx vite build --config vite.framework-dev.config.js",
62
- "package:scorm12": "npm run lint && LMS_FORMAT=scorm1.2 PACKAGE=true npx vite build --config vite.framework-dev.config.js",
63
- "package:cmi5": "npm run lint && LMS_FORMAT=cmi5 PACKAGE=true npx vite build --config vite.framework-dev.config.js",
64
- "package:lti": "npm run lint && LMS_FORMAT=lti PACKAGE=true npx vite build --config vite.framework-dev.config.js",
60
+ "package": "npm run lint && npm run test:coverage && PACKAGE=true npx vite build --config vite.framework-dev.config.js",
61
+ "package:scorm2004": "npm run lint && npm run test:coverage && LMS_FORMAT=scorm2004 PACKAGE=true npx vite build --config vite.framework-dev.config.js",
62
+ "package:scorm12": "npm run lint && npm run test:coverage && LMS_FORMAT=scorm1.2 PACKAGE=true npx vite build --config vite.framework-dev.config.js",
63
+ "package:cmi5": "npm run lint && npm run test:coverage && LMS_FORMAT=cmi5 PACKAGE=true npx vite build --config vite.framework-dev.config.js",
64
+ "package:lti": "npm run lint && npm run test:coverage && LMS_FORMAT=lti PACKAGE=true npx vite build --config vite.framework-dev.config.js",
65
65
  "test": "vitest run --config tests/vitest.config.js",
66
66
  "test:e2e": "vitest run --config tests/vitest.e2e.config.js",
67
- "test:all": "npm test && npm run test:e2e",
67
+ "test:drivers": "vitest run --config tests/vitest.e2e.scorm2004.config.js && vitest run --config tests/vitest.e2e.scorm12.config.js && vitest run --config tests/vitest.e2e.cmi5.config.js && vitest run --config tests/vitest.e2e.lti.config.js",
68
+ "test:all": "npm test && npm run test:e2e && npm run test:drivers",
68
69
  "test:cloud": "vitest run --config tests/vitest.cloud.config.js",
69
70
  "test:watch": "vitest --config tests/vitest.config.js",
70
71
  "test:coverage": "vitest run --config tests/vitest.config.js --coverage",
71
- "prerelease:check": "npm run lint && npm run lint:responsive && npm run lint:responsive:structure && npm run build",
72
- "prerelease:check:full": "npm run prerelease:check && npm run smoke:responsive -- --profile=expanded",
73
- "prepublishOnly": "echo '📦 Preparing coursecode for npm...'"
72
+ "prerelease:check": "npm run lint && npm run lint:responsive && npm run lint:responsive:structure && npm run test:coverage && npm run build && npm run smoke:packed-create",
73
+ "prerelease:check:full": "npm run prerelease:check && npm run test:e2e && npm run test:drivers && npm run smoke:responsive -- --profile=expanded",
74
+ "prepublishOnly": "npm run prerelease:check"
74
75
  },
75
76
  "repository": {
76
77
  "type": "git",
@@ -98,34 +99,40 @@
98
99
  },
99
100
  "homepage": "https://github.com/course-code-framework/coursecode/blob/main/framework/docs/USER_GUIDE.md",
100
101
  "engines": {
101
- "node": ">=18.0.0"
102
+ "node": ">=20.19.0"
102
103
  },
103
104
  "dependencies": {
104
- "@modelcontextprotocol/sdk": "^1.0.0",
105
- "acorn": "^8.15.0",
105
+ "@modelcontextprotocol/sdk": "^1.29.0",
106
+ "acorn": "^8.17.0",
106
107
  "archiver": "^7.0.1",
107
108
  "commander": "^14.0.3",
108
109
  "lz-string": "^1.5.0",
109
- "mammoth": "^1.8.0",
110
+ "mammoth": "^1.12.0",
110
111
  "marked": "^17.0.6",
111
112
  "marked-gfm-heading-id": "^4.1.4",
112
113
  "node-pptx-parser": "^1.0.1",
113
- "pdf2json": "^4.0.2",
114
+ "pdf2json": "^4.0.3",
114
115
  "pdf2md": "^1.0.2",
115
- "puppeteer-core": "^24.42.0",
116
+ "puppeteer-core": "^24.43.1",
116
117
  "win-ca": "^3.5.1",
117
- "ws": "^8.18.0"
118
+ "ws": "^8.21.0"
118
119
  },
119
120
  "devDependencies": {
120
- "@vitejs/plugin-legacy": "^7.2.1",
121
- "@vitest/coverage-v8": "^4.1.5",
121
+ "@vitest/coverage-v8": "^4.1.10",
122
122
  "@xapi/cmi5": "^1.4.0",
123
- "eslint": "^10.2.1",
124
- "globals": "^17.5.0",
125
- "jose": "^6.2.2",
123
+ "eslint": "^10.6.0",
124
+ "globals": "^17.7.0",
126
125
  "pptxgenjs": "^4.0.1",
127
- "vite": "~7.2.2",
128
- "vite-plugin-static-copy": "^3.4.0",
129
- "vitest": "^4.1.5"
126
+ "vite": "~8.1.4",
127
+ "vite-plugin-static-copy": "^4.1.1",
128
+ "vitest": "^4.1.10"
129
+ },
130
+ "overrides": {
131
+ "uuid": "^11.1.1"
132
+ },
133
+ "pnpm": {
134
+ "overrides": {
135
+ "uuid": "^11.1.1"
136
+ }
130
137
  }
131
138
  }
@@ -92,13 +92,15 @@
92
92
  * EXTERNAL HOSTING (CDN deployment with proxy packages):
93
93
  * format: 'scorm1.2-proxy' | 'scorm2004-proxy' | 'cmi5-remote'
94
94
  * externalUrl: 'https://cdn.example.com/courses/my-course' // Where course is hosted
95
- * accessControl: {
96
- * clients: {
97
- * 'acme-corp': { token: 'generated-token-here' },
98
- * 'globex': { token: 'another-token-here' }
99
- * }
100
- * }
101
- * Generate tokens: coursecode token --add <clientId>
95
+ * accessControl: { enforcement: 'server' }
96
+ * Client tokens are stored outside course source in the gitignored file
97
+ * .coursecode/access-control.json (created by coursecode token --add <clientId>).
98
+ * The CDN/backend MUST validate credentials before serving index.html/assets.
99
+ *
100
+ * LMS PACKAGE MASTERY (optional):
101
+ * lms: { masteryScore: 80 } // 0-100; omitted by default
102
+ * Only set this when the whole package has one LMS-level threshold. Individual
103
+ * assessment passing scores remain in their assessment settings.
102
104
  * Build output: One package per client (e.g., acme-corp_proxy.zip)
103
105
  *
104
106
  * CLOUD DEPLOYMENT:
@@ -0,0 +1,44 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
3
+
4
+ # Force LF line endings for text files
5
+ *.js text eol=lf
6
+ *.mjs text eol=lf
7
+ *.cjs text eol=lf
8
+ *.json text eol=lf
9
+ *.css text eol=lf
10
+ *.html text eol=lf
11
+ *.htm text eol=lf
12
+ *.xml text eol=lf
13
+ *.xsd text eol=lf
14
+ *.dtd text eol=lf
15
+ *.md text eol=lf
16
+ *.txt text eol=lf
17
+ *.yml text eol=lf
18
+ *.yaml text eol=lf
19
+ *.env text eol=lf
20
+
21
+ # Binary files
22
+ *.png binary
23
+ *.jpg binary
24
+ *.jpeg binary
25
+ *.gif binary
26
+ *.ico binary
27
+ *.webp binary
28
+ *.svg binary
29
+ *.mp3 binary
30
+ *.mp4 binary
31
+ *.wav binary
32
+ *.ogg binary
33
+ *.webm binary
34
+ *.woff binary
35
+ *.woff2 binary
36
+ *.ttf binary
37
+ *.eot binary
38
+ *.otf binary
39
+ *.pdf binary
40
+ *.zip binary
41
+
42
+ # Linguist overrides for GitHub stats
43
+ *.css linguist-detectable=false
44
+ *.xsd linguist-detectable=false
@@ -0,0 +1,38 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build output
5
+ dist/
6
+ *.zip
7
+
8
+ # Preview export (comment this line to include in repo for GitHub Pages deployment)
9
+ course-preview/
10
+
11
+ # Environment
12
+ .env
13
+ .env.local
14
+
15
+ # Google Cloud service account credentials (NEVER commit these!)
16
+ **/service-account*.json
17
+ **/*-credentials.json
18
+ **/*-keyfile.json
19
+
20
+ # System files
21
+ .DS_Store
22
+ Thumbs.db
23
+
24
+ # IDE
25
+ .idea/
26
+ .vscode/
27
+ *.swp
28
+ *.swo
29
+
30
+ # CourseCode Cloud (project binding)
31
+ .coursecode/
32
+
33
+ # Narration cache
34
+ .narration-cache.json
35
+
36
+ # Logs
37
+ *.log
38
+ npm-debug.log*
@@ -4,6 +4,9 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "CourseCode project",
7
+ "engines": {
8
+ "node": ">=20.19.0"
9
+ },
7
10
  "scripts": {
8
11
  "dev": "vite build --mode development --watch",
9
12
  "build": "npm run lint && vite build",
@@ -13,16 +16,22 @@
13
16
  },
14
17
  "dependencies": {
15
18
  "@xapi/cmi5": "^1.4.0",
16
- "jose": "^6.2.2",
17
19
  "lz-string": "^1.5.0",
18
20
  "marked": "^17.0.6",
19
21
  "marked-gfm-heading-id": "^4.1.4"
20
22
  },
21
23
  "devDependencies": {
22
- "@vitejs/plugin-legacy": "~7.2.1",
23
24
  "archiver": "^7.0.1",
24
25
  "eslint": "^9.39.4",
25
- "vite": "~7.2.2",
26
- "vite-plugin-static-copy": "^3.4.0"
26
+ "vite": "~8.1.4",
27
+ "vite-plugin-static-copy": "^4.1.1"
28
+ },
29
+ "overrides": {
30
+ "uuid": "^11.1.1"
31
+ },
32
+ "pnpm": {
33
+ "overrides": {
34
+ "uuid": "^11.1.1"
35
+ }
27
36
  }
28
37
  }