@lessonkit/lxpack 1.0.0 → 1.0.1

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;
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 {
@@ -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,7 +942,7 @@ 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,
@@ -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 };
package/dist/index.d.ts 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.js';
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 };
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  telemetryEventToLessonkit
3
- } from "./chunk-PSUSESH3.js";
3
+ } from "./chunk-DYQI222N.js";
4
4
 
5
5
  // src/validateDescriptor.ts
6
6
  import { validateId } from "@lessonkit/core";
7
7
 
8
8
  // src/spaPath.ts
9
9
  import { realpathSync } from "fs";
10
- import { relative, resolve, sep, win32 } from "path";
10
+ import { isAbsolute, relative, resolve, sep, win32 } from "path";
11
11
  function resolveComparablePath(p) {
12
12
  if (/^[a-zA-Z]:[/\\]/.test(p)) {
13
13
  return win32.resolve(p);
@@ -17,10 +17,11 @@ function resolveComparablePath(p) {
17
17
  function isSafeRelativeSpaPath(spaPath) {
18
18
  if (!spaPath.length || spaPath.includes("\0")) return false;
19
19
  if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
20
- if (/^[a-zA-Z]:[/\\]/.test(spaPath)) return false;
21
- const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0);
20
+ if (/^[a-zA-Z]:/.test(spaPath)) return false;
21
+ if (spaPath === "." || spaPath === "./") return false;
22
+ const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0 && s !== ".");
22
23
  if (segments.some((s) => s === "..")) return false;
23
- return true;
24
+ return segments.length > 0;
24
25
  }
25
26
  function assertResolvedPathUnderRoot(root, target) {
26
27
  const rootResolved = resolveComparablePath(root);
@@ -52,13 +53,25 @@ function assertRealPathUnderRoot(root, target) {
52
53
  }
53
54
  assertResolvedPathUnderRoot(rootReal, targetCheck);
54
55
  }
56
+ function normalizePathForComparison(p) {
57
+ const resolved = resolveComparablePath(p);
58
+ return /^[a-zA-Z]:[/\\]/.test(resolved) ? resolved.toLowerCase() : resolved;
59
+ }
60
+ function relativePathUnderRoot(root, target) {
61
+ const rootResolved = normalizePathForComparison(root);
62
+ const targetResolved = normalizePathForComparison(target);
63
+ if (/^[a-zA-Z]:[/\\]/.test(rootResolved)) {
64
+ return win32.relative(rootResolved, targetResolved);
65
+ }
66
+ return relative(rootResolved, targetResolved);
67
+ }
55
68
  function isResolvedPathUnderRoot(root, target) {
56
- const rootResolved = resolveComparablePath(root);
57
- const targetResolved = resolveComparablePath(target);
69
+ const rootResolved = normalizePathForComparison(root);
70
+ const targetResolved = normalizePathForComparison(target);
58
71
  if (targetResolved === rootResolved) return true;
59
- const prefix = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep;
60
- const win32Prefix = rootResolved.endsWith(win32.sep) ? rootResolved : rootResolved + win32.sep;
61
- return targetResolved.startsWith(prefix) || targetResolved.startsWith(win32Prefix);
72
+ const rel = relativePathUnderRoot(root, target);
73
+ if (!rel) return true;
74
+ return !rel.startsWith("..") && !isAbsolute(rel);
62
75
  }
63
76
 
64
77
  // src/theme.ts
@@ -79,6 +92,70 @@ function themeToLxpackRuntime(input) {
79
92
  // src/validateDescriptor.ts
80
93
  var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
81
94
  var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
95
+ function isRecord(value) {
96
+ return typeof value === "object" && value !== null && !Array.isArray(value);
97
+ }
98
+ function parseLessonDescriptor(raw) {
99
+ if (!isRecord(raw)) {
100
+ return { id: "", title: "" };
101
+ }
102
+ return {
103
+ id: typeof raw.id === "string" ? raw.id : "",
104
+ title: typeof raw.title === "string" ? raw.title : "",
105
+ spaPath: typeof raw.spaPath === "string" ? raw.spaPath : void 0
106
+ };
107
+ }
108
+ function parseAssessmentDescriptor(raw) {
109
+ if (!isRecord(raw)) {
110
+ return { checkId: "", question: "", choices: [], answer: "" };
111
+ }
112
+ return {
113
+ checkId: typeof raw.checkId === "string" ? raw.checkId : "",
114
+ question: typeof raw.question === "string" ? raw.question : "",
115
+ choices: Array.isArray(raw.choices) ? raw.choices.filter((c) => typeof c === "string") : [],
116
+ answer: typeof raw.answer === "string" ? raw.answer : "",
117
+ passingScore: typeof raw.passingScore === "number" ? raw.passingScore : void 0
118
+ };
119
+ }
120
+ function parseCourseDescriptorInput(input) {
121
+ if (!isRecord(input)) return null;
122
+ const trackingRaw = input.tracking;
123
+ let tracking;
124
+ if (isRecord(trackingRaw)) {
125
+ const completionRaw = trackingRaw.completion;
126
+ const xapiRaw = trackingRaw.xapi;
127
+ tracking = {
128
+ completion: isRecord(completionRaw) ? {
129
+ threshold: typeof completionRaw.threshold === "number" ? completionRaw.threshold : void 0
130
+ } : void 0,
131
+ xapi: isRecord(xapiRaw) ? {
132
+ activityIri: typeof xapiRaw.activityIri === "string" ? xapiRaw.activityIri : void 0
133
+ } : void 0
134
+ };
135
+ }
136
+ const themeRaw = input.theme;
137
+ let theme;
138
+ if (isRecord(themeRaw)) {
139
+ theme = {
140
+ preset: typeof themeRaw.preset === "string" ? themeRaw.preset : void 0
141
+ };
142
+ if (isRecord(themeRaw.theme)) {
143
+ theme.theme = themeRaw.theme;
144
+ }
145
+ }
146
+ return {
147
+ courseId: typeof input.courseId === "string" ? input.courseId : "",
148
+ title: typeof input.title === "string" ? input.title : "",
149
+ version: typeof input.version === "string" ? input.version : void 0,
150
+ layout: typeof input.layout === "string" ? input.layout : void 0,
151
+ lessons: Array.isArray(input.lessons) ? input.lessons.map(parseLessonDescriptor) : [],
152
+ assessments: Array.isArray(input.assessments) ? input.assessments.map(parseAssessmentDescriptor) : void 0,
153
+ theme,
154
+ tracking,
155
+ spaDistDir: typeof input.spaDistDir === "string" ? input.spaDistDir : void 0,
156
+ spaLessonId: typeof input.spaLessonId === "string" ? input.spaLessonId : void 0
157
+ };
158
+ }
82
159
  function normalizeDescriptor(input) {
83
160
  const course = validateId(input.courseId, "courseId");
84
161
  if (!course.ok) throw new Error("normalizeDescriptor called with invalid courseId");
@@ -112,6 +189,31 @@ function normalizeDescriptor(input) {
112
189
  };
113
190
  }
114
191
  function validateDescriptor(input) {
192
+ const parsed = parseCourseDescriptorInput(input);
193
+ if (parsed === null) {
194
+ return { ok: false, issues: [{ path: "course", message: "must be an object" }] };
195
+ }
196
+ return validateDescriptorParsed(parsed);
197
+ }
198
+ function validateDescriptorForTarget(input, target) {
199
+ const result = validateDescriptor(input);
200
+ if (!result.ok || !target) return result;
201
+ if (target !== "xapi" && target !== "cmi5") return result;
202
+ const activityIri = result.descriptor.tracking?.xapi?.activityIri?.trim();
203
+ if (!activityIri) {
204
+ return {
205
+ ok: false,
206
+ issues: [
207
+ {
208
+ path: "course.tracking.xapi.activityIri",
209
+ message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
210
+ }
211
+ ]
212
+ };
213
+ }
214
+ return result;
215
+ }
216
+ function validateDescriptorParsed(input) {
115
217
  const issues = [];
116
218
  const course = validateId(input.courseId, "courseId");
117
219
  if (!course.ok) issues.push(...course.issues.map((i) => ({ path: i.path, message: i.message })));
@@ -247,7 +349,7 @@ function validateDescriptor(input) {
247
349
  }
248
350
 
249
351
  // src/validateProjectPaths.ts
250
- import { isAbsolute, resolve as resolve2 } from "path";
352
+ import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
251
353
  function validatePathField(value, fieldPath, projectRoot, issues) {
252
354
  if (!isSafeRelativeSpaPath(value)) {
253
355
  issues.push({
@@ -257,7 +359,7 @@ function validatePathField(value, fieldPath, projectRoot, issues) {
257
359
  return;
258
360
  }
259
361
  try {
260
- assertResolvedPathUnderRoot(projectRoot, resolve2(projectRoot, value));
362
+ assertRealPathUnderRoot(projectRoot, resolve2(projectRoot, value));
261
363
  } catch {
262
364
  issues.push({
263
365
  path: fieldPath,
@@ -285,16 +387,16 @@ function resolveSafePackageOutputOverride(projectRoot, override) {
285
387
  if (!trimmed) {
286
388
  throw new Error("output override must be a non-empty path");
287
389
  }
288
- if (isAbsolute(trimmed)) {
390
+ if (isAbsolute2(trimmed)) {
289
391
  const resolved2 = resolve2(trimmed);
290
- assertResolvedPathUnderRoot(root, resolved2);
392
+ assertRealPathUnderRoot(root, resolved2);
291
393
  return resolved2;
292
394
  }
293
395
  if (!isSafeRelativeSpaPath(trimmed)) {
294
396
  throw new Error(`unsafe output path: ${override}`);
295
397
  }
296
398
  const resolved = resolve2(root, trimmed);
297
- assertResolvedPathUnderRoot(root, resolved);
399
+ assertRealPathUnderRoot(root, resolved);
298
400
  return resolved;
299
401
  }
300
402
 
@@ -341,6 +443,18 @@ function extractAssessments(descriptor) {
341
443
  }
342
444
 
343
445
  // src/interchange.ts
446
+ function mapDescriptorTracking(tracking) {
447
+ if (!tracking) return void 0;
448
+ const mapped = {};
449
+ if (tracking.completion?.threshold !== void 0) {
450
+ mapped.completion = { threshold: tracking.completion.threshold };
451
+ }
452
+ const activityIri = tracking.xapi?.activityIri?.trim();
453
+ if (activityIri) {
454
+ mapped.xapi = { activityIri };
455
+ }
456
+ return Object.keys(mapped).length > 0 ? mapped : void 0;
457
+ }
344
458
  function resolveSpaLessons(descriptor) {
345
459
  const mapped = mapLessonkitIds(descriptor);
346
460
  if (descriptor.layout === "single-spa") {
@@ -378,7 +492,7 @@ function descriptorToInterchange(descriptor) {
378
492
  type: "spa",
379
493
  path: l.path
380
494
  })),
381
- tracking: descriptor.tracking,
495
+ tracking: mapDescriptorTracking(descriptor.tracking),
382
496
  runtime: runtime ? {
383
497
  theme: runtime.theme,
384
498
  cssVariables: runtime.cssVariables
@@ -455,7 +569,7 @@ async function writeLxpackProject(options) {
455
569
  const descriptor = validation.descriptor;
456
570
  const outDir = resolve4(options.outDir);
457
571
  if (options.projectRoot) {
458
- assertResolvedPathUnderRoot(resolve4(options.projectRoot), outDir);
572
+ assertRealPathUnderRoot(resolve4(options.projectRoot), outDir);
459
573
  }
460
574
  const spaDirs = await resolveSpaDirs({ ...options, descriptor });
461
575
  const interchange = descriptorToInterchange(descriptor);
@@ -487,14 +601,14 @@ import {
487
601
  } from "@lxpack/api";
488
602
 
489
603
  // src/packaging/validateInputs.ts
490
- import { join as join3, resolve as resolve5, win32 as win322 } from "path";
604
+ import { isAbsolute as isAbsolute3, join as join3, resolve as resolve5, win32 as win322 } from "path";
491
605
  function validatePackageInputs(options) {
492
606
  const { target, output, outputBaseDir } = options;
493
607
  const outDir = resolve5(options.outDir);
494
608
  const projectRoot = options.projectRoot ? resolve5(options.projectRoot) : void 0;
495
609
  if (projectRoot) {
496
610
  try {
497
- assertResolvedPathUnderRoot(projectRoot, outDir);
611
+ assertRealPathUnderRoot(projectRoot, outDir);
498
612
  } catch (err) {
499
613
  return {
500
614
  ok: false,
@@ -541,7 +655,7 @@ function validatePackageInputs(options) {
541
655
  if (projectRoot && output) {
542
656
  const resolvedOutput = resolve5(projectRoot, output);
543
657
  try {
544
- assertResolvedPathUnderRoot(projectRoot, resolvedOutput);
658
+ assertRealPathUnderRoot(projectRoot, resolvedOutput);
545
659
  } catch (err) {
546
660
  return {
547
661
  ok: false,
@@ -570,17 +684,21 @@ function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
570
684
  if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
571
685
  return artifactPath;
572
686
  }
573
- const stagingResolved = resolveComparablePath(stagingRoot);
574
- const relative2 = resolved === stagingResolved ? "" : resolved.slice(stagingResolved.length).replace(/^[/\\]/, "");
575
- if (!relative2) return outDir;
687
+ const rel = relativePathUnderRoot(stagingRoot, resolved);
688
+ if (rel.startsWith("..") || isAbsolute3(rel)) {
689
+ return artifactPath;
690
+ }
691
+ if (!rel) return outDir;
576
692
  if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
577
- return win322.join(outDir, relative2.replace(/\//g, win322.sep));
693
+ return win322.join(outDir, rel.replace(/\//g, win322.sep));
578
694
  }
579
- return join3(outDir, relative2);
695
+ return join3(outDir, rel);
580
696
  }
581
697
 
582
698
  // src/packaging/promote.ts
583
699
  import * as fsp from "fs/promises";
700
+ import { randomUUID } from "crypto";
701
+ import { dirname, join as join4 } from "path";
584
702
  async function pathExists(path) {
585
703
  try {
586
704
  await fsp.access(path);
@@ -599,22 +717,36 @@ async function renameOrCopy(from, to) {
599
717
  await fsp.rm(from, { recursive: true, force: true });
600
718
  }
601
719
  }
720
+ async function assertNoLegacyPromoteArtifacts(outDir) {
721
+ const legacyTmp = `${outDir}.tmp-promote`;
722
+ const legacyBak = `${outDir}.bak`;
723
+ const stale = [];
724
+ if (await pathExists(legacyTmp)) stale.push(legacyTmp);
725
+ if (await pathExists(legacyBak)) stale.push(legacyBak);
726
+ if (stale.length) {
727
+ throw new Error(
728
+ `[lessonkit/lxpack] cannot promote: remove stale packaging artifacts from a previous failed run: ${stale.join(", ")}`
729
+ );
730
+ }
731
+ }
602
732
  async function promoteStagingToOutDir(stagingDir, outDir) {
603
- const tmpPromote = `${outDir}.tmp-promote`;
604
- const backup = `${outDir}.bak`;
733
+ await assertNoLegacyPromoteArtifacts(outDir);
734
+ const parent = dirname(outDir);
735
+ const tmpPromote = await fsp.mkdtemp(join4(parent, ".lk-promote-"));
605
736
  await renameOrCopy(stagingDir, tmpPromote);
606
737
  const hadOutDir = await pathExists(outDir);
607
- if (hadOutDir) {
738
+ const backup = hadOutDir ? await fsp.mkdtemp(join4(parent, ".lk-backup-")) : void 0;
739
+ if (hadOutDir && backup) {
608
740
  await renameOrCopy(outDir, backup);
609
741
  }
610
742
  try {
611
743
  await renameOrCopy(tmpPromote, outDir);
612
744
  } catch (promoteError) {
613
- if (hadOutDir) {
745
+ if (hadOutDir && backup) {
614
746
  try {
615
747
  await renameOrCopy(backup, outDir);
616
748
  } catch (restoreError) {
617
- const failedPromote2 = `${outDir}.failed-promote-${Date.now()}`;
749
+ const failedPromote2 = join4(parent, `.lk-failed-promote-${randomUUID()}`);
618
750
  try {
619
751
  await renameOrCopy(tmpPromote, failedPromote2);
620
752
  } catch {
@@ -638,7 +770,7 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
638
770
  }
639
771
  throw promoteError;
640
772
  }
641
- const failedPromote = `${outDir}.failed-promote-${Date.now()}`;
773
+ const failedPromote = join4(parent, `.lk-failed-promote-${randomUUID()}`);
642
774
  try {
643
775
  await renameOrCopy(tmpPromote, failedPromote);
644
776
  } catch {
@@ -646,19 +778,19 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
646
778
  }
647
779
  throw promoteError;
648
780
  }
649
- if (hadOutDir) {
781
+ if (backup) {
650
782
  await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
651
783
  }
652
784
  }
653
785
 
654
786
  // src/packaging/staging.ts
655
787
  import * as fsp2 from "fs/promises";
656
- import { dirname, join as join4 } from "path";
788
+ import { dirname as dirname2, join as join5 } from "path";
657
789
  import { tmpdir } from "os";
658
790
  import { packageLessonkit } from "@lxpack/api";
659
791
  async function buildStagingPackage(options) {
660
792
  const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
661
- const stagingDir = await fsp2.mkdtemp(join4(tmpdir(), "lessonkit-lxpack-"));
793
+ const stagingDir = await fsp2.mkdtemp(join5(tmpdir(), "lessonkit-lxpack-"));
662
794
  try {
663
795
  let spaDirs;
664
796
  try {
@@ -677,8 +809,8 @@ async function buildStagingPackage(options) {
677
809
  }
678
810
  const interchange = descriptorToInterchange(descriptor);
679
811
  const outputBase = outputBaseDir ?? ".lxpack/out";
680
- await fsp2.mkdir(join4(stagingDir, outputBase), { recursive: true });
681
- const defaultOutput = output ?? (dir ? join4(outputBase, target) : join4(outputBase, `course-${target}.zip`));
812
+ await fsp2.mkdir(join5(stagingDir, outputBase), { recursive: true });
813
+ const defaultOutput = output ?? (dir ? join5(outputBase, target) : join5(outputBase, `course-${target}.zip`));
682
814
  const build = await packageLessonkit({
683
815
  interchange,
684
816
  spaDirs,
@@ -715,7 +847,7 @@ async function buildStagingPackage(options) {
715
847
  }
716
848
  }
717
849
  async function ensureOutDirParent(outDir) {
718
- await fsp2.mkdir(dirname(outDir), { recursive: true });
850
+ await fsp2.mkdir(dirname2(outDir), { recursive: true });
719
851
  }
720
852
 
721
853
  // src/packageCourse.ts
@@ -726,14 +858,15 @@ async function validateLessonkitProject(options) {
726
858
  });
727
859
  }
728
860
  async function buildLessonkitProject(options) {
729
- return buildCourse({
861
+ const buildOptions = {
730
862
  courseDir: resolve6(options.courseDir),
731
863
  target: options.target,
732
864
  output: options.output,
733
865
  dir: options.dir,
734
866
  outputBaseDir: options.outputBaseDir,
735
867
  assessments: options.assessments
736
- });
868
+ };
869
+ return buildCourse(buildOptions);
737
870
  }
738
871
  async function packageLessonkitCourse(options) {
739
872
  const { target, output, dir, outputBaseDir, ...writeOpts } = options;
@@ -753,7 +886,7 @@ async function packageLessonkitCourse(options) {
753
886
  };
754
887
  }
755
888
  const outDir = inputValidation.outDir;
756
- const descriptorValidation = validateDescriptor(writeOpts.descriptor);
889
+ const descriptorValidation = validateDescriptorForTarget(writeOpts.descriptor, target);
757
890
  if (!descriptorValidation.ok) {
758
891
  return {
759
892
  ok: false,
@@ -766,22 +899,6 @@ async function packageLessonkitCourse(options) {
766
899
  };
767
900
  }
768
901
  const descriptor = descriptorValidation.descriptor;
769
- if (target === "xapi" || target === "cmi5") {
770
- const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
771
- if (!activityIri) {
772
- return {
773
- ok: false,
774
- courseDir: outDir,
775
- target,
776
- issues: [
777
- {
778
- path: "course.tracking.xapi.activityIri",
779
- message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
780
- }
781
- ]
782
- };
783
- }
784
- }
785
902
  const staged = await buildStagingPackage({
786
903
  ...writeOpts,
787
904
  descriptor,
@@ -875,14 +992,19 @@ function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
875
992
  }
876
993
  const config = raw;
877
994
  const issues = [];
878
- if (config.schemaVersion !== 1) {
995
+ let schemaVersion = config.schemaVersion;
996
+ if (schemaVersion === "1") {
997
+ schemaVersion = 1;
998
+ }
999
+ if (schemaVersion !== 1) {
879
1000
  issues.push({
880
1001
  path: "schemaVersion",
881
1002
  message: `must be 1 (got ${String(config.schemaVersion)})`
882
1003
  });
883
1004
  }
884
- const name = config.name;
885
- if (typeof name !== "string" || !name.trim()) {
1005
+ const nameRaw = config.name;
1006
+ const name = typeof nameRaw === "string" ? nameRaw.trim() : "";
1007
+ if (!name) {
886
1008
  issues.push({ path: "name", message: "must be a non-empty string" });
887
1009
  }
888
1010
  const courseRaw = config.course;
@@ -1000,6 +1122,7 @@ export {
1000
1122
  telemetryEventToLessonkit,
1001
1123
  themeToLxpackRuntime,
1002
1124
  validateDescriptor,
1125
+ validateDescriptorForTarget,
1003
1126
  validateLessonkitProject,
1004
1127
  validatePackageInputs,
1005
1128
  validateProjectPaths,
@@ -0,0 +1,100 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://lessonkit.dev/schemas/lessonkit-manifest.v1.json",
4
+ "title": "LessonkitManifestV1",
5
+ "description": "Root lessonkit.json project manifest (schemaVersion 1)",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schemaVersion", "name", "course", "paths"],
9
+ "properties": {
10
+ "schemaVersion": { "const": 1 },
11
+ "name": { "type": "string", "minLength": 1 },
12
+ "course": { "$ref": "#/$defs/LessonkitCourseDescriptor" },
13
+ "paths": {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "required": ["spaDistDir", "lxpackOutDir", "outputBaseDir"],
17
+ "properties": {
18
+ "spaDistDir": { "type": "string", "minLength": 1 },
19
+ "lxpackOutDir": { "type": "string", "minLength": 1 },
20
+ "outputBaseDir": { "type": "string", "minLength": 1 }
21
+ }
22
+ }
23
+ },
24
+ "$defs": {
25
+ "LessonkitId": {
26
+ "type": "string",
27
+ "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
28
+ "maxLength": 64
29
+ },
30
+ "LessonkitCourseDescriptor": {
31
+ "type": "object",
32
+ "required": ["courseId", "title", "layout", "lessons"],
33
+ "properties": {
34
+ "courseId": { "$ref": "#/$defs/LessonkitId" },
35
+ "title": { "type": "string", "minLength": 1 },
36
+ "version": { "type": "string" },
37
+ "layout": { "enum": ["single-spa", "per-lesson-spa"] },
38
+ "lessons": {
39
+ "type": "array",
40
+ "minItems": 1,
41
+ "items": { "$ref": "#/$defs/LessonDescriptor" }
42
+ },
43
+ "assessments": {
44
+ "type": "array",
45
+ "items": { "$ref": "#/$defs/AssessmentDescriptor" }
46
+ },
47
+ "theme": {
48
+ "type": "object",
49
+ "properties": {
50
+ "preset": { "enum": ["default", "light", "dark", "brand"] },
51
+ "theme": { "type": "object" }
52
+ }
53
+ },
54
+ "tracking": {
55
+ "type": "object",
56
+ "properties": {
57
+ "completion": {
58
+ "type": "object",
59
+ "properties": {
60
+ "threshold": { "type": "number", "minimum": 0, "maximum": 1 }
61
+ }
62
+ },
63
+ "xapi": {
64
+ "type": "object",
65
+ "properties": {
66
+ "activityIri": { "type": "string", "minLength": 1 }
67
+ }
68
+ }
69
+ }
70
+ },
71
+ "spaDistDir": { "type": "string", "minLength": 1 },
72
+ "spaLessonId": { "$ref": "#/$defs/LessonkitId" }
73
+ }
74
+ },
75
+ "LessonDescriptor": {
76
+ "type": "object",
77
+ "required": ["id", "title"],
78
+ "properties": {
79
+ "id": { "$ref": "#/$defs/LessonkitId" },
80
+ "title": { "type": "string", "minLength": 1 },
81
+ "spaPath": { "type": "string", "minLength": 1 }
82
+ }
83
+ },
84
+ "AssessmentDescriptor": {
85
+ "type": "object",
86
+ "required": ["checkId", "question", "choices", "answer"],
87
+ "properties": {
88
+ "checkId": { "$ref": "#/$defs/LessonkitId" },
89
+ "question": { "type": "string", "minLength": 1 },
90
+ "choices": {
91
+ "type": "array",
92
+ "minItems": 1,
93
+ "items": { "type": "string", "minLength": 1 }
94
+ },
95
+ "answer": { "type": "string", "minLength": 1 },
96
+ "passingScore": { "type": "number", "exclusiveMinimum": 0 }
97
+ }
98
+ }
99
+ }
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/lxpack",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "private": false,
5
5
  "description": "LXPack export adapter for LessonKit courses (SCORM, standalone, xAPI, cmi5).",
6
6
  "license": "Apache-2.0",
@@ -38,10 +38,12 @@
38
38
  "types": "./dist/bridge.d.ts",
39
39
  "import": "./dist/bridge.js",
40
40
  "require": "./dist/bridge.cjs"
41
- }
41
+ },
42
+ "./lessonkit-manifest.v1.json": "./lessonkit-manifest.v1.json"
42
43
  },
43
44
  "files": [
44
- "dist"
45
+ "dist",
46
+ "lessonkit-manifest.v1.json"
45
47
  ],
46
48
  "scripts": {
47
49
  "build": "tsup src/index.ts src/bridge.ts --format esm,cjs --dts",
@@ -53,8 +55,8 @@
53
55
  "lint": "echo \"(no lint configured yet)\""
54
56
  },
55
57
  "dependencies": {
56
- "@lessonkit/core": "1.0.0",
57
- "@lessonkit/themes": "1.0.0",
58
+ "@lessonkit/core": "1.0.1",
59
+ "@lessonkit/themes": "1.0.1",
58
60
  "@lxpack/api": "^0.6.2",
59
61
  "@lxpack/spa-bridge": "^0.6.2",
60
62
  "@lxpack/tracking-schema": "^0.6.2",
@@ -1,32 +0,0 @@
1
- // src/telemetry.ts
2
- import { LESSONKIT_TELEMETRY_EVENTS } from "@lxpack/tracking-schema";
3
- var SUPPORTED = new Set(LESSONKIT_TELEMETRY_EVENTS);
4
- function telemetryEventToLessonkit(event) {
5
- if (!SUPPORTED.has(event.name)) {
6
- return null;
7
- }
8
- const name = event.name;
9
- const mapped = {
10
- name,
11
- lessonId: event.lessonId
12
- };
13
- if (name === "quiz_completed" || name === "quiz_answered") {
14
- const data = event.data;
15
- mapped.assessmentId = data?.checkId;
16
- if (data && "score" in data) {
17
- mapped.score = data.score;
18
- mapped.maxScore = data.maxScore;
19
- mapped.passingScore = data.passingScore;
20
- }
21
- if (data) {
22
- mapped.data = data;
23
- }
24
- } else if (name === "interaction" && event.data) {
25
- mapped.data = event.data;
26
- }
27
- return mapped;
28
- }
29
-
30
- export {
31
- telemetryEventToLessonkit
32
- };