Skip to main content

Cross-Frame Communication

This guide explains how to use scorm-again with SCORM content running in sandboxed iframes, enabling secure cross-origin communication between your LMS and content frames.

When to Use Cross-Frame Communication

Use this architecture when:

  • Content runs in a sandboxed <iframe> for security isolation
  • Content is served from a different origin than your LMS
  • You need to prevent content from accessing parent window directly
  • Corporate security policies require frame isolation

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│ LMS Frame (Parent) │
│ lms.example.com │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ CrossFrameLMS │ │
│ │ - Holds real Scorm12API or Scorm2004API │ │
│ │ - Listens for postMessage from child │ │
│ │ - Executes API calls and sends responses │ │
│ └─────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ postMessage │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Content Frame (Child iframe) │ │
│ │ content.cdn.com │ │
│ │ ┌───────────────────────────────────────────┐ │ │
│ │ │ CrossFrameAPI │ │ │
│ │ │ - Proxy that mimics SCORM API │ │ │
│ │ │ - Returns cached values synchronously │ │ │
│ │ │ - Sends postMessage to parent async │ │ │
│ │ │ - Updates cache when response arrives │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Components

CrossFrameLMS (Parent Frame)

The CrossFrameLMS class runs in your LMS frame. It:

  1. Wraps your real SCORM API instance
  2. Listens for postMessage requests from child frames
  3. Executes the requested API methods
  4. Sends results back via postMessage

CrossFrameAPI (Child Frame)

The CrossFrameAPI class runs in the content frame. It:

  1. Provides a SCORM-compatible API interface
  2. Returns cached values synchronously (SCORM requires synchronous API)
  3. Sends postMessage to parent for actual operations
  4. Updates its cache when responses arrive

Setup

Step 1: Parent Frame (LMS)

In your LMS launch page that contains the iframe:

<script src="scorm-again.min.js"></script>
<script>
// Create the real API
const api = new Scorm2004API({
lmsCommitUrl: "/api/scorm/commit",
// ... your settings
});

// Initialize with learner data
api.loadFromJSON({
"cmi.learner_id": "student123",
"cmi.learner_name": "John Doe"
});

// Wrap with CrossFrameLMS
// Restrict to specific content origin for security
const crossFrame = new CrossFrameLMS(api, "https://content.cdn.com");
</script>

<iframe
id="content-frame"
src="https://content.cdn.com/course/index.html"
sandbox="allow-scripts allow-same-origin"
></iframe>

Step 2: Child Frame (Content)

In the content's launch page:

<script src="cross-frame-api.min.js"></script>
<script>
// Create the proxy API
// It will automatically attach to window.API (SCORM 1.2)
// or window.API_1484_11 (SCORM 2004)
const api = new CrossFrameAPI("https://lms.example.com", window.parent);

// The content can now use standard SCORM calls
// These return synchronously but update asynchronously
api.Initialize("");
const name = api.GetValue("cmi.learner_name"); // Returns cached value
api.SetValue("cmi.score.raw", "85");
api.Commit("");
api.Terminate("");
</script>

Configuration Options

CrossFrameLMS Constructor

new CrossFrameLMS(api: IBaseAPI, targetOrigin?: string)
ParameterTypeDefaultDescription
apiIBaseAPIrequiredThe real SCORM API instance
targetOriginstring"*"Allowed origin for messages. Use specific origin in production!

CrossFrameAPI Constructor

new CrossFrameAPI(targetOrigin?: string, targetWindow?: Window)
ParameterTypeDefaultDescription
targetOriginstring"*"Origin of the parent LMS frame
targetWindowWindowwindow.parentThe parent window to communicate with

Security Considerations

Origin Validation

Always specify explicit origins in production:

// UNSAFE - accepts messages from any origin
const crossFrame = new CrossFrameLMS(api, "*");

// SAFE - only accepts messages from specific origin
const crossFrame = new CrossFrameLMS(api, "https://content.cdn.com");

Sandbox Attributes

Use appropriate iframe sandbox attributes:

<!-- Minimal permissions for SCORM content -->
<iframe
src="..."
sandbox="allow-scripts allow-same-origin"
></iframe>
AttributePurpose
allow-scriptsRequired for SCORM content to run
allow-same-originRequired for content to access its own origin
allow-formsOnly if content has form submissions

Message Sanitization

The CrossFrameAPI automatically:

  • Removes function parameters (non-cloneable)
  • Validates message structure before processing
  • Logs warnings for dropped parameters

How It Works

Synchronous Facade Pattern

SCORM requires synchronous API calls, but postMessage is asynchronous. CrossFrameAPI solves this with a cache:

  1. SetValue calls: Cache updated immediately, then sent to parent
  2. GetValue calls: Return cached value immediately, then refresh cache from parent
  3. Initialize/Terminate/Commit: Return "true" immediately, sync full state async
// When content calls:
api.SetValue("cmi.score.raw", "85");

