@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.cjs CHANGED
@@ -1600,6 +1600,101 @@ function generateLrsBridgeCode(options) {
1600
1600
  return hasValidMbox || hasValidAccount;
1601
1601
  }
1602
1602
 
1603
+ // ========================================================================
1604
+ // CROSS-FRAME ACTOR SHARING
1605
+ // PA-Patcher injects the bridge into ALL HTML files in a SCORM package.
1606
+ // Rise courses have multiple HTML files (index.html, scormcontent/index.html),
1607
+ // each getting their own bridge instance with its own LRS.actor.
1608
+ // The skin overlay (email gate) only runs in one frame and sets the actor there.
1609
+ // Without sharing, other bridge instances keep the Anonymous Learner actor.
1610
+ // Solution: persist actor to localStorage so all bridge instances can use it.
1611
+ // ========================================================================
1612
+ var ACTOR_STORAGE_KEY = 'pa_lrs_shared_actor';
1613
+
1614
+ function isAnonymousActor(actor) {
1615
+ if (!actor) return true;
1616
+ if (!actor.name) return true;
1617
+ var n = actor.name.toLowerCase();
1618
+ return n === 'anonymous learner' || n === 'unknown learner' || n === 'anonymous' || n === 'unknown';
1619
+ }
1620
+
1621
+ /**
1622
+ * Persist actor to localStorage for cross-frame sharing.
1623
+ * Only stores non-anonymous actors.
1624
+ */
1625
+ function persistActor(actor) {
1626
+ if (!actor || isAnonymousActor(actor)) return;
1627
+ try {
1628
+ localStorage.setItem(ACTOR_STORAGE_KEY, JSON.stringify(actor));
1629
+ log('Actor persisted to localStorage:', actor.name);
1630
+ } catch (e) { /* localStorage unavailable */ }
1631
+ }
1632
+
1633
+ /**
1634
+ * Load shared actor from localStorage.
1635
+ * Returns the stored actor or null.
1636
+ */
1637
+ function loadSharedActor() {
1638
+ try {
1639
+ var stored = localStorage.getItem(ACTOR_STORAGE_KEY);
1640
+ if (stored) {
1641
+ var actor = JSON.parse(stored);
1642
+ if (actor && !isAnonymousActor(actor)) {
1643
+ return actor;
1644
+ }
1645
+ }
1646
+ } catch (e) { /* localStorage unavailable or parse error */ }
1647
+ return null;
1648
+ }
1649
+
1650
+ /**
1651
+ * Get the best available actor for statement building.
1652
+ * Priority: LRS.actor (if non-anonymous) > localStorage shared actor > LRS.actor > extractActor()
1653
+ */
1654
+ function getActor() {
1655
+ // If current actor is non-anonymous, use it
1656
+ if (LRS.actor && !isAnonymousActor(LRS.actor)) {
1657
+ return LRS.actor;
1658
+ }
1659
+ // Check localStorage for actor set by another frame (e.g., skin overlay)
1660
+ var shared = loadSharedActor();
1661
+ if (shared) {
1662
+ // Update local actor so future calls are fast
1663
+ LRS.actor = shared;
1664
+ log('Actor loaded from cross-frame storage:', shared.name);
1665
+ return shared;
1666
+ }
1667
+ // Fallback to current actor or re-extract
1668
+ return LRS.actor || extractActor();
1669
+ }
1670
+
1671
+ /**
1672
+ * Set actor on the bridge with cross-frame persistence.
1673
+ * Called by skins and external code via window.pa_patcher.lrs.setActor(actor)
1674
+ */
1675
+ LRS.setActor = function(actor) {
1676
+ LRS.actor = actor;
1677
+ persistActor(actor);
1678
+ // Also try to propagate to other frames in the hierarchy
1679
+ try {
1680
+ var w = window;
1681
+ for (var i = 0; i < 10; i++) {
1682
+ try {
1683
+ if (w !== window && w.pa_patcher && w.pa_patcher.lrs) {
1684
+ w.pa_patcher.lrs.actor = actor;
1685
+ }
1686
+ } catch (e) { /* cross-origin */ }
1687
+ if (w === w.parent) break;
1688
+ w = w.parent;
1689
+ }
1690
+ } catch (e) {}
1691
+ if (window.console && window.console.info) {
1692
+ console.info('[PA-LRS] Actor set:', actor.name,
1693
+ actor.mbox ? '(' + actor.mbox + ')' : '',
1694
+ '\u2014 shared across frames');
1695
+ }
1696
+ };
1697
+
1603
1698
  /**
1604
1699
  * Async version that fetches Bravais session if needed
1605
1700
  * Also re-checks all sources in case they became available
@@ -1611,6 +1706,7 @@ function generateLrsBridgeCode(options) {
1611
1706
  // Try to enhance actor with email if missing
1612
1707
  enhanceActorWithEmail(actor, function(enhancedActor) {
1613
1708
  LRS.actor = enhancedActor;
1709
+ persistActor(enhancedActor);
1614
1710
  callback(enhancedActor);
1615
1711
  });
1616
1712
  }
@@ -1956,12 +2052,12 @@ function generateLrsBridgeCode(options) {
1956
2052
  // Without this override, our statements have a different object.id than cloudplayer
1957
2053
  // statements, so Analytics treats them as unrelated to the document.
1958
2054
  if (docData.guid) {
1959
- if (LRS.courseInfo.guid && LRS.courseInfo.guid !== docData.guid) {
1960
- log('Overriding baked-in GUID with Bravais GUID:', LRS.courseInfo.guid, '->', docData.guid);
1961
- }
2055
+ var oldGuid = LRS.courseInfo.guid;
1962
2056
  LRS.courseInfo.guid = docData.guid;
1963
2057
  LRS.courseInfo.id = 'http://xyleme.com/bravais/document/' + docData.guid;
1964
- log('Document GUID from API:', docData.guid);
2058
+ // Always-visible log (not behind DEBUG) so we can confirm override in production
2059
+ console.info('[PA-LRS] Document GUID set from API:', docData.guid,
2060
+ oldGuid && oldGuid !== docData.guid ? '(was: ' + oldGuid + ')' : '');
1965
2061
  }
1966
2062
 
1967
2063
  // Version GUID \u2014 same principle: Bravais version GUID must override baked-in
@@ -2270,21 +2366,20 @@ function generateLrsBridgeCode(options) {
2270
2366
  info.sharedLinkName = launchInfo.sharedLinkName || info.sharedLinkName;
2271
2367
  }
2272
2368
 
2273
- // 3b. Try to extract GUIDs from Xyleme Cloud Player's internal data
2274
- // This is more reliable than API calls which may fail with 401
2275
- if (!info.guid || !info.versionGuid) {
2276
- var cloudPlayerData = extractFromXylemeCloudPlayer();
2277
- if (cloudPlayerData) {
2278
- info.guid = cloudPlayerData.guid || info.guid;
2279
- info.versionGuid = cloudPlayerData.versionGuid || info.versionGuid;
2280
- info.documentId = cloudPlayerData.documentId || info.documentId;
2281
- info.title = cloudPlayerData.title || info.title;
2282
- log('Updated course info from Cloud Player:', {
2283
- guid: info.guid,
2284
- versionGuid: info.versionGuid,
2285
- documentId: info.documentId
2286
- });
2287
- }
2369
+ // 3b. Always try to extract GUIDs from Xyleme Cloud Player's internal data.
2370
+ // Cloud Player data is authoritative \u2014 it has the canonical Bravais GUID.
2371
+ // This overrides any baked-in GUID which is just a random UUID from wrap time.
2372
+ var cloudPlayerData = extractFromXylemeCloudPlayer();
2373
+ if (cloudPlayerData) {
2374
+ if (cloudPlayerData.guid) info.guid = cloudPlayerData.guid;
2375
+ if (cloudPlayerData.versionGuid) info.versionGuid = cloudPlayerData.versionGuid;
2376
+ info.documentId = cloudPlayerData.documentId || info.documentId;
2377
+ info.title = cloudPlayerData.title || info.title;
2378
+ log('Updated course info from Cloud Player:', {
2379
+ guid: info.guid,
2380
+ versionGuid: info.versionGuid,
2381
+ documentId: info.documentId
2382
+ });
2288
2383
  }
2289
2384
 
2290
2385
  // 4. Try getCourseTitle() from preloadIntegrity.js
@@ -2618,7 +2713,7 @@ function generateLrsBridgeCode(options) {
2618
2713
 
2619
2714
  var statement = {
2620
2715
  id: generateUUID(),
2621
- actor: LRS.actor || extractActor(),
2716
+ actor: getActor(),
2622
2717
  verb: typeof verb === 'string' ? (VERBS[verb] || { id: verb, display: { 'en-US': verb } }) : verb,
2623
2718
  object: courseObj,
2624
2719
  timestamp: new Date().toISOString()
@@ -3040,7 +3135,7 @@ function generateLrsBridgeCode(options) {
3040
3135
  function buildStatementXyleme(verb, activityObject, result, additionalContext) {
3041
3136
  var statement = {
3042
3137
  id: generateUUID(),
3043
- actor: LRS.actor || extractActor(),
3138
+ actor: getActor(),
3044
3139
  verb: typeof verb === 'string' ? (VERBS[verb] || { id: verb, display: { 'en-US': verb } }) : verb,
3045
3140
  object: activityObject,
3046
3141
  timestamp: new Date().toISOString()
@@ -4769,6 +4864,15 @@ function generateLrsBridgeCode(options) {
4769
4864
  // Extract actor (sync first for immediate use)
4770
4865
  extractActor();
4771
4866
 
4867
+ // Check localStorage for actor set by another frame (e.g., skin overlay in a different HTML file)
4868
+ if (isAnonymousActor(LRS.actor)) {
4869
+ var sharedActor = loadSharedActor();
4870
+ if (sharedActor) {
4871
+ LRS.actor = sharedActor;
4872
+ log('Loaded actor from cross-frame storage at init:', sharedActor.name);
4873
+ }
4874
+ }
4875
+
4772
4876
  log('Bridge initialized in mode:', LRS.mode);
4773
4877
  log('Actor:', LRS.actor);
4774
4878
  log('Course:', LRS.courseInfo);
@@ -4906,12 +5010,18 @@ function generateLrsBridgeCode(options) {
4906
5010
 
4907
5011
  // Always-visible bridge summary (not gated by DEBUG)
4908
5012
  // This ensures diagnostics are available even in production builds
5013
+ // Shows current GUID (may be from Cloud Player, baked-in, or pending API override)
5014
+ var currentGuid = LRS.courseInfo ? LRS.courseInfo.guid : DOCUMENT_GUID;
5015
+ var currentVerGuid = LRS.courseInfo ? LRS.courseInfo.versionGuid : VERSION_GUID;
5016
+ var sharedToken = LRS.courseInfo ? LRS.courseInfo.sharedLinkToken : null;
4909
5017
  if (window.console && window.console.info) {
4910
5018
  console.info('[PA-LRS] Bridge v' + LRS.version + ': mode=' + LRS.mode +
4911
5019
  ', endpoint=' + (LRS_ENDPOINT ? LRS_ENDPOINT.substring(0, 30) + '...' : 'NONE') +
4912
5020
  ', proxy=' + (LRS_PROXY_ENDPOINT ? 'yes' : 'no') +
4913
- ', docGuid=' + (DOCUMENT_GUID || 'NONE') +
4914
- ', verGuid=' + (VERSION_GUID || 'NONE') +
5021
+ ', docGuid=' + (currentGuid || 'NONE') +
5022
+ (currentGuid !== DOCUMENT_GUID ? ' (overridden from baked: ' + DOCUMENT_GUID + ')' : '') +
5023
+ ', verGuid=' + (currentVerGuid || 'NONE') +
5024
+ ', sharedToken=' + (sharedToken || 'NONE') +
4915
5025
  ', actor=' + (LRS.actor ? (LRS.actor.name || 'anonymous') : 'none') +
4916
5026
  ', hasRefreshActor=' + (typeof LRS.refreshActor === 'function'));
4917
5027
  }
@@ -4982,35 +5092,30 @@ function generateLrsBridgeCode(options) {
4982
5092
  });
4983
5093
  }
4984
5094
 
4985
- if (!LRS.courseInfo.guid) {
4986
- // When we have a shared link token, use /api/shared/{token}/documents directly
4987
- // This endpoint does NOT require authentication and returns both document GUID and version GUID
4988
- if (sharedLinkToken) {
4989
- log('Have shared link token, fetching document data via shared API...');
4990
- // fetchDocumentMetadata will use /api/shared/{token}/documents when sharedLinkToken is present
4991
- fetchDocumentMetadata(null, function(docData) {
4992
- if (docData) {
4993
- updateCourseInfoFromApi(docData);
4994
- // Also update document name if available
4995
- if (docData.name) {
4996
- LRS.courseInfo.sharedLinkName = docData.name;
4997
- }
4998
- } else {
4999
- warn('Could not fetch document data from shared API - statements may fail aggregation');
5095
+ // ALWAYS fetch document metadata from the Bravais API.
5096
+ // Even if we have a baked-in GUID, we must override it with the canonical
5097
+ // Bravais GUID so our statements link to the same document as cloudplayer.
5098
+ // The baked-in GUID is a random UUID from wrap time; the Bravais GUID is
5099
+ // assigned at upload and is what Analytics uses for aggregation.
5100
+ if (sharedLinkToken) {
5101
+ log('Have shared link token, fetching document data via shared API...');
5102
+ fetchDocumentMetadata(null, function(docData) {
5103
+ if (docData) {
5104
+ updateCourseInfoFromApi(docData);
5105
+ if (docData.name) {
5106
+ LRS.courseInfo.sharedLinkName = docData.name;
5000
5107
  }
5001
- maybeEnrichAndReady();
5002
- });
5003
- } else if (documentId) {
5004
- // No shared link token, try with extracted document ID (requires auth)
5005
- log('No shared link token, fetching document data with ID:', documentId);
5006
- fetchDocDataAndReady(documentId);
5007
- } else {
5008
- // No identifiers available
5009
- warn('No shared link token or document ID - statements may fail aggregation');
5108
+ } else {
5109
+ warn('Could not fetch document data from shared API - statements may use baked-in GUID');
5110
+ }
5010
5111
  maybeEnrichAndReady();
5011
- }
5112
+ });
5113
+ } else if (documentId) {
5114
+ log('Fetching document data with ID:', documentId);
5115
+ fetchDocDataAndReady(documentId);
5012
5116
  } else {
5013
- // Already have GUID \u2014 still try to enrich packageName if missing
5117
+ // No identifiers available
5118
+ warn('No shared link token or document ID - statements will use baked-in GUID');
5014
5119
  maybeEnrichAndReady();
5015
5120
  }
5016
5121