UNPKG

scorm-again

Version:

A modern SCORM JavaScript run-time library for SCORM 1.2 and SCORM 2004

185 lines (181 loc) 6.72 kB
this.CrossFrameLMS = (function () { 'use strict'; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const _CrossFrameLMS = class _CrossFrameLMS { /** * Creates a new CrossFrameLMS instance. * @param api - The SCORM API instance to delegate calls to * @param targetOrigin - Origin to accept messages from. Default "*" accepts all origins. * @param options - Configuration options */ constructor(api) { let targetOrigin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "*"; let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; __publicField(this, "_api"); __publicField(this, "_origin"); __publicField(this, "_rateLimit"); __publicField(this, "_requestTimes", []); __publicField(this, "_destroyed", false); __publicField(this, "_boundOnMessage"); this._api = api; this._origin = targetOrigin; this._rateLimit = options.rateLimit ?? 100; if (targetOrigin === "*") { console.warn("CrossFrameLMS: Using wildcard origin ('*') allows any origin to send messages. This is insecure for production use. Specify an explicit origin (e.g., 'https://content.example.com') to restrict message sources."); } this._boundOnMessage = this._onMessage.bind(this); window.addEventListener("message", this._boundOnMessage); } /** * Destroys this instance, removing event listeners and preventing further message processing. * Once destroyed, the instance cannot be reused. */ destroy() { if (this._destroyed) return; this._destroyed = true; window.removeEventListener("message", this._boundOnMessage); this._requestTimes.length = 0; } /** * Checks if the rate limit has been exceeded. * Uses a sliding window of 1 second. * @returns true if rate limit exceeded, false otherwise */ _isRateLimited() { const now = Date.now(); this._requestTimes = this._requestTimes.filter(t => now - t < 1e3); if (this._requestTimes.length >= this._rateLimit) { return true; } this._requestTimes.push(now); return false; } /** * Type guard to validate MessageData structure */ static _isValidMessageData(data) { if (typeof data !== "object" || data === null) return false; const msg = data; if (typeof msg.messageId !== "string" || msg.messageId.length === 0) return false; if (typeof msg.method !== "string" || msg.method.length === 0) return false; if (msg.params !== void 0 && !Array.isArray(msg.params)) return false; if (msg.isHeartbeat !== void 0 && typeof msg.isHeartbeat !== "boolean") return false; return true; } /** * Handles incoming postMessage events from child frames. */ _onMessage(ev) { if (this._destroyed) return; if (this._origin !== "*" && ev.origin !== this._origin) { return; } if (!_CrossFrameLMS._isValidMessageData(ev.data)) return; const msg = ev.data; if (!ev.source) return; if (!("postMessage" in ev.source)) return; const source = ev.source; if (msg.isHeartbeat) { const resp = { messageId: msg.messageId, isHeartbeat: true }; source.postMessage(resp, this._origin); return; } if (this._isRateLimited()) { const resp = { messageId: msg.messageId, error: { message: "Rate limit exceeded", code: "101" } }; source.postMessage(resp, this._origin); return; } if (!_CrossFrameLMS.ALLOWED_METHODS.has(msg.method)) { const resp = { messageId: msg.messageId, error: { message: `Method not allowed: ${msg.method}`, code: "101" } }; source.postMessage(resp, this._origin); return; } this._process(msg, source); } /** * Processes a validated message by invoking the requested API method. */ _process(msg, source) { const sendResponse = (result, error) => { const resp = { messageId: msg.messageId }; if (result !== void 0) resp.result = result; if (error !== void 0) resp.error = error; source.postMessage(resp, this._origin); }; try { const fn = this._api[msg.method]; if (typeof fn !== "function") { sendResponse(void 0, { message: `Method ${msg.method} not found` }); return; } const params = Array.isArray(msg.params) ? msg.params : []; const result = fn.apply(this._api, params); if (result && typeof result.then === "function") { result.then(r => sendResponse(r)).catch(e => { const message = e instanceof Error ? e.message : "Unknown error"; const code = e && typeof e === "object" && "code" in e && typeof e.code === "string" ? e.code : void 0; const errorObj = { message }; if (code !== void 0) { errorObj.code = code; } sendResponse(void 0, errorObj); }); } else { sendResponse(result); } } catch (e) { const message = e instanceof Error ? e.message : "Unknown error"; const code = e && typeof e === "object" && "code" in e && typeof e.code === "string" ? e.code : void 0; const errorObj = { message }; if (code !== void 0) { errorObj.code = code; } sendResponse(void 0, errorObj); } } }; /** * Strict allowlist of methods that can be invoked via cross-frame messages. * Only SCORM API methods and internal helpers are permitted. */ __publicField(_CrossFrameLMS, "ALLOWED_METHODS", /* @__PURE__ */new Set([ // SCORM 1.2 methods "LMSInitialize", "LMSFinish", "LMSGetValue", "LMSSetValue", "LMSCommit", "LMSGetLastError", "LMSGetErrorString", "LMSGetDiagnostic", // SCORM 2004 methods "Initialize", "Terminate", "GetValue", "SetValue", "Commit", "GetLastError", "GetErrorString", "GetDiagnostic", // Internal method for cache warming "getFlattenedCMI"])); let CrossFrameLMS = _CrossFrameLMS; return CrossFrameLMS; })(); //# sourceMappingURL=cross-frame-lms.js.map