// CrossFrameAPI:
// 1. Updates local cache immediately
// 2. Returns "true" synchronously
// 3. Sends postMessage to parent (async)
// 4. Parent executes real SetValue
// 5. Parent sends result back
// 6. CrossFrameAPI updates cache with any server-side changes

Message Protocol

Messages use a simple request/response protocol:

Request (child → parent):

{
messageId: "cfapi-1702745234567-0", // Unique ID for correlation
method: "SetValue", // API method name
params: ["cmi.score.raw", "85"] // Method parameters
}

Response (parent → child):

{
messageId: "cfapi-1702745234567-0", // Matching request ID
result: "true", // Method return value
error?: { message: string, stack?: string } // Only on error
}

Timeout Handling

Requests timeout after 5 seconds:

// CrossFrameAPI handles timeouts internally
// If parent doesn't respond in 5 seconds:
// - Promise rejects with timeout error
// - Error is captured and cached
// - GetLastError() returns appropriate error code

Complete Example

LMS Launch Page (parent.html)

<!DOCTYPE html>
<html>
<head>
<title>LMS - Course Launch</title>
<script src="scorm-again.min.js"></script>
</head>
<body>
<h1>Course: Introduction to SCORM</h1>
<div id="progress">Progress: <span id="status">Not started</span></div>

<iframe
id="content-frame"
src="https://content.cdn.com/course/index.html"
sandbox="allow-scripts allow-same-origin"
style="width: 100%; height: 600px; border: 1px solid #ccc;"
></iframe>

<script>
// Create and configure the real API
const api = new Scorm2004API({
lmsCommitUrl: "/api/scorm/commit",
autocommit: true,
autocommitSeconds: 30
});

// Load learner data from your LMS backend
api.loadFromJSON({
"cmi.learner_id": "student_12345",
"cmi.learner_name": "Jane Smith",
"cmi.entry": "ab-initio",
"cmi.mode": "normal",
"cmi.credit": "credit"
});

// Listen for status changes
api.on("SetValue.cmi.completion_status", function(element, value) {
document.getElementById("status").textContent = value;
});

// Create cross-frame bridge
const crossFrame = new CrossFrameLMS(api, "https://content.cdn.com");

console.log("LMS ready, waiting for content...");
</script>
</body>
</html>

Content Launch Page (content/index.html)

<!DOCTYPE html>
<html>
<head>
<title>SCORM Content</title>
<script src="cross-frame-api.min.js"></script>
</head>
<body>
<h1>Welcome to the Course</h1>
<p>Learner: <span id="learner-name">Loading...</span></p>
<button onclick="markComplete()">Mark Complete</button>

<script>
// Create proxy API pointing to LMS parent
const api = new CrossFrameAPI("https://lms.example.com", window.parent);

// Standard SCORM initialization
api.Initialize("");

// Get learner name (returns cached/empty initially, updates async)
document.getElementById("learner-name").textContent =
api.GetValue("cmi.learner_name") || "Loading...";

// Refresh after async update
setTimeout(function() {
document.getElementById("learner-name").textContent =
api.GetValue("cmi.learner_name");
}, 1000);

function markComplete() {
api.SetValue("cmi.completion_status", "completed");
api.SetValue("cmi.success_status", "passed");
api.SetValue("cmi.score.scaled", "1.0");
api.Commit("");
api.Terminate("");
alert("Course completed!");
}
</script>
</body>
</html>

Troubleshooting

Messages Not Received

Symptom: Content calls API but nothing happens in LMS

Causes:

  1. Origin mismatch - Check that origins match exactly (including protocol and port)
  2. Sandbox blocking - Ensure allow-scripts and allow-same-origin are set
  3. Wrong target window - Verify targetWindow parameter

Debug:

// In content frame, check if messages are being sent
window.addEventListener("message", function(e) {
console.log("Content received:", e.data, "from:", e.origin);
});

Cached Values Stale

Symptom: GetValue returns old/empty values

Cause: The synchronous return uses cached values; async update hasn't completed

Solution: Use a small delay for critical reads, or listen for value changes:

api.Initialize("");
// Wait briefly for initial cache population
setTimeout(function() {
const name = api.GetValue("cmi.learner_name");
console.log("Name:", name);
}, 500);

Timeout Errors

Symptom: Errors about method timeout

Causes:

  1. Parent frame not loaded yet
  2. CrossFrameLMS not initialized
  3. Network issues between frames

Solution: Ensure parent is fully loaded before content makes API calls:

// In content, wait for ready signal or use delay
window.addEventListener("load", function() {
setTimeout(function() {
api.Initialize("");
}, 100);
});

Limitations

  1. Initial GetValue returns empty: First GetValue call returns cached (empty) value until async response arrives
  2. No real-time sync: Cache updates are eventual, not immediate
  3. 5-second timeout: Long operations may timeout
  4. Function parameters dropped: Callbacks cannot be passed through postMessage