@lessonkit/lxpack 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,9 +4,15 @@
4
4
  [![Documentation](https://readthedocs.org/projects/lessonkit/badge/?version=latest)](https://lessonkit.readthedocs.io/en/latest/reference/packaging.html)
5
5
  [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
6
6
 
7
- Package Vite SPAs for LMS delivery — SCORM 1.2/2004, standalone, xAPI, and cmi5. `@lessonkit/lxpack` bundles [`@lxpack/*`](https://www.npmjs.com/org/lxpack) as direct dependencies (no separate `@lxpack/api` install).
7
+ Package Vite SPAs for LMS delivery — SCORM 1.2/2004, standalone, xAPI, and cmi5. Bundles [`@lxpack/*`](https://www.npmjs.com/org/lxpack) as direct dependencies.
8
8
 
9
- Requires Node.js **18+**.
9
+ ## When to install
10
+
11
+ - Custom packaging pipelines without the CLI
12
+ - Validating `lessonkit.json` / course descriptors in CI
13
+ - Programmatic LMS export from your own build tools
14
+
15
+ Most authors use `lessonkit package` (CLI) which calls this package internally.
10
16
 
11
17
  ## Install
12
18
 
@@ -14,13 +20,18 @@ Requires Node.js **18+**.
14
20
  npm install @lessonkit/lxpack
15
21
  ```
16
22
 
23
+ Requires Node.js **18+** minimum; **20.19+** recommended for CLI scaffold workflows.
24
+
17
25
  ## Usage
18
26
 
19
27
  ```typescript
20
- import { packageLessonkitCourse } from "@lessonkit/lxpack";
28
+ import { packageLessonkitCourse, parseLessonkitManifest } from "@lessonkit/lxpack";
29
+
30
+ const manifest = parseLessonkitManifest(await readFile("lessonkit.json", "utf8"));
31
+ if (!manifest.ok) throw manifest.error;
21
32
 
22
33
  const result = await packageLessonkitCourse({
23
- descriptor: courseDescriptor,
34
+ descriptor: manifest.value,
24
35
  outDir: ".lxpack/course",
25
36
  spaDistDir: "dist",
26
37
  target: "scorm12",
@@ -30,19 +41,50 @@ const result = await packageLessonkitCourse({
30
41
  if (!result.ok) throw new Error("packaging failed");
31
42
  ```
32
43
 
33
- Prefer the CLI: `lessonkit package --target scorm12` reads `lessonkit.json` and runs the same pipeline.
44
+ Prefer the CLI: `lessonkit package --target scorm12` reads `lessonkit.json` and runs the same staged pipeline.
45
+
46
+ ## Layouts
47
+
48
+ | Layout | Use case |
49
+ | --- | --- |
50
+ | `single-spa` | One Vite SPA for the whole course (CLI default) |
51
+ | `per-lesson-spa` | One dist per lesson (advanced; see packaging reference) |
52
+
53
+ ## Portable interchange (1.6.0)
54
+
55
+ Export a `.lkcourse` archive for team handoff (not LMS upload):
56
+
57
+ ```typescript
58
+ import { exportLkcourse, validateLkcourse, importLkcourse } from "@lessonkit/lxpack";
59
+
60
+ await exportLkcourse({ projectRoot, manifest, includeBlockTree: true });
61
+ validateLkcourse("course.lkcourse");
62
+ await importLkcourse({ archivePath: "course.lkcourse", targetDir: "./restored" });
63
+ ```
64
+
65
+ Schemas: `@lessonkit/lxpack/lkcourse-format.v1.json`, `@lessonkit/lxpack/block-tree.v1.json`. See [Portable interchange](https://lessonkit.readthedocs.io/en/latest/reference/interchange.html).
34
66
 
35
67
  ## Browser bridge
36
68
 
37
- When embedded in an LXPack iframe, `@lessonkit/react` forwards completion events to `window.parent.lxpackBridge.v1`. Direct API:
69
+ When embedded in an LXPack iframe, `@lessonkit/react` forwards completion events to `window.parent.lxpackBridge.v1`:
38
70
 
39
71
  ```typescript
40
72
  import { forwardTelemetryToBridge } from "@lessonkit/lxpack/bridge";
41
73
  ```
42
74
 
75
+ Production builds require `allowedParentOrigins` when `bridge: "auto"`.
76
+
77
+ ## Common issues
78
+
79
+ | Symptom | Fix |
80
+ | --- | --- |
81
+ | React/manifest ID mismatch | Run strict parity validation; align IDs in `App.tsx` and `lessonkit.json` |
82
+ | xAPI/cmi5 validation failure | Set HTTPS `activityIri` in manifest |
83
+ | Empty `dist/` | Run `lessonkit build` before `package` (or omit `--no-build`) |
84
+
43
85
  ## Docs
44
86
 
45
- [Packaging reference](https://lessonkit.readthedocs.io/en/latest/reference/packaging.html) · [LXPack bridge](https://lessonkit.readthedocs.io/en/latest/reference/lxpack-bridge.html) · [Golden example](https://github.com/eddiethedean/lessonkit/tree/main/examples/lxpack-golden)
87
+ [Packaging reference](https://lessonkit.readthedocs.io/en/latest/reference/packaging.html) · [LXPack bridge](https://lessonkit.readthedocs.io/en/latest/reference/lxpack-bridge.html) · [Manifest](https://lessonkit.readthedocs.io/en/latest/reference/manifest.html) · [Golden example](https://github.com/eddiethedean/lessonkit/tree/main/examples/lxpack-golden) · [TypeDoc API index](https://lessonkit.readthedocs.io/en/latest/reference/api.html)
46
88
 
47
89
  ## License
48
90
 
@@ -0,0 +1,40 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://lessonkit.dev/schemas/block-tree.v1.json",
4
+ "title": "BlockTreeV1",
5
+ "description": "Best-effort static JSX block inventory for a LessonKit course",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schemaVersion", "blocks", "sources"],
9
+ "properties": {
10
+ "schemaVersion": { "const": 1 },
11
+ "sources": {
12
+ "type": "array",
13
+ "items": { "type": "string", "minLength": 1 }
14
+ },
15
+ "blocks": {
16
+ "type": "array",
17
+ "items": { "$ref": "#/$defs/BlockTreeNode" }
18
+ }
19
+ },
20
+ "$defs": {
21
+ "BlockTreeNode": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "required": ["type"],
25
+ "properties": {
26
+ "type": { "type": "string", "minLength": 1 },
27
+ "rawTag": { "type": "string" },
28
+ "courseId": { "type": "string" },
29
+ "lessonId": { "type": "string" },
30
+ "checkId": { "type": "string" },
31
+ "blockId": { "type": "string" },
32
+ "nodeId": { "type": "string" },
33
+ "children": {
34
+ "type": "array",
35
+ "items": { "$ref": "#/$defs/BlockTreeNode" }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
package/dist/bridge.cjs CHANGED
@@ -20,23 +20,27 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/bridge.ts
21
21
  var bridge_exports = {};
22
22
  __export(bridge_exports, {
23
+ BRANCH_TELEMETRY_EVENTS: () => BRANCH_TELEMETRY_EVENTS,
23
24
  DEFAULT_BRIDGE_PASSING_SCORE: () => import_spa_bridge2.DEFAULT_BRIDGE_PASSING_SCORE,
24
25
  LESSONKIT_TELEMETRY_EVENTS: () => import_tracking_schema2.LESSONKIT_TELEMETRY_EVENTS,
25
26
  LXPACK_BRIDGE_VERSIONS: () => import_spa_bridge2.LXPACK_BRIDGE_VERSIONS,
27
+ branchTelemetryToBridgeTrackEvent: () => branchTelemetryToBridgeTrackEvent,
26
28
  createLxpackBridge: () => createLxpackBridge,
27
29
  createLxpackBridgeHost: () => import_spa_bridge2.createLxpackBridgeHost,
28
30
  dispatchBridgeAction: () => dispatchBridgeAction,
29
31
  forwardTelemetryToBridge: () => forwardTelemetryToBridge,
30
- getLxpackBridge: () => import_spa_bridge2.getLxpackBridge,
32
+ getLxpackBridge: () => getLxpackBridge,
33
+ isParentOriginAllowed: () => isParentOriginAllowed,
31
34
  mapLessonkitTelemetryToBridgeAction: () => import_tracking_schema2.mapLessonkitTelemetryToBridgeAction,
32
35
  mapLessonkitTelemetryToLxpack: () => import_tracking_schema2.mapLessonkitTelemetryToLxpack,
33
36
  normalizeAssessmentPassingScore: () => normalizeAssessmentPassingScore,
34
37
  normalizeAssessmentScore: () => normalizeAssessmentScore,
35
- normalizePassingThreshold: () => import_spa_bridge2.normalizePassingThreshold,
36
- normalizeScore: () => import_spa_bridge2.normalizeScore,
38
+ normalizePassingThreshold: () => normalizePassingThreshold,
39
+ normalizeScore: () => normalizeScore,
37
40
  notifyLxpackAssessment: () => notifyLxpackAssessment,
38
41
  notifyLxpackCourseComplete: () => notifyLxpackCourseComplete,
39
42
  notifyLxpackLessonComplete: () => notifyLxpackLessonComplete,
43
+ resolveParentOrigin: () => resolveParentOrigin,
40
44
  supportedBridgeVersions: () => import_spa_bridge2.supportedBridgeVersions,
41
45
  telemetryEventToLessonkit: () => telemetryEventToLessonkit
42
46
  });
@@ -48,56 +52,163 @@ var import_tracking_schema3 = require("@lxpack/tracking-schema");
48
52
 
49
53
  // src/telemetry.ts
50
54
  var import_tracking_schema = require("@lxpack/tracking-schema");
51
- var SUPPORTED = new Set(import_tracking_schema.LESSONKIT_TELEMETRY_EVENTS);
55
+ var BRANCH_TELEMETRY_EVENTS = ["branch_node_viewed", "branch_selected"];
56
+ var ASSESSMENT_TELEMETRY_EVENTS = ["assessment_answered"];
57
+ var SUPPORTED = /* @__PURE__ */ new Set([
58
+ ...import_tracking_schema.LESSONKIT_TELEMETRY_EVENTS,
59
+ ...BRANCH_TELEMETRY_EVENTS,
60
+ ...ASSESSMENT_TELEMETRY_EVENTS
61
+ ]);
52
62
  function isQuizAnsweredData(data) {
53
- return typeof data === "object" && data !== null && typeof data.checkId === "string";
63
+ return typeof data === "object" && data !== null && typeof data.checkId === "string" && data.checkId.length > 0;
54
64
  }
55
65
  function isQuizCompletedData(data) {
56
- return typeof data === "object" && data !== null && typeof data.checkId === "string";
66
+ return typeof data === "object" && data !== null && typeof data.checkId === "string" && data.checkId.length > 0;
67
+ }
68
+ function isAssessmentAnsweredData(data) {
69
+ return typeof data === "object" && data !== null && typeof data.checkId === "string" && data.checkId.length > 0;
57
70
  }
58
71
  function isInteractionData(data) {
59
72
  return typeof data === "object" && data !== null;
60
73
  }
74
+ function isBranchNodeViewedData(data) {
75
+ return typeof data === "object" && data !== null && typeof data.blockId === "string" && typeof data.nodeId === "string";
76
+ }
77
+ function isBranchSelectedData(data) {
78
+ return typeof data === "object" && data !== null && typeof data.blockId === "string" && typeof data.fromNodeId === "string" && typeof data.toNodeId === "string";
79
+ }
61
80
  function telemetryEventToLessonkit(event) {
62
81
  if (!SUPPORTED.has(event.name)) {
63
82
  return null;
64
83
  }
65
- const name = event.name;
66
84
  const mapped = {
67
- name,
85
+ name: event.name,
68
86
  lessonId: event.lessonId
69
87
  };
70
- if (name === "quiz_completed" || name === "quiz_answered") {
88
+ if (event.name === "quiz_completed" || event.name === "quiz_answered" || event.name === "assessment_answered") {
71
89
  const data = event.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
- }
79
- mapped.data = data;
90
+ if (!isQuizAnsweredData(data) && !isQuizCompletedData(data) && !isAssessmentAnsweredData(data)) {
91
+ return null;
80
92
  }
81
- } else if (name === "interaction" && event.data && isInteractionData(event.data)) {
93
+ mapped.assessmentId = data.checkId;
94
+ if ("score" in data) {
95
+ mapped.score = data.score;
96
+ mapped.maxScore = data.maxScore;
97
+ mapped.passingScore = data.passingScore;
98
+ }
99
+ mapped.data = data;
100
+ } else if (mapped.name === "interaction" && event.data && isInteractionData(event.data)) {
101
+ mapped.data = event.data;
102
+ } else if (event.name === "branch_node_viewed" && isBranchNodeViewedData(event.data)) {
103
+ mapped.data = event.data;
104
+ } else if (event.name === "branch_selected" && isBranchSelectedData(event.data)) {
82
105
  mapped.data = event.data;
83
106
  }
84
107
  return mapped;
85
108
  }
109
+ function answeredTelemetryToBridgeTrackEvent(event) {
110
+ if (event.name !== "quiz_answered" && event.name !== "assessment_answered") {
111
+ return null;
112
+ }
113
+ const lessonkitEvent = telemetryEventToLessonkit(event);
114
+ if (!lessonkitEvent?.assessmentId) return null;
115
+ return (0, import_tracking_schema.mapLessonkitTelemetryToLxpack)({
116
+ ...lessonkitEvent,
117
+ name: "quiz_answered"
118
+ });
119
+ }
120
+ function branchTelemetryToBridgeTrackEvent(event) {
121
+ if (event.name === "branch_node_viewed" && isBranchNodeViewedData(event.data)) {
122
+ return {
123
+ type: "interaction",
124
+ id: "branch_node_viewed",
125
+ data: { ...event.data, lessonkitEvent: event.name }
126
+ };
127
+ }
128
+ if (event.name === "branch_selected" && isBranchSelectedData(event.data)) {
129
+ return {
130
+ type: "interaction",
131
+ id: "branch_selected",
132
+ data: { ...event.data, lessonkitEvent: event.name }
133
+ };
134
+ }
135
+ return null;
136
+ }
86
137
 
87
138
  // src/bridge.ts
139
+ var import_meta = {};
140
+ var DEFAULT_BRIDGE_PASSING_SCORE2 = 1;
141
+ function clamp01(value) {
142
+ return Math.min(1, Math.max(0, value));
143
+ }
144
+ function normalizeScore(raw) {
145
+ const { score, maxScore } = raw;
146
+ if (typeof score !== "number" || !Number.isFinite(score)) return null;
147
+ if (typeof maxScore === "number" && maxScore > 0) {
148
+ return clamp01(score / maxScore);
149
+ }
150
+ if (score > 1 && score <= 100) {
151
+ return clamp01(score / 100);
152
+ }
153
+ return clamp01(score);
154
+ }
155
+ function normalizePassingThreshold(raw) {
156
+ const { passingScore, maxScore } = raw ?? {};
157
+ if (typeof passingScore !== "number" || !Number.isFinite(passingScore)) {
158
+ return DEFAULT_BRIDGE_PASSING_SCORE2;
159
+ }
160
+ if (typeof maxScore === "number" && maxScore > 1) {
161
+ return clamp01(passingScore / maxScore);
162
+ }
163
+ if (typeof maxScore === "number" && maxScore <= 1) {
164
+ return clamp01(passingScore);
165
+ }
166
+ if (passingScore > 1 && passingScore <= 100) {
167
+ return clamp01(passingScore / 100);
168
+ }
169
+ return clamp01(passingScore);
170
+ }
88
171
  function normalizeAssessmentScore(opts) {
89
172
  if (typeof opts.score !== "number" || !Number.isFinite(opts.score)) {
90
173
  return null;
91
174
  }
92
- return (0, import_spa_bridge.normalizeScore)({ score: opts.score, maxScore: opts.maxScore });
175
+ return normalizeScore({ score: opts.score, maxScore: opts.maxScore });
93
176
  }
94
177
  function normalizeAssessmentPassingScore(opts) {
95
- return (0, import_spa_bridge.normalizePassingThreshold)({
178
+ return normalizePassingThreshold({
96
179
  passingScore: opts?.passingScore,
97
180
  maxScore: opts?.maxScore
98
181
  });
99
182
  }
100
- function getBridge(parentWindow) {
183
+ function resolveParentOrigin(parentWindow) {
184
+ if (typeof window === "undefined") return null;
185
+ const parent = parentWindow ?? window.parent;
186
+ if (!parent || parent === window) return null;
187
+ try {
188
+ return parent.location.origin;
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
193
+ function isProductionRuntime() {
194
+ try {
195
+ if (import_meta.env?.PROD === true) return true;
196
+ } catch {
197
+ }
198
+ const g = globalThis;
199
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
200
+ }
201
+ function isParentOriginAllowed(allowedParentOrigins, parentWindow, mode) {
202
+ if (mode === "off") return false;
203
+ if (isProductionRuntime() && !allowedParentOrigins?.length) return false;
204
+ if (!allowedParentOrigins?.length) return true;
205
+ const origin = resolveParentOrigin(parentWindow);
206
+ if (!origin) return false;
207
+ return allowedParentOrigins.includes(origin);
208
+ }
209
+ function getBridge(parentWindow, opts) {
210
+ const mode = opts?.mode ?? "auto";
211
+ if (!isParentOriginAllowed(opts?.allowedParentOrigins, parentWindow, mode)) return null;
101
212
  const fromSdk = (0, import_spa_bridge.getLxpackBridge)(parentWindow);
102
213
  if (fromSdk) return fromSdk;
103
214
  if (typeof window === "undefined") return null;
@@ -105,21 +216,33 @@ function getBridge(parentWindow) {
105
216
  if (!parent || parent === window) return null;
106
217
  return parent.lxpackBridge?.v1 ?? parent.lxpack ?? null;
107
218
  }
219
+ function getLxpackBridge(parentWindow, opts) {
220
+ return getBridge(parentWindow, opts);
221
+ }
108
222
  function isDevEnvironment() {
223
+ try {
224
+ if (import_meta.env?.DEV === true) return true;
225
+ if (import_meta.env?.PROD === true) return false;
226
+ } catch {
227
+ }
109
228
  const g = globalThis;
110
229
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
111
230
  }
112
- function dispatchBridgeAction(bridge, action) {
231
+ function handleBridgeError(err, onBridgeError) {
232
+ onBridgeError?.(err);
233
+ if (isDevEnvironment()) {
234
+ console.warn(
235
+ "[lessonkit/lxpack] lxpack bridge action failed:",
236
+ err instanceof Error ? err.message : err
237
+ );
238
+ }
239
+ }
240
+ function dispatchBridgeAction(bridge, action, opts) {
113
241
  if (!action) return;
114
242
  try {
115
243
  dispatchBridgeActionInner(bridge, action);
116
244
  } catch (err) {
117
- if (isDevEnvironment()) {
118
- console.warn(
119
- "[lessonkit/lxpack] lxpack bridge action failed:",
120
- err instanceof Error ? err.message : err
121
- );
122
- }
245
+ handleBridgeError(err, opts?.onBridgeError);
123
246
  }
124
247
  }
125
248
  function dispatchBridgeActionInner(bridge, action) {
@@ -132,7 +255,7 @@ function dispatchBridgeActionInner(bridge, action) {
132
255
  bridge.completeCourse?.();
133
256
  return;
134
257
  case "submitAssessment": {
135
- const scaled = (0, import_spa_bridge.normalizeScore)({
258
+ const scaled = normalizeScore({
136
259
  score: action.score,
137
260
  maxScore: action.maxScore
138
261
  });
@@ -140,7 +263,7 @@ function dispatchBridgeActionInner(bridge, action) {
140
263
  bridge.submitAssessment?.({
141
264
  id: action.id,
142
265
  score: scaled,
143
- passingScore: (0, import_spa_bridge.normalizePassingThreshold)({
266
+ passingScore: normalizePassingThreshold({
144
267
  passingScore: action.passingScore,
145
268
  maxScore: action.maxScore
146
269
  }),
@@ -155,13 +278,16 @@ function dispatchBridgeActionInner(bridge, action) {
155
278
  return;
156
279
  }
157
280
  }
158
- function forwardAssessmentCompletedToBridge(bridge, event) {
281
+ function forwardAssessmentCompletedToBridge(bridge, event, onBridgeMiss) {
159
282
  const data = event.data;
160
283
  const scaled = normalizeAssessmentScore({
161
284
  score: data.score,
162
285
  maxScore: data.maxScore
163
286
  });
164
- if (scaled === null) return;
287
+ if (scaled === null) {
288
+ onBridgeMiss?.(event);
289
+ return;
290
+ }
165
291
  bridge.submitAssessment?.({
166
292
  id: data.checkId,
167
293
  score: scaled,
@@ -172,50 +298,97 @@ function forwardAssessmentCompletedToBridge(bridge, event) {
172
298
  maxScore: data.maxScore
173
299
  });
174
300
  }
175
- function forwardTelemetryToBridge(event, mode = "auto", parentWindow) {
301
+ function forwardTelemetryToBridge(event, mode = "auto", parentWindow, opts) {
176
302
  if (mode === "off") return;
177
- const bridge = getBridge(parentWindow);
303
+ const bridge = getBridge(parentWindow, {
304
+ allowedParentOrigins: opts?.allowedParentOrigins,
305
+ mode
306
+ });
178
307
  if (!bridge) return;
179
- if (event.name === "assessment_completed") {
180
- forwardAssessmentCompletedToBridge(bridge, event);
181
- return;
308
+ try {
309
+ if (event.name === "assessment_completed") {
310
+ forwardAssessmentCompletedToBridge(bridge, event, opts?.onBridgeMiss);
311
+ return;
312
+ }
313
+ const answeredTrack = answeredTelemetryToBridgeTrackEvent(event);
314
+ if (answeredTrack) {
315
+ bridge.track?.(answeredTrack);
316
+ return;
317
+ }
318
+ const branchTrack = branchTelemetryToBridgeTrackEvent(event);
319
+ if (branchTrack) {
320
+ bridge.track?.(branchTrack);
321
+ return;
322
+ }
323
+ const lessonkitEvent = telemetryEventToLessonkit(event);
324
+ if (!lessonkitEvent) return;
325
+ const action = (0, import_tracking_schema3.mapLessonkitTelemetryToBridgeAction)(lessonkitEvent);
326
+ dispatchBridgeActionInner(bridge, action);
327
+ } catch (err) {
328
+ handleBridgeError(err, opts?.onBridgeError);
182
329
  }
183
- const lessonkitEvent = telemetryEventToLessonkit(event);
184
- if (!lessonkitEvent) return;
185
- const action = (0, import_tracking_schema3.mapLessonkitTelemetryToBridgeAction)(lessonkitEvent);
186
- dispatchBridgeAction(bridge, action);
187
330
  }
188
- function createLxpackBridge() {
189
- return getBridge();
331
+ function createLxpackBridge(opts) {
332
+ return getBridge(void 0, opts);
190
333
  }
191
- function notifyLxpackLessonComplete(lessonId) {
192
- const bridge = getBridge();
334
+ function notifyLxpackLessonComplete(lessonId, opts) {
335
+ const bridge = getBridge(void 0, opts);
193
336
  if (!bridge?.completeLesson) return false;
194
- bridge.completeLesson(lessonId);
195
- return true;
337
+ try {
338
+ bridge.completeLesson(lessonId);
339
+ return true;
340
+ } catch (err) {
341
+ handleBridgeError(err, opts?.onBridgeError);
342
+ return false;
343
+ }
196
344
  }
197
- function notifyLxpackCourseComplete() {
198
- const bridge = getBridge();
345
+ function notifyLxpackCourseComplete(opts) {
346
+ const bridge = getBridge(void 0, opts);
199
347
  if (!bridge?.completeCourse) return false;
200
- bridge.completeCourse();
201
- return true;
348
+ try {
349
+ bridge.completeCourse();
350
+ return true;
351
+ } catch (err) {
352
+ handleBridgeError(err, opts?.onBridgeError);
353
+ return false;
354
+ }
202
355
  }
203
- function notifyLxpackAssessment(payload) {
204
- const bridge = getBridge();
356
+ function notifyLxpackAssessment(payload, opts) {
357
+ const bridge = getBridge(void 0, opts);
205
358
  if (!bridge?.submitAssessment) return false;
206
- bridge.submitAssessment(payload);
207
- return true;
359
+ const scaled = normalizeAssessmentScore({
360
+ score: payload.score,
361
+ maxScore: payload.maxScore
362
+ });
363
+ if (scaled === null) return false;
364
+ try {
365
+ bridge.submitAssessment({
366
+ ...payload,
367
+ score: scaled,
368
+ passingScore: normalizeAssessmentPassingScore({
369
+ passingScore: payload.passingScore,
370
+ maxScore: payload.maxScore
371
+ })
372
+ });
373
+ return true;
374
+ } catch (err) {
375
+ handleBridgeError(err, opts?.onBridgeError);
376
+ return false;
377
+ }
208
378
  }
209
379
  // Annotate the CommonJS export names for ESM import in node:
210
380
  0 && (module.exports = {
381
+ BRANCH_TELEMETRY_EVENTS,
211
382
  DEFAULT_BRIDGE_PASSING_SCORE,
212
383
  LESSONKIT_TELEMETRY_EVENTS,
213
384
  LXPACK_BRIDGE_VERSIONS,
385
+ branchTelemetryToBridgeTrackEvent,
214
386
  createLxpackBridge,
215
387
  createLxpackBridgeHost,
216
388
  dispatchBridgeAction,
217
389
  forwardTelemetryToBridge,
218
390
  getLxpackBridge,
391
+ isParentOriginAllowed,
219
392
  mapLessonkitTelemetryToBridgeAction,
220
393
  mapLessonkitTelemetryToLxpack,
221
394
  normalizeAssessmentPassingScore,
@@ -225,6 +398,7 @@ function notifyLxpackAssessment(payload) {
225
398
  notifyLxpackAssessment,
226
399
  notifyLxpackCourseComplete,
227
400
  notifyLxpackLessonComplete,
401
+ resolveParentOrigin,
228
402
  supportedBridgeVersions,
229
403
  telemetryEventToLessonkit
230
404
  });
package/dist/bridge.d.cts CHANGED
@@ -1,9 +1,23 @@
1
1
  import { LmsBridgeMode, TelemetryEvent, CheckId, LessonId } from '@lessonkit/core';
2
2
  import { LxpackBridgeV1, LxpackBridgeSubmitAssessmentPayload } from '@lxpack/spa-bridge';
3
- export { DEFAULT_BRIDGE_PASSING_SCORE, LXPACK_BRIDGE_VERSIONS, LxpackBridgeSubmitAssessmentPayload, LxpackBridgeV1, createLxpackBridgeHost, getLxpackBridge, normalizePassingThreshold, normalizeScore, supportedBridgeVersions } from '@lxpack/spa-bridge';
3
+ export { DEFAULT_BRIDGE_PASSING_SCORE, LXPACK_BRIDGE_VERSIONS, LxpackBridgeSubmitAssessmentPayload, LxpackBridgeV1, createLxpackBridgeHost, supportedBridgeVersions } from '@lxpack/spa-bridge';
4
4
  import { mapLessonkitTelemetryToBridgeAction } from '@lxpack/tracking-schema';
5
5
  export { LESSONKIT_TELEMETRY_EVENTS, LessonkitBridgeAction, LessonkitTelemetryEvent, LessonkitTelemetryEventName, TrackingSchemaEvent, mapLessonkitTelemetryToBridgeAction, mapLessonkitTelemetryToLxpack } from '@lxpack/tracking-schema';
6
- export { t as telemetryEventToLessonkit } from './telemetry-gCxlwc7I.cjs';
6
+ export { B as BRANCH_TELEMETRY_EVENTS, b as branchTelemetryToBridgeTrackEvent, t as telemetryEventToLessonkit } from './telemetry-0fIWoomS.cjs';
7
+
8
+ /**
9
+ * Scale a raw quiz score to 0–1 for the LXPack parent bridge.
10
+ * When `maxScore > 1`, always treats `score` as raw points (fixes partial-credit 1/N cases).
11
+ */
12
+ declare function normalizeScore(raw: {
13
+ score?: number;
14
+ maxScore?: number;
15
+ }): number | null;
16
+ /** Scale a raw passing threshold to 0–1 for the LXPack parent bridge. */
17
+ declare function normalizePassingThreshold(raw?: {
18
+ passingScore?: number;
19
+ maxScore?: number;
20
+ }): number;
7
21
 
8
22
  /**
9
23
  * Scale a raw quiz score to 0–1 for the LXPack parent bridge.
@@ -15,26 +29,60 @@ declare function normalizeAssessmentScore(opts: {
15
29
  }): number | null;
16
30
  /**
17
31
  * Scale a raw passing threshold to 0–1 for the LXPack parent bridge.
18
- * Delegates to `@lxpack/spa-bridge` (default 0.7 when omitted).
32
+ * Default 1.0 (100%) when omitted — matches React SPA default.
19
33
  */
20
34
  declare function normalizeAssessmentPassingScore(opts?: {
21
35
  passingScore?: number;
22
36
  maxScore?: number;
23
37
  }): number;
38
+ type BridgeAccessOptions = {
39
+ /** Allowed parent-frame origins (scheme + host + port). When set, bridge calls require a matching origin. */
40
+ allowedParentOrigins?: string[];
41
+ /** LMS bridge mode; `"auto"` in production requires `allowedParentOrigins`. */
42
+ mode?: LxpackBridgeMode;
43
+ onBridgeError?: (err: unknown) => void;
44
+ };
45
+ /** Resolve the parent frame origin when embedded (same-origin parent or document.referrer fallback). */
46
+ declare function resolveParentOrigin(parentWindow?: Window): string | null;
47
+ /** Returns true when no allowlist is configured or the resolved parent origin is listed. */
48
+ declare function isParentOriginAllowed(allowedParentOrigins: string[] | undefined, parentWindow?: Window, mode?: LxpackBridgeMode): boolean;
49
+ /** Resolve the LXPack parent bridge when the parent origin passes validation. */
50
+ declare function getLxpackBridge(parentWindow?: Window, opts?: BridgeAccessOptions): LxpackBridgeV1 | null;
24
51
  /** @deprecated Use `LmsBridgeMode` from `@lessonkit/core`. */
25
52
  type LxpackBridgeMode = LmsBridgeMode;
26
53
  /** Apply a mapped bridge action to an LXPack bridge instance. */
27
- declare function dispatchBridgeAction(bridge: LxpackBridgeV1, action: ReturnType<typeof mapLessonkitTelemetryToBridgeAction>): void;
28
- declare function forwardTelemetryToBridge(event: TelemetryEvent, mode?: LxpackBridgeMode, parentWindow?: Window): void;
29
- declare function createLxpackBridge(): LxpackBridgeV1 | null;
30
- declare function notifyLxpackLessonComplete(lessonId: LessonId): boolean;
31
- declare function notifyLxpackCourseComplete(): boolean;
54
+ declare function dispatchBridgeAction(bridge: LxpackBridgeV1, action: ReturnType<typeof mapLessonkitTelemetryToBridgeAction>, opts?: {
55
+ onBridgeError?: (err: unknown) => void;
56
+ }): void;
57
+ type ForwardTelemetryToBridgeOptions = {
58
+ onBridgeError?: (err: unknown) => void;
59
+ /** Called when assessment_completed cannot be forwarded (e.g. missing/invalid score). */
60
+ onBridgeMiss?: (event: TelemetryEvent) => void;
61
+ allowedParentOrigins?: string[];
62
+ };
63
+ /**
64
+ * Map a LessonKit telemetry event to LMS bridge actions (`track`, `complete`, assessment score).
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * import { forwardTelemetryToBridge } from "@lessonkit/lxpack/bridge";
69
+ *
70
+ * forwardTelemetryToBridge(event, "auto", window.parent, {
71
+ * allowedParentOrigins: ["https://lms.example.com"],
72
+ * onBridgeMiss: (e) => console.warn("bridge miss", e.name),
73
+ * });
74
+ * ```
75
+ */
76
+ declare function forwardTelemetryToBridge(event: TelemetryEvent, mode?: LxpackBridgeMode, parentWindow?: Window, opts?: ForwardTelemetryToBridgeOptions): void;
77
+ declare function createLxpackBridge(opts?: BridgeAccessOptions): LxpackBridgeV1 | null;
78
+ declare function notifyLxpackLessonComplete(lessonId: LessonId, opts?: BridgeAccessOptions): boolean;
79
+ declare function notifyLxpackCourseComplete(opts?: BridgeAccessOptions): boolean;
32
80
  /**
33
81
  * Submit assessment results to the parent LXPack bridge.
34
- * `score` must already be on a 0–1 scale (use `normalizeAssessmentScore` for raw points).
82
+ * Raw point scores are normalized to 0–1 before submission.
35
83
  */
36
84
  declare function notifyLxpackAssessment(payload: LxpackBridgeSubmitAssessmentPayload & {
37
85
  id: CheckId;
38
- }): boolean;
86
+ }, opts?: BridgeAccessOptions): boolean;
39
87
 
40
- export { type LxpackBridgeMode, createLxpackBridge, dispatchBridgeAction, forwardTelemetryToBridge, normalizeAssessmentPassingScore, normalizeAssessmentScore, notifyLxpackAssessment, notifyLxpackCourseComplete, notifyLxpackLessonComplete };
88
+ export { type BridgeAccessOptions, type ForwardTelemetryToBridgeOptions, type LxpackBridgeMode, createLxpackBridge, dispatchBridgeAction, forwardTelemetryToBridge, getLxpackBridge, isParentOriginAllowed, normalizeAssessmentPassingScore, normalizeAssessmentScore, normalizePassingThreshold, normalizeScore, notifyLxpackAssessment, notifyLxpackCourseComplete, notifyLxpackLessonComplete, resolveParentOrigin };