@patch-adams/core 1.5.18 → 1.5.20

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/index.js CHANGED
@@ -1591,6 +1591,101 @@ function generateLrsBridgeCode(options) {
1591
1591
  return hasValidMbox || hasValidAccount;
1592
1592
  }
1593
1593
 
1594
+ // ========================================================================
1595
+ // CROSS-FRAME ACTOR SHARING
1596
+ // PA-Patcher injects the bridge into ALL HTML files in a SCORM package.
1597
+ // Rise courses have multiple HTML files (index.html, scormcontent/index.html),
1598
+ // each getting their own bridge instance with its own LRS.actor.
1599
+ // The skin overlay (email gate) only runs in one frame and sets the actor there.
1600
+ // Without sharing, other bridge instances keep the Anonymous Learner actor.
1601
+ // Solution: persist actor to localStorage so all bridge instances can use it.
1602
+ // ========================================================================
1603
+ var ACTOR_STORAGE_KEY = 'pa_lrs_shared_actor';
1604
+
1605
+ function isAnonymousActor(actor) {
1606
+ if (!actor) return true;
1607
+ if (!actor.name) return true;
1608
+ var n = actor.name.toLowerCase();
1609
+ return n === 'anonymous learner' || n === 'unknown learner' || n === 'anonymous' || n === 'unknown';
1610
+ }
1611
+
1612
+ /**
1613
+ * Persist actor to localStorage for cross-frame sharing.
1614
+ * Only stores non-anonymous actors.
1615
+ */
1616
+ function persistActor(actor) {
1617
+ if (!actor || isAnonymousActor(actor)) return;
1618
+ try {
1619
+ localStorage.setItem(ACTOR_STORAGE_KEY, JSON.stringify(actor));
1620
+ log('Actor persisted to localStorage:', actor.name);
1621
+ } catch (e) { /* localStorage unavailable */ }
1622
+ }
1623
+
1624
+ /**
1625
+ * Load shared actor from localStorage.
1626
+ * Returns the stored actor or null.
1627
+ */
1628
+ function loadSharedActor() {
1629
+ try {
1630
+ var stored = localStorage.getItem(ACTOR_STORAGE_KEY);
1631
+ if (stored) {
1632
+ var actor = JSON.parse(stored);
1633
+ if (actor && !isAnonymousActor(actor)) {
1634
+ return actor;
1635
+ }
1636
+ }
1637
+ } catch (e) { /* localStorage unavailable or parse error */ }
1638
+ return null;
1639
+ }
1640
+
1641
+ /**
1642
+ * Get the best available actor for statement building.
1643
+ * Priority: LRS.actor (if non-anonymous) > localStorage shared actor > LRS.actor > extractActor()
1644
+ */
1645
+ function getActor() {
1646
+ // If current actor is non-anonymous, use it
1647
+ if (LRS.actor && !isAnonymousActor(LRS.actor)) {
1648
+ return LRS.actor;
1649
+ }
1650
+ // Check localStorage for actor set by another frame (e.g., skin overlay)
1651
+ var shared = loadSharedActor();
1652
+ if (shared) {
1653
+ // Update local actor so future calls are fast
1654
+ LRS.actor = shared;
1655
+ log('Actor loaded from cross-frame storage:', shared.name);
1656
+ return shared;
1657
+ }
1658
+ // Fallback to current actor or re-extract
1659
+ return LRS.actor || extractActor();
1660
+ }
1661
+
1662
+ /**
1663
+ * Set actor on the bridge with cross-frame persistence.
1664
+ * Called by skins and external code via window.pa_patcher.lrs.setActor(actor)
1665
+ */
1666
+ LRS.setActor = function(actor) {
1667
+ LRS.actor = actor;
1668
+ persistActor(actor);
1669
+ // Also try to propagate to other frames in the hierarchy
1670
+ try {
1671
+ var w = window;
1672
+ for (var i = 0; i < 10; i++) {
1673
+ try {
1674
+ if (w !== window && w.pa_patcher && w.pa_patcher.lrs) {
1675
+ w.pa_patcher.lrs.actor = actor;
1676
+ }
1677
+ } catch (e) { /* cross-origin */ }
1678
+ if (w === w.parent) break;
1679
+ w = w.parent;
1680
+ }
1681
+ } catch (e) {}
1682
+ if (window.console && window.console.info) {
1683
+ console.info('[PA-LRS] Actor set:', actor.name,
1684
+ actor.mbox ? '(' + actor.mbox + ')' : '',
1685
+ '\u2014 shared across frames');
1686
+ }
1687
+ };
1688
+
1594
1689
  /**
1595
1690
  * Async version that fetches Bravais session if needed
1596
1691
  * Also re-checks all sources in case they became available
@@ -1602,6 +1697,7 @@ function generateLrsBridgeCode(options) {
1602
1697
  // Try to enhance actor with email if missing
1603
1698
  enhanceActorWithEmail(actor, function(enhancedActor) {
1604
1699
  LRS.actor = enhancedActor;
1700
+ persistActor(enhancedActor);
1605
1701
  callback(enhancedActor);
1606
1702
  });
1607
1703
  }
@@ -1947,12 +2043,12 @@ function generateLrsBridgeCode(options) {
1947
2043
  // Without this override, our statements have a different object.id than cloudplayer
1948
2044
  // statements, so Analytics treats them as unrelated to the document.
1949
2045
  if (docData.guid) {
1950
- if (LRS.courseInfo.guid && LRS.courseInfo.guid !== docData.guid) {
1951
- log('Overriding baked-in GUID with Bravais GUID:', LRS.courseInfo.guid, '->', docData.guid);
1952
- }
2046
+ var oldGuid = LRS.courseInfo.guid;
1953
2047
  LRS.courseInfo.guid = docData.guid;
1954
2048
  LRS.courseInfo.id = 'http://xyleme.com/bravais/document/' + docData.guid;
1955
- log('Document GUID from API:', docData.guid);
2049
+ // Always-visible log (not behind DEBUG) so we can confirm override in production
2050
+ console.info('[PA-LRS] Document GUID set from API:', docData.guid,
2051
+ oldGuid && oldGuid !== docData.guid ? '(was: ' + oldGuid + ')' : '');
1956
2052
  }
1957
2053
 
1958
2054
  // Version GUID \u2014 same principle: Bravais version GUID must override baked-in
@@ -2261,21 +2357,20 @@ function generateLrsBridgeCode(options) {
2261
2357
  info.sharedLinkName = launchInfo.sharedLinkName || info.sharedLinkName;
2262
2358
  }
2263
2359
 
2264
- // 3b. Try to extract GUIDs from Xyleme Cloud Player's internal data
2265
- // This is more reliable than API calls which may fail with 401
2266
- if (!info.guid || !info.versionGuid) {
2267
- var cloudPlayerData = extractFromXylemeCloudPlayer();
2268
- if (cloudPlayerData) {
2269
- info.guid = cloudPlayerData.guid || info.guid;
2270
- info.versionGuid = cloudPlayerData.versionGuid || info.versionGuid;
2271
- info.documentId = cloudPlayerData.documentId || info.documentId;
2272
- info.title = cloudPlayerData.title || info.title;
2273
- log('Updated course info from Cloud Player:', {
2274
- guid: info.guid,
2275
- versionGuid: info.versionGuid,
2276
- documentId: info.documentId
2277
- });
2278
- }
2360
+ // 3b. Always try to extract GUIDs from Xyleme Cloud Player's internal data.
2361
+ // Cloud Player data is authoritative \u2014 it has the canonical Bravais GUID.
2362
+ // This overrides any baked-in GUID which is just a random UUID from wrap time.
2363
+ var cloudPlayerData = extractFromXylemeCloudPlayer();
2364
+ if (cloudPlayerData) {
2365
+ if (cloudPlayerData.guid) info.guid = cloudPlayerData.guid;
2366
+ if (cloudPlayerData.versionGuid) info.versionGuid = cloudPlayerData.versionGuid;
2367
+ info.documentId = cloudPlayerData.documentId || info.documentId;
2368
+ info.title = cloudPlayerData.title || info.title;
2369
+ log('Updated course info from Cloud Player:', {
2370
+ guid: info.guid,
2371
+ versionGuid: info.versionGuid,
2372
+ documentId: info.documentId
2373
+ });
2279
2374
  }
2280
2375
 
2281
2376
  // 4. Try getCourseTitle() from preloadIntegrity.js
@@ -2609,7 +2704,7 @@ function generateLrsBridgeCode(options) {
2609
2704
 
2610
2705
  var statement = {
2611
2706
  id: generateUUID(),
2612
- actor: LRS.actor || extractActor(),
2707
+ actor: getActor(),
2613
2708
  verb: typeof verb === 'string' ? (VERBS[verb] || { id: verb, display: { 'en-US': verb } }) : verb,
2614
2709
  object: courseObj,
2615
2710
  timestamp: new Date().toISOString()
@@ -3031,7 +3126,7 @@ function generateLrsBridgeCode(options) {
3031
3126
  function buildStatementXyleme(verb, activityObject, result, additionalContext) {
3032
3127
  var statement = {
3033
3128
  id: generateUUID(),
3034
- actor: LRS.actor || extractActor(),
3129
+ actor: getActor(),
3035
3130
  verb: typeof verb === 'string' ? (VERBS[verb] || { id: verb, display: { 'en-US': verb } }) : verb,
3036
3131
  object: activityObject,
3037
3132
  timestamp: new Date().toISOString()
@@ -4760,6 +4855,15 @@ function generateLrsBridgeCode(options) {
4760
4855
  // Extract actor (sync first for immediate use)
4761
4856
  extractActor();
4762
4857
 
4858
+ // Check localStorage for actor set by another frame (e.g., skin overlay in a different HTML file)
4859
+ if (isAnonymousActor(LRS.actor)) {
4860
+ var sharedActor = loadSharedActor();
4861
+ if (sharedActor) {
4862
+ LRS.actor = sharedActor;
4863
+ log('Loaded actor from cross-frame storage at init:', sharedActor.name);
4864
+ }
4865
+ }
4866
+
4763
4867
  log('Bridge initialized in mode:', LRS.mode);
4764
4868
  log('Actor:', LRS.actor);
4765
4869
  log('Course:', LRS.courseInfo);
@@ -4897,12 +5001,18 @@ function generateLrsBridgeCode(options) {
4897
5001
 
4898
5002
  // Always-visible bridge summary (not gated by DEBUG)
4899
5003
  // This ensures diagnostics are available even in production builds
5004
+ // Shows current GUID (may be from Cloud Player, baked-in, or pending API override)
5005
+ var currentGuid = LRS.courseInfo ? LRS.courseInfo.guid : DOCUMENT_GUID;
5006
+ var currentVerGuid = LRS.courseInfo ? LRS.courseInfo.versionGuid : VERSION_GUID;
5007
+ var sharedToken = LRS.courseInfo ? LRS.courseInfo.sharedLinkToken : null;
4900
5008
  if (window.console && window.console.info) {
4901
5009
  console.info('[PA-LRS] Bridge v' + LRS.version + ': mode=' + LRS.mode +
4902
5010
  ', endpoint=' + (LRS_ENDPOINT ? LRS_ENDPOINT.substring(0, 30) + '...' : 'NONE') +
4903
5011
  ', proxy=' + (LRS_PROXY_ENDPOINT ? 'yes' : 'no') +
4904
- ', docGuid=' + (DOCUMENT_GUID || 'NONE') +
4905
- ', verGuid=' + (VERSION_GUID || 'NONE') +
5012
+ ', docGuid=' + (currentGuid || 'NONE') +
5013
+ (currentGuid !== DOCUMENT_GUID ? ' (overridden from baked: ' + DOCUMENT_GUID + ')' : '') +
5014
+ ', verGuid=' + (currentVerGuid || 'NONE') +
5015
+ ', sharedToken=' + (sharedToken || 'NONE') +
4906
5016
  ', actor=' + (LRS.actor ? (LRS.actor.name || 'anonymous') : 'none') +
4907
5017
  ', hasRefreshActor=' + (typeof LRS.refreshActor === 'function'));
4908
5018
  }
@@ -4973,35 +5083,30 @@ function generateLrsBridgeCode(options) {
4973
5083
  });
4974
5084
  }
4975
5085
 
4976
- if (!LRS.courseInfo.guid) {
4977
- // When we have a shared link token, use /api/shared/{token}/documents directly
4978
- // This endpoint does NOT require authentication and returns both document GUID and version GUID
4979
- if (sharedLinkToken) {
4980
- log('Have shared link token, fetching document data via shared API...');
4981
- // fetchDocumentMetadata will use /api/shared/{token}/documents when sharedLinkToken is present
4982
- fetchDocumentMetadata(null, function(docData) {
4983
- if (docData) {
4984
- updateCourseInfoFromApi(docData);
4985
- // Also update document name if available
4986
- if (docData.name) {
4987
- LRS.courseInfo.sharedLinkName = docData.name;
4988
- }
4989
- } else {
4990
- warn('Could not fetch document data from shared API - statements may fail aggregation');
5086
+ // ALWAYS fetch document metadata from the Bravais API.
5087
+ // Even if we have a baked-in GUID, we must override it with the canonical
5088
+ // Bravais GUID so our statements link to the same document as cloudplayer.
5089
+ // The baked-in GUID is a random UUID from wrap time; the Bravais GUID is
5090
+ // assigned at upload and is what Analytics uses for aggregation.
5091
+ if (sharedLinkToken) {
5092
+ log('Have shared link token, fetching document data via shared API...');
5093
+ fetchDocumentMetadata(null, function(docData) {
5094
+ if (docData) {
5095
+ updateCourseInfoFromApi(docData);
5096
+ if (docData.name) {
5097
+ LRS.courseInfo.sharedLinkName = docData.name;
4991
5098
  }
4992
- maybeEnrichAndReady();
4993
- });
4994
- } else if (documentId) {
4995
- // No shared link token, try with extracted document ID (requires auth)
4996
- log('No shared link token, fetching document data with ID:', documentId);
4997
- fetchDocDataAndReady(documentId);
4998
- } else {
4999
- // No identifiers available
5000
- warn('No shared link token or document ID - statements may fail aggregation');
5099
+ } else {
5100
+ warn('Could not fetch document data from shared API - statements may use baked-in GUID');
5101
+ }
5001
5102
  maybeEnrichAndReady();
5002
- }
5103
+ });
5104
+ } else if (documentId) {
5105
+ log('Fetching document data with ID:', documentId);
5106
+ fetchDocDataAndReady(documentId);
5003
5107
  } else {
5004
- // Already have GUID \u2014 still try to enrich packageName if missing
5108
+ // No identifiers available
5109
+ warn('No shared link token or document ID - statements will use baked-in GUID');
5005
5110
  maybeEnrichAndReady();
5006
5111
  }
5007
5112