@lessonkit/lxpack 1.0.0 → 1.0.2

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/dist/bridge.cjs CHANGED
@@ -49,6 +49,15 @@ var import_tracking_schema3 = require("@lxpack/tracking-schema");
49
49
  // src/telemetry.ts
50
50
  var import_tracking_schema = require("@lxpack/tracking-schema");
51
51
  var SUPPORTED = new Set(import_tracking_schema.LESSONKIT_TELEMETRY_EVENTS);
52
+ function isQuizAnsweredData(data) {
53
+ return typeof data === "object" && data !== null && typeof data.checkId === "string";
54
+ }
55
+ function isQuizCompletedData(data) {
56
+ return typeof data === "object" && data !== null && typeof data.checkId === "string";
57
+ }
58
+ function isInteractionData(data) {
59
+ return typeof data === "object" && data !== null;
60
+ }
52
61
  function telemetryEventToLessonkit(event) {
53
62
  if (!SUPPORTED.has(event.name)) {
54
63
  return null;
@@ -60,16 +69,16 @@ function telemetryEventToLessonkit(event) {
60
69
  };
61
70
  if (name === "quiz_completed" || name === "quiz_answered") {
62
71
  const data = event.data;
63
- mapped.assessmentId = data?.checkId;
64
- if (data && "score" in data) {
65
- mapped.score = data.score;
66
- mapped.maxScore = data.maxScore;
67
- mapped.passingScore = data.passingScore;
68
- }
69
- if (data) {
72
+ if (isQuizAnsweredData(data) || isQuizCompletedData(data)) {
73
+ mapped.assessmentId = data.checkId;
74
+ if ("score" in data) {
75
+ mapped.score = data.score;
76
+ mapped.maxScore = data.maxScore;
77
+ mapped.passingScore = data.passingScore;
78
+ }
70
79
  mapped.data = data;
71
80
  }
72
- } else if (name === "interaction" && event.data) {
81
+ } else if (name === "interaction" && event.data && isInteractionData(event.data)) {
73
82
  mapped.data = event.data;
74
83
  }
75
84
  return mapped;
@@ -94,7 +103,7 @@ function getBridge(parentWindow) {
94
103
  if (typeof window === "undefined") return null;
95
104
  const parent = parentWindow ?? window.parent;
96
105
  if (!parent || parent === window) return null;
97
- return parent.lxpack ?? null;
106
+ return parent.lxpackBridge?.v1 ?? parent.lxpack ?? null;
98
107
  }
99
108
  function isDevEnvironment() {
100
109
  const g = globalThis;
package/dist/bridge.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  telemetryEventToLessonkit
3
- } from "./chunk-PSUSESH3.js";
3
+ } from "./chunk-DYQI222N.js";
4
4
 
5
5
  // src/bridge.ts
6
6
  import {
@@ -41,7 +41,7 @@ function getBridge(parentWindow) {
41
41
  if (typeof window === "undefined") return null;
42
42
  const parent = parentWindow ?? window.parent;
43
43
  if (!parent || parent === window) return null;
44
- return parent.lxpack ?? null;
44
+ return parent.lxpackBridge?.v1 ?? parent.lxpack ?? null;
45
45
  }
46
46
  function isDevEnvironment() {
47
47
  const g = globalThis;
@@ -0,0 +1,41 @@
1
+ // src/telemetry.ts
2
+ import { LESSONKIT_TELEMETRY_EVENTS } from "@lxpack/tracking-schema";
3
+ var SUPPORTED = new Set(LESSONKIT_TELEMETRY_EVENTS);
4
+ function isQuizAnsweredData(data) {
5
+ return typeof data === "object" && data !== null && typeof data.checkId === "string";
6
+ }
7
+ function isQuizCompletedData(data) {
8
+ return typeof data === "object" && data !== null && typeof data.checkId === "string";
9
+ }
10
+ function isInteractionData(data) {
11
+ return typeof data === "object" && data !== null;
12
+ }
13
+ function telemetryEventToLessonkit(event) {
14
+ if (!SUPPORTED.has(event.name)) {
15
+ return null;
16
+ }
17
+ const name = event.name;
18
+ const mapped = {
19
+ name,
20
+ lessonId: event.lessonId
21
+ };
22
+ if (name === "quiz_completed" || name === "quiz_answered") {
23
+ const data = event.data;
24
+ if (isQuizAnsweredData(data) || isQuizCompletedData(data)) {
25
+ mapped.assessmentId = data.checkId;
26
+ if ("score" in data) {
27
+ mapped.score = data.score;
28
+ mapped.maxScore = data.maxScore;
29
+ mapped.passingScore = data.passingScore;
30
+ }
31
+ mapped.data = data;
32
+ }
33
+ } else if (name === "interaction" && event.data && isInteractionData(event.data)) {
34
+ mapped.data = event.data;
35
+ }
36
+ return mapped;
37
+ }
38
+
39
+ export {
40
+ telemetryEventToLessonkit
41
+ };
package/dist/index.cjs CHANGED
@@ -53,6 +53,7 @@ __export(index_exports, {
53
53
  telemetryEventToLessonkit: () => telemetryEventToLessonkit,
54
54
  themeToLxpackRuntime: () => themeToLxpackRuntime,
55
55
  validateDescriptor: () => validateDescriptor,
56
+ validateDescriptorForTarget: () => validateDescriptorForTarget,
56
57
  validateLessonkitProject: () => validateLessonkitProject,
57
58
  validatePackageInputs: () => validatePackageInputs,
58
59
  validateProjectPaths: () => validateProjectPaths,
@@ -75,10 +76,11 @@ function resolveComparablePath(p) {
75
76
  function isSafeRelativeSpaPath(spaPath) {
76
77
  if (!spaPath.length || spaPath.includes("\0")) return false;
77
78
  if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
78
- if (/^[a-zA-Z]:[/\\]/.test(spaPath)) return false;
79
- const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0);
79
+ if (/^[a-zA-Z]:/.test(spaPath)) return false;
80
+ if (spaPath === "." || spaPath === "./") return false;
81
+ const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0 && s !== ".");
80
82
  if (segments.some((s) => s === "..")) return false;
81
- return true;
83
+ return segments.length > 0;
82
84
  }
83
85
  function assertResolvedPathUnderRoot(root, target) {
84
86
  const rootResolved = resolveComparablePath(root);
@@ -110,13 +112,25 @@ function assertRealPathUnderRoot(root, target) {
110
112
  }
111
113
  assertResolvedPathUnderRoot(rootReal, targetCheck);
112
114
  }
115
+ function normalizePathForComparison(p) {
116
+ const resolved = resolveComparablePath(p);
117
+ return /^[a-zA-Z]:[/\\]/.test(resolved) ? resolved.toLowerCase() : resolved;
118
+ }
119
+ function relativePathUnderRoot(root, target) {
120
+ const rootResolved = normalizePathForComparison(root);
121
+ const targetResolved = normalizePathForComparison(target);
122
+ if (/^[a-zA-Z]:[/\\]/.test(rootResolved)) {
123
+ return import_node_path.win32.relative(rootResolved, targetResolved);
124
+ }
125
+ return (0, import_node_path.relative)(rootResolved, targetResolved);
126
+ }
113
127
  function isResolvedPathUnderRoot(root, target) {
114
- const rootResolved = resolveComparablePath(root);
115
- const targetResolved = resolveComparablePath(target);
128
+ const rootResolved = normalizePathForComparison(root);
129
+ const targetResolved = normalizePathForComparison(target);
116
130
  if (targetResolved === rootResolved) return true;
117
- const prefix = rootResolved.endsWith(import_node_path.sep) ? rootResolved : rootResolved + import_node_path.sep;
118
- const win32Prefix = rootResolved.endsWith(import_node_path.win32.sep) ? rootResolved : rootResolved + import_node_path.win32.sep;
119
- return targetResolved.startsWith(prefix) || targetResolved.startsWith(win32Prefix);
131
+ const rel = relativePathUnderRoot(root, target);
132
+ if (!rel) return true;
133
+ return !rel.startsWith("..") && !(0, import_node_path.isAbsolute)(rel);
120
134
  }
121
135
 
122
136
  // src/theme.ts
@@ -137,6 +151,70 @@ function themeToLxpackRuntime(input) {
137
151
  // src/validateDescriptor.ts
138
152
  var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
139
153
  var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
154
+ function isRecord(value) {
155
+ return typeof value === "object" && value !== null && !Array.isArray(value);
156
+ }
157
+ function parseLessonDescriptor(raw) {
158
+ if (!isRecord(raw)) {
159
+ return { id: "", title: "" };
160
+ }
161
+ return {
162
+ id: typeof raw.id === "string" ? raw.id : "",
163
+ title: typeof raw.title === "string" ? raw.title : "",
164
+ spaPath: typeof raw.spaPath === "string" ? raw.spaPath : void 0
165
+ };
166
+ }
167
+ function parseAssessmentDescriptor(raw) {
168
+ if (!isRecord(raw)) {
169
+ return { checkId: "", question: "", choices: [], answer: "" };
170
+ }
171
+ return {
172
+ checkId: typeof raw.checkId === "string" ? raw.checkId : "",
173
+ question: typeof raw.question === "string" ? raw.question : "",
174
+ choices: Array.isArray(raw.choices) ? raw.choices.filter((c) => typeof c === "string") : [],
175
+ answer: typeof raw.answer === "string" ? raw.answer : "",
176
+ passingScore: typeof raw.passingScore === "number" ? raw.passingScore : void 0
177
+ };
178
+ }
179
+ function parseCourseDescriptorInput(input) {
180
+ if (!isRecord(input)) return null;
181
+ const trackingRaw = input.tracking;
182
+ let tracking;
183
+ if (isRecord(trackingRaw)) {
184
+ const completionRaw = trackingRaw.completion;
185
+ const xapiRaw = trackingRaw.xapi;
186
+ tracking = {
187
+ completion: isRecord(completionRaw) ? {
188
+ threshold: typeof completionRaw.threshold === "number" ? completionRaw.threshold : void 0
189
+ } : void 0,
190
+ xapi: isRecord(xapiRaw) ? {
191
+ activityIri: typeof xapiRaw.activityIri === "string" ? xapiRaw.activityIri : void 0
192
+ } : void 0
193
+ };
194
+ }
195
+ const themeRaw = input.theme;
196
+ let theme;
197
+ if (isRecord(themeRaw)) {
198
+ theme = {
199
+ preset: typeof themeRaw.preset === "string" ? themeRaw.preset : void 0
200
+ };
201
+ if (isRecord(themeRaw.theme)) {
202
+ theme.theme = themeRaw.theme;
203
+ }
204
+ }
205
+ return {
206
+ courseId: typeof input.courseId === "string" ? input.courseId : "",
207
+ title: typeof input.title === "string" ? input.title : "",
208
+ version: typeof input.version === "string" ? input.version : void 0,
209
+ layout: typeof input.layout === "string" ? input.layout : void 0,
210
+ lessons: Array.isArray(input.lessons) ? input.lessons.map(parseLessonDescriptor) : [],
211
+ assessments: Array.isArray(input.assessments) ? input.assessments.map(parseAssessmentDescriptor) : void 0,
212
+ theme,
213
+ tracking,
214
+ spaDistDir: typeof input.spaDistDir === "string" ? input.spaDistDir : void 0,
215
+ spaLessonId: typeof input.spaLessonId === "string" ? input.spaLessonId : void 0
216
+ };
217
+ }
140
218
  function normalizeDescriptor(input) {
141
219
  const course = (0, import_core.validateId)(input.courseId, "courseId");
142
220
  if (!course.ok) throw new Error("normalizeDescriptor called with invalid courseId");
@@ -170,6 +248,31 @@ function normalizeDescriptor(input) {
170
248
  };
171
249
  }
172
250
  function validateDescriptor(input) {
251
+ const parsed = parseCourseDescriptorInput(input);
252
+ if (parsed === null) {
253
+ return { ok: false, issues: [{ path: "course", message: "must be an object" }] };
254
+ }
255
+ return validateDescriptorParsed(parsed);
256
+ }
257
+ function validateDescriptorForTarget(input, target) {
258
+ const result = validateDescriptor(input);
259
+ if (!result.ok || !target) return result;
260
+ if (target !== "xapi" && target !== "cmi5") return result;
261
+ const activityIri = result.descriptor.tracking?.xapi?.activityIri?.trim();
262
+ if (!activityIri) {
263
+ return {
264
+ ok: false,
265
+ issues: [
266
+ {
267
+ path: "course.tracking.xapi.activityIri",
268
+ message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
269
+ }
270
+ ]
271
+ };
272
+ }
273
+ return result;
274
+ }
275
+ function validateDescriptorParsed(input) {
173
276
  const issues = [];
174
277
  const course = (0, import_core.validateId)(input.courseId, "courseId");
175
278
  if (!course.ok) issues.push(...course.issues.map((i) => ({ path: i.path, message: i.message })));
@@ -315,7 +418,7 @@ function validatePathField(value, fieldPath, projectRoot, issues) {
315
418
  return;
316
419
  }
317
420
  try {
318
- assertResolvedPathUnderRoot(projectRoot, (0, import_node_path2.resolve)(projectRoot, value));
421
+ assertRealPathUnderRoot(projectRoot, (0, import_node_path2.resolve)(projectRoot, value));
319
422
  } catch {
320
423
  issues.push({
321
424
  path: fieldPath,
@@ -345,14 +448,14 @@ function resolveSafePackageOutputOverride(projectRoot, override) {
345
448
  }
346
449
  if ((0, import_node_path2.isAbsolute)(trimmed)) {
347
450
  const resolved2 = (0, import_node_path2.resolve)(trimmed);
348
- assertResolvedPathUnderRoot(root, resolved2);
451
+ assertRealPathUnderRoot(root, resolved2);
349
452
  return resolved2;
350
453
  }
351
454
  if (!isSafeRelativeSpaPath(trimmed)) {
352
455
  throw new Error(`unsafe output path: ${override}`);
353
456
  }
354
457
  const resolved = (0, import_node_path2.resolve)(root, trimmed);
355
- assertResolvedPathUnderRoot(root, resolved);
458
+ assertRealPathUnderRoot(root, resolved);
356
459
  return resolved;
357
460
  }
358
461
 
@@ -399,6 +502,18 @@ function extractAssessments(descriptor) {
399
502
  }
400
503
 
401
504
  // src/interchange.ts
505
+ function mapDescriptorTracking(tracking) {
506
+ if (!tracking) return void 0;
507
+ const mapped = {};
508
+ if (tracking.completion?.threshold !== void 0) {
509
+ mapped.completion = { threshold: tracking.completion.threshold };
510
+ }
511
+ const activityIri = tracking.xapi?.activityIri?.trim();
512
+ if (activityIri) {
513
+ mapped.xapi = { activityIri };
514
+ }
515
+ return Object.keys(mapped).length > 0 ? mapped : void 0;
516
+ }
402
517
  function resolveSpaLessons(descriptor) {
403
518
  const mapped = mapLessonkitIds(descriptor);
404
519
  if (descriptor.layout === "single-spa") {
@@ -436,7 +551,7 @@ function descriptorToInterchange(descriptor) {
436
551
  type: "spa",
437
552
  path: l.path
438
553
  })),
439
- tracking: descriptor.tracking,
554
+ tracking: mapDescriptorTracking(descriptor.tracking),
440
555
  runtime: runtime ? {
441
556
  theme: runtime.theme,
442
557
  cssVariables: runtime.cssVariables
@@ -513,7 +628,7 @@ async function writeLxpackProject(options) {
513
628
  const descriptor = validation.descriptor;
514
629
  const outDir = (0, import_node_path4.resolve)(options.outDir);
515
630
  if (options.projectRoot) {
516
- assertResolvedPathUnderRoot((0, import_node_path4.resolve)(options.projectRoot), outDir);
631
+ assertRealPathUnderRoot((0, import_node_path4.resolve)(options.projectRoot), outDir);
517
632
  }
518
633
  const spaDirs = await resolveSpaDirs({ ...options, descriptor });
519
634
  const interchange = descriptorToInterchange(descriptor);
@@ -537,7 +652,7 @@ async function writeLxpackProject(options) {
537
652
  }
538
653
 
539
654
  // src/packageCourse.ts
540
- var import_node_path7 = require("path");
655
+ var import_node_path8 = require("path");
541
656
  var fsp3 = __toESM(require("fs/promises"), 1);
542
657
  var import_api2 = require("@lxpack/api");
543
658
 
@@ -549,7 +664,7 @@ function validatePackageInputs(options) {
549
664
  const projectRoot = options.projectRoot ? (0, import_node_path5.resolve)(options.projectRoot) : void 0;
550
665
  if (projectRoot) {
551
666
  try {
552
- assertResolvedPathUnderRoot(projectRoot, outDir);
667
+ assertRealPathUnderRoot(projectRoot, outDir);
553
668
  } catch (err) {
554
669
  return {
555
670
  ok: false,
@@ -596,7 +711,7 @@ function validatePackageInputs(options) {
596
711
  if (projectRoot && output) {
597
712
  const resolvedOutput = (0, import_node_path5.resolve)(projectRoot, output);
598
713
  try {
599
- assertResolvedPathUnderRoot(projectRoot, resolvedOutput);
714
+ assertRealPathUnderRoot(projectRoot, resolvedOutput);
600
715
  } catch (err) {
601
716
  return {
602
717
  ok: false,
@@ -625,17 +740,21 @@ function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
625
740
  if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
626
741
  return artifactPath;
627
742
  }
628
- const stagingResolved = resolveComparablePath(stagingRoot);
629
- const relative2 = resolved === stagingResolved ? "" : resolved.slice(stagingResolved.length).replace(/^[/\\]/, "");
630
- if (!relative2) return outDir;
743
+ const rel = relativePathUnderRoot(stagingRoot, resolved);
744
+ if (rel.startsWith("..") || (0, import_node_path5.isAbsolute)(rel)) {
745
+ return artifactPath;
746
+ }
747
+ if (!rel) return outDir;
631
748
  if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
632
- return import_node_path5.win32.join(outDir, relative2.replace(/\//g, import_node_path5.win32.sep));
749
+ return import_node_path5.win32.join(outDir, rel.replace(/\//g, import_node_path5.win32.sep));
633
750
  }
634
- return (0, import_node_path5.join)(outDir, relative2);
751
+ return (0, import_node_path5.join)(outDir, rel);
635
752
  }
636
753
 
637
754
  // src/packaging/promote.ts
638
755
  var fsp = __toESM(require("fs/promises"), 1);
756
+ var import_node_crypto = require("crypto");
757
+ var import_node_path6 = require("path");
639
758
  async function pathExists(path) {
640
759
  try {
641
760
  await fsp.access(path);
@@ -654,22 +773,36 @@ async function renameOrCopy(from, to) {
654
773
  await fsp.rm(from, { recursive: true, force: true });
655
774
  }
656
775
  }
776
+ async function assertNoLegacyPromoteArtifacts(outDir) {
777
+ const legacyTmp = `${outDir}.tmp-promote`;
778
+ const legacyBak = `${outDir}.bak`;
779
+ const stale = [];
780
+ if (await pathExists(legacyTmp)) stale.push(legacyTmp);
781
+ if (await pathExists(legacyBak)) stale.push(legacyBak);
782
+ if (stale.length) {
783
+ throw new Error(
784
+ `[lessonkit/lxpack] cannot promote: remove stale packaging artifacts from a previous failed run: ${stale.join(", ")}`
785
+ );
786
+ }
787
+ }
657
788
  async function promoteStagingToOutDir(stagingDir, outDir) {
658
- const tmpPromote = `${outDir}.tmp-promote`;
659
- const backup = `${outDir}.bak`;
789
+ await assertNoLegacyPromoteArtifacts(outDir);
790
+ const parent = (0, import_node_path6.dirname)(outDir);
791
+ const tmpPromote = await fsp.mkdtemp((0, import_node_path6.join)(parent, ".lk-promote-"));
660
792
  await renameOrCopy(stagingDir, tmpPromote);
661
793
  const hadOutDir = await pathExists(outDir);
662
- if (hadOutDir) {
794
+ const backup = hadOutDir ? await fsp.mkdtemp((0, import_node_path6.join)(parent, ".lk-backup-")) : void 0;
795
+ if (hadOutDir && backup) {
663
796
  await renameOrCopy(outDir, backup);
664
797
  }
665
798
  try {
666
799
  await renameOrCopy(tmpPromote, outDir);
667
800
  } catch (promoteError) {
668
- if (hadOutDir) {
801
+ if (hadOutDir && backup) {
669
802
  try {
670
803
  await renameOrCopy(backup, outDir);
671
804
  } catch (restoreError) {
672
- const failedPromote2 = `${outDir}.failed-promote-${Date.now()}`;
805
+ const failedPromote2 = (0, import_node_path6.join)(parent, `.lk-failed-promote-${(0, import_node_crypto.randomUUID)()}`);
673
806
  try {
674
807
  await renameOrCopy(tmpPromote, failedPromote2);
675
808
  } catch {
@@ -693,7 +826,7 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
693
826
  }
694
827
  throw promoteError;
695
828
  }
696
- const failedPromote = `${outDir}.failed-promote-${Date.now()}`;
829
+ const failedPromote = (0, import_node_path6.join)(parent, `.lk-failed-promote-${(0, import_node_crypto.randomUUID)()}`);
697
830
  try {
698
831
  await renameOrCopy(tmpPromote, failedPromote);
699
832
  } catch {
@@ -701,19 +834,19 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
701
834
  }
702
835
  throw promoteError;
703
836
  }
704
- if (hadOutDir) {
837
+ if (backup) {
705
838
  await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
706
839
  }
707
840
  }
708
841
 
709
842
  // src/packaging/staging.ts
710
843
  var fsp2 = __toESM(require("fs/promises"), 1);
711
- var import_node_path6 = require("path");
844
+ var import_node_path7 = require("path");
712
845
  var import_node_os = require("os");
713
846
  var import_api = require("@lxpack/api");
714
847
  async function buildStagingPackage(options) {
715
848
  const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
716
- const stagingDir = await fsp2.mkdtemp((0, import_node_path6.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
849
+ const stagingDir = await fsp2.mkdtemp((0, import_node_path7.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
717
850
  try {
718
851
  let spaDirs;
719
852
  try {
@@ -732,8 +865,8 @@ async function buildStagingPackage(options) {
732
865
  }
733
866
  const interchange = descriptorToInterchange(descriptor);
734
867
  const outputBase = outputBaseDir ?? ".lxpack/out";
735
- await fsp2.mkdir((0, import_node_path6.join)(stagingDir, outputBase), { recursive: true });
736
- const defaultOutput = output ?? (dir ? (0, import_node_path6.join)(outputBase, target) : (0, import_node_path6.join)(outputBase, `course-${target}.zip`));
868
+ await fsp2.mkdir((0, import_node_path7.join)(stagingDir, outputBase), { recursive: true });
869
+ const defaultOutput = output ?? (dir ? (0, import_node_path7.join)(outputBase, target) : (0, import_node_path7.join)(outputBase, `course-${target}.zip`));
737
870
  const build = await (0, import_api.packageLessonkit)({
738
871
  interchange,
739
872
  spaDirs,
@@ -770,25 +903,26 @@ async function buildStagingPackage(options) {
770
903
  }
771
904
  }
772
905
  async function ensureOutDirParent(outDir) {
773
- await fsp2.mkdir((0, import_node_path6.dirname)(outDir), { recursive: true });
906
+ await fsp2.mkdir((0, import_node_path7.dirname)(outDir), { recursive: true });
774
907
  }
775
908
 
776
909
  // src/packageCourse.ts
777
910
  async function validateLessonkitProject(options) {
778
911
  return (0, import_api2.validateCourse)({
779
- courseDir: (0, import_node_path7.resolve)(options.courseDir),
912
+ courseDir: (0, import_node_path8.resolve)(options.courseDir),
780
913
  target: options.target
781
914
  });
782
915
  }
783
916
  async function buildLessonkitProject(options) {
784
- return (0, import_api2.buildCourse)({
785
- courseDir: (0, import_node_path7.resolve)(options.courseDir),
917
+ const buildOptions = {
918
+ courseDir: (0, import_node_path8.resolve)(options.courseDir),
786
919
  target: options.target,
787
920
  output: options.output,
788
921
  dir: options.dir,
789
922
  outputBaseDir: options.outputBaseDir,
790
923
  assessments: options.assessments
791
- });
924
+ };
925
+ return (0, import_api2.buildCourse)(buildOptions);
792
926
  }
793
927
  async function packageLessonkitCourse(options) {
794
928
  const { target, output, dir, outputBaseDir, ...writeOpts } = options;
@@ -808,11 +942,11 @@ async function packageLessonkitCourse(options) {
808
942
  };
809
943
  }
810
944
  const outDir = inputValidation.outDir;
811
- const descriptorValidation = validateDescriptor(writeOpts.descriptor);
945
+ const descriptorValidation = validateDescriptorForTarget(writeOpts.descriptor, target);
812
946
  if (!descriptorValidation.ok) {
813
947
  return {
814
948
  ok: false,
815
- courseDir: outDir,
949
+ courseDir: (0, import_node_path8.resolve)(writeOpts.outDir),
816
950
  target,
817
951
  issues: descriptorValidation.issues.map((i) => ({
818
952
  path: i.path,
@@ -821,22 +955,6 @@ async function packageLessonkitCourse(options) {
821
955
  };
822
956
  }
823
957
  const descriptor = descriptorValidation.descriptor;
824
- if (target === "xapi" || target === "cmi5") {
825
- const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
826
- if (!activityIri) {
827
- return {
828
- ok: false,
829
- courseDir: outDir,
830
- target,
831
- issues: [
832
- {
833
- path: "course.tracking.xapi.activityIri",
834
- message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
835
- }
836
- ]
837
- };
838
- }
839
- }
840
958
  const staged = await buildStagingPackage({
841
959
  ...writeOpts,
842
960
  descriptor,
@@ -930,14 +1048,19 @@ function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
930
1048
  }
931
1049
  const config = raw;
932
1050
  const issues = [];
933
- if (config.schemaVersion !== 1) {
1051
+ let schemaVersion = config.schemaVersion;
1052
+ if (schemaVersion === "1") {
1053
+ schemaVersion = 1;
1054
+ }
1055
+ if (schemaVersion !== 1) {
934
1056
  issues.push({
935
1057
  path: "schemaVersion",
936
1058
  message: `must be 1 (got ${String(config.schemaVersion)})`
937
1059
  });
938
1060
  }
939
- const name = config.name;
940
- if (typeof name !== "string" || !name.trim()) {
1061
+ const nameRaw = config.name;
1062
+ const name = typeof nameRaw === "string" ? nameRaw.trim() : "";
1063
+ if (!name) {
941
1064
  issues.push({ path: "name", message: "must be a non-empty string" });
942
1065
  }
943
1066
  const courseRaw = config.course;
@@ -1026,6 +1149,15 @@ var import_tracking_schema2 = require("@lxpack/tracking-schema");
1026
1149
  // src/telemetry.ts
1027
1150
  var import_tracking_schema = require("@lxpack/tracking-schema");
1028
1151
  var SUPPORTED = new Set(import_tracking_schema.LESSONKIT_TELEMETRY_EVENTS);
1152
+ function isQuizAnsweredData(data) {
1153
+ return typeof data === "object" && data !== null && typeof data.checkId === "string";
1154
+ }
1155
+ function isQuizCompletedData(data) {
1156
+ return typeof data === "object" && data !== null && typeof data.checkId === "string";
1157
+ }
1158
+ function isInteractionData(data) {
1159
+ return typeof data === "object" && data !== null;
1160
+ }
1029
1161
  function telemetryEventToLessonkit(event) {
1030
1162
  if (!SUPPORTED.has(event.name)) {
1031
1163
  return null;
@@ -1037,16 +1169,16 @@ function telemetryEventToLessonkit(event) {
1037
1169
  };
1038
1170
  if (name === "quiz_completed" || name === "quiz_answered") {
1039
1171
  const data = event.data;
1040
- mapped.assessmentId = data?.checkId;
1041
- if (data && "score" in data) {
1042
- mapped.score = data.score;
1043
- mapped.maxScore = data.maxScore;
1044
- mapped.passingScore = data.passingScore;
1045
- }
1046
- if (data) {
1172
+ if (isQuizAnsweredData(data) || isQuizCompletedData(data)) {
1173
+ mapped.assessmentId = data.checkId;
1174
+ if ("score" in data) {
1175
+ mapped.score = data.score;
1176
+ mapped.maxScore = data.maxScore;
1177
+ mapped.passingScore = data.passingScore;
1178
+ }
1047
1179
  mapped.data = data;
1048
1180
  }
1049
- } else if (name === "interaction" && event.data) {
1181
+ } else if (name === "interaction" && event.data && isInteractionData(event.data)) {
1050
1182
  mapped.data = event.data;
1051
1183
  }
1052
1184
  return mapped;
@@ -1079,6 +1211,7 @@ var import_validators2 = require("@lxpack/validators");
1079
1211
  telemetryEventToLessonkit,
1080
1212
  themeToLxpackRuntime,
1081
1213
  validateDescriptor,
1214
+ validateDescriptorForTarget,
1082
1215
  validateLessonkitProject,
1083
1216
  validatePackageInputs,
1084
1217
  validateProjectPaths,
package/dist/index.d.cts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { CheckId, CourseId, LessonId } from '@lessonkit/core';
2
2
  import { ThemePresetName, LessonkitThemeV1 } from '@lessonkit/themes';
3
- import { LessonkitInterchangeV1 } from '@lxpack/validators';
4
- export { LessonkitInterchangeV1, MaterializeLessonkitOptions, MaterializeLessonkitResult, lessonkitInterchangeSchema, materializeLessonkitProject, parseLessonkitInterchange } from '@lxpack/validators';
5
3
  import { ExportTarget, BuildCourseResult, ValidateCourseResult } from '@lxpack/api';
6
4
  export { ExportTarget } from '@lxpack/api';
5
+ import { LessonkitInterchangeV1 } from '@lxpack/validators';
6
+ export { LessonkitInterchangeV1, MaterializeLessonkitOptions, MaterializeLessonkitResult, lessonkitInterchangeSchema, materializeLessonkitProject, parseLessonkitInterchange } from '@lxpack/validators';
7
7
  export { LESSONKIT_TELEMETRY_EVENTS, LessonkitBridgeAction, LessonkitTelemetryEvent, LessonkitTelemetryEventName, TrackingSchemaEvent, mapLessonkitTelemetryToBridgeAction, mapLessonkitTelemetryToLxpack } from '@lxpack/tracking-schema';
8
8
  export { t as telemetryEventToLessonkit } from './telemetry-gCxlwc7I.cjs';
9
9
 
@@ -53,10 +53,14 @@ type MappedLessonkitIds = {
53
53
  checkIds: CheckId[];
54
54
  };
55
55
 
56
- type DescriptorValidationIssue = {
56
+ /** Shared validation issue shape across lxpack parsers. */
57
+ type ValidationIssue = {
57
58
  path: string;
58
59
  message: string;
60
+ severity?: string;
59
61
  };
62
+
63
+ type DescriptorValidationIssue = ValidationIssue;
60
64
  type DescriptorValidationResult = {
61
65
  ok: true;
62
66
  descriptor: LessonkitCourseDescriptor;
@@ -64,7 +68,8 @@ type DescriptorValidationResult = {
64
68
  ok: false;
65
69
  issues: DescriptorValidationIssue[];
66
70
  };
67
- declare function validateDescriptor(input: LessonkitCourseDescriptor): DescriptorValidationResult;
71
+ declare function validateDescriptor(input: unknown): DescriptorValidationResult;
72
+ declare function validateDescriptorForTarget(input: unknown, target?: ExportTarget): DescriptorValidationResult;
68
73
 
69
74
  type ProjectPathsInput = {
70
75
  spaDistDir?: string;
@@ -94,7 +99,7 @@ declare function themeToLxpackRuntime(input: {
94
99
  }): LxpackRuntimeTheme;
95
100
 
96
101
  type SpaLessonEntry = {
97
- id: string;
102
+ id: LessonId;
98
103
  title: string;
99
104
  path: string;
100
105
  };
@@ -188,7 +193,7 @@ type BuildLessonkitProjectOptions = {
188
193
  output?: string;
189
194
  dir?: boolean;
190
195
  outputBaseDir?: string;
191
- assessments?: unknown[];
196
+ assessments?: LxpackInjectedAssessment[];
192
197
  };
193
198
  type PackageLessonkitCourseOptions = WriteLxpackProjectOptions & {
194
199
  target: ExportTarget;
@@ -268,4 +273,4 @@ type ParseManifestResult = {
268
273
  declare function parseLessonkitManifest(raw: unknown, label?: string, projectRoot?: string): ParseManifestResult;
269
274
  declare function loadLessonkitManifestFromFile(readJson: () => Promise<unknown>, label?: string, projectRoot?: string): Promise<ParseManifestResult>;
270
275
 
271
- export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitManifest, type LessonkitManifestPaths, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type ManifestParseIssue, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type PackageValidationIssue, type ParseManifestResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type ValidatePackageInputsResult, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, buildStagingPackage, descriptorToInterchange, ensureOutDirParent, extractAssessments, loadLessonkitManifestFromFile, mapLessonkitIds, packageLessonkitCourse, parseLessonkitManifest, promoteStagingToOutDir, remapArtifactPaths, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateLessonkitProject, validatePackageInputs, validateProjectPaths, writeLxpackProject };
276
+ export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitManifest, type LessonkitManifestPaths, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type ManifestParseIssue, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type PackageValidationIssue, type ParseManifestResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type ValidatePackageInputsResult, type ValidationIssue, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, buildStagingPackage, descriptorToInterchange, ensureOutDirParent, extractAssessments, loadLessonkitManifestFromFile, mapLessonkitIds, packageLessonkitCourse, parseLessonkitManifest, promoteStagingToOutDir, remapArtifactPaths, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateDescriptorForTarget, validateLessonkitProject, validatePackageInputs, validateProjectPaths, writeLxpackProject };