@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/cli.js CHANGED
@@ -1922,6 +1922,101 @@ function generateLrsBridgeCode(options) {
1922
1922
  return hasValidMbox || hasValidAccount;
1923
1923
  }
1924
1924
 
1925
+ // ========================================================================
1926
+ // CROSS-FRAME ACTOR SHARING
1927
+ // PA-Patcher injects the bridge into ALL HTML files in a SCORM package.
1928
+ // Rise courses have multiple HTML files (index.html, scormcontent/index.html),
1929
+ // each getting their own bridge instance with its own LRS.actor.
1930
+ // The skin overlay (email gate) only runs in one frame and sets the actor there.
1931
+ // Without sharing, other bridge instances keep the Anonymous Learner actor.
1932
+ // Solution: persist actor to localStorage so all bridge instances can use it.
1933
+ // ========================================================================
1934
+ var ACTOR_STORAGE_KEY = 'pa_lrs_shared_actor';
1935
+
1936
+ function isAnonymousActor(actor) {
1937
+ if (!actor) return true;
1938
+ if (!actor.name) return true;
1939
+ var n = actor.name.toLowerCase();
1940
+ return n === 'anonymous learner' || n === 'unknown learner' || n === 'anonymous' || n === 'unknown';
1941
+ }
1942
+
1943
+ /**
1944
+ * Persist actor to localStorage for cross-frame sharing.
1945
+ * Only stores non-anonymous actors.
1946
+ */
1947
+ function persistActor(actor) {
1948
+ if (!actor || isAnonymousActor(actor)) return;
1949
+ try {
1950
+ localStorage.setItem(ACTOR_STORAGE_KEY, JSON.stringify(actor));
1951
+ log('Actor persisted to localStorage:', actor.name);
1952
+ } catch (e) { /* localStorage unavailable */ }
1953
+ }
1954
+
1955
+ /**
1956
+ * Load shared actor from localStorage.
1957
+ * Returns the stored actor or null.
1958
+ */
1959
+ function loadSharedActor() {
1960
+ try {
1961
+ var stored = localStorage.getItem(ACTOR_STORAGE_KEY);
1962
+ if (stored) {
1963
+ var actor = JSON.parse(stored);
1964
+ if (actor && !isAnonymousActor(actor)) {
1965
+ return actor;
1966
+ }
1967
+ }
1968
+ } catch (e) { /* localStorage unavailable or parse error */ }
1969
+ return null;
1970
+ }
1971
+
1972
+ /**
1973
+ * Get the best available actor for statement building.
1974
+ * Priority: LRS.actor (if non-anonymous) > localStorage shared actor > LRS.actor > extractActor()
1975
+ */
1976
+ function getActor() {
1977
+ // If current actor is non-anonymous, use it
1978
+ if (LRS.actor && !isAnonymousActor(LRS.actor)) {
1979
+ return LRS.actor;
1980
+ }
1981
+ // Check localStorage for actor set by another frame (e.g., skin overlay)
1982
+ var shared = loadSharedActor();
1983
+ if (shared) {
1984
+ // Update local actor so future calls are fast
1985
+ LRS.actor = shared;
1986
+ log('Actor loaded from cross-frame storage:', shared.name);
1987
+ return shared;
1988
+ }
1989
+ // Fallback to current actor or re-extract
1990
+ return LRS.actor || extractActor();
1991
+ }
1992
+
1993
+ /**
1994
+ * Set actor on the bridge with cross-frame persistence.
1995
+ * Called by skins and external code via window.pa_patcher.lrs.setActor(actor)
1996
+ */
1997
+ LRS.setActor = function(actor) {
1998
+ LRS.actor = actor;
1999
+ persistActor(actor);
2000
+ // Also try to propagate to other frames in the hierarchy
2001
+ try {
2002
+ var w = window;
2003
+ for (var i = 0; i < 10; i++) {
2004
+ try {
2005
+ if (w !== window && w.pa_patcher && w.pa_patcher.lrs) {
2006
+ w.pa_patcher.lrs.actor = actor;
2007
+ }
2008
+ } catch (e) { /* cross-origin */ }
2009
+ if (w === w.parent) break;
2010
+ w = w.parent;
2011
+ }
2012
+ } catch (e) {}
2013
+ if (window.console && window.console.info) {
2014
+ console.info('[PA-LRS] Actor set:', actor.name,
2015
+ actor.mbox ? '(' + actor.mbox + ')' : '',
2016
+ '\u2014 shared across frames');
2017
+ }
2018
+ };
2019
+
1925
2020
  /**
1926
2021
  * Async version that fetches Bravais session if needed
1927
2022
  * Also re-checks all sources in case they became available
@@ -1933,6 +2028,7 @@ function generateLrsBridgeCode(options) {
1933
2028
  // Try to enhance actor with email if missing
1934
2029
  enhanceActorWithEmail(actor, function(enhancedActor) {
1935
2030
  LRS.actor = enhancedActor;
2031
+ persistActor(enhancedActor);
1936
2032
  callback(enhancedActor);
1937
2033
  });
1938
2034
  }
@@ -2278,12 +2374,12 @@ function generateLrsBridgeCode(options) {
2278
2374
  // Without this override, our statements have a different object.id than cloudplayer
2279
2375
  // statements, so Analytics treats them as unrelated to the document.
2280
2376
  if (docData.guid) {
2281
- if (LRS.courseInfo.guid && LRS.courseInfo.guid !== docData.guid) {
2282
- log('Overriding baked-in GUID with Bravais GUID:', LRS.courseInfo.guid, '->', docData.guid);
2283
- }
2377
+ var oldGuid = LRS.courseInfo.guid;
2284
2378
  LRS.courseInfo.guid = docData.guid;
2285
2379
  LRS.courseInfo.id = 'http://xyleme.com/bravais/document/' + docData.guid;
2286
- log('Document GUID from API:', docData.guid);
2380
+ // Always-visible log (not behind DEBUG) so we can confirm override in production
2381
+ console.info('[PA-LRS] Document GUID set from API:', docData.guid,
2382
+ oldGuid && oldGuid !== docData.guid ? '(was: ' + oldGuid + ')' : '');
2287
2383
  }
2288
2384
 
2289
2385
  // Version GUID \u2014 same principle: Bravais version GUID must override baked-in
@@ -2592,21 +2688,20 @@ function generateLrsBridgeCode(options) {
2592
2688
  info.sharedLinkName = launchInfo.sharedLinkName || info.sharedLinkName;
2593
2689
  }
2594
2690
 
2595
- // 3b. Try to extract GUIDs from Xyleme Cloud Player's internal data
2596
- // This is more reliable than API calls which may fail with 401
2597
- if (!info.guid || !info.versionGuid) {
2598
- var cloudPlayerData = extractFromXylemeCloudPlayer();
2599
- if (cloudPlayerData) {
2600
- info.guid = cloudPlayerData.guid || info.guid;
2601
- info.versionGuid = cloudPlayerData.versionGuid || info.versionGuid;
2602
- info.documentId = cloudPlayerData.documentId || info.documentId;
2603
- info.title = cloudPlayerData.title || info.title;
2604
- log('Updated course info from Cloud Player:', {
2605
- guid: info.guid,
2606
- versionGuid: info.versionGuid,
2607
- documentId: info.documentId
2608
- });
2609
- }
2691
+ // 3b. Always try to extract GUIDs from Xyleme Cloud Player's internal data.
2692
+ // Cloud Player data is authoritative \u2014 it has the canonical Bravais GUID.
2693
+ // This overrides any baked-in GUID which is just a random UUID from wrap time.
2694
+ var cloudPlayerData = extractFromXylemeCloudPlayer();
2695
+ if (cloudPlayerData) {
2696
+ if (cloudPlayerData.guid) info.guid = cloudPlayerData.guid;
2697
+ if (cloudPlayerData.versionGuid) info.versionGuid = cloudPlayerData.versionGuid;
2698
+ info.documentId = cloudPlayerData.documentId || info.documentId;
2699
+ info.title = cloudPlayerData.title || info.title;
2700
+ log('Updated course info from Cloud Player:', {
2701
+ guid: info.guid,
2702
+ versionGuid: info.versionGuid,
2703
+ documentId: info.documentId
2704
+ });
2610
2705
  }
2611
2706
 
2612
2707
  // 4. Try getCourseTitle() from preloadIntegrity.js
@@ -2940,7 +3035,7 @@ function generateLrsBridgeCode(options) {
2940
3035
 
2941
3036
  var statement = {
2942
3037
  id: generateUUID(),
2943
- actor: LRS.actor || extractActor(),
3038
+ actor: getActor(),
2944
3039
  verb: typeof verb === 'string' ? (VERBS[verb] || { id: verb, display: { 'en-US': verb } }) : verb,
2945
3040
  object: courseObj,
2946
3041
  timestamp: new Date().toISOString()
@@ -3362,7 +3457,7 @@ function generateLrsBridgeCode(options) {
3362
3457
  function buildStatementXyleme(verb, activityObject, result, additionalContext) {
3363
3458
  var statement = {
3364
3459
  id: generateUUID(),
3365
- actor: LRS.actor || extractActor(),
3460
+ actor: getActor(),
3366
3461
  verb: typeof verb === 'string' ? (VERBS[verb] || { id: verb, display: { 'en-US': verb } }) : verb,
3367
3462
  object: activityObject,
3368
3463
  timestamp: new Date().toISOString()
@@ -5091,6 +5186,15 @@ function generateLrsBridgeCode(options) {
5091
5186
  // Extract actor (sync first for immediate use)
5092
5187
  extractActor();
5093
5188
 
5189
+ // Check localStorage for actor set by another frame (e.g., skin overlay in a different HTML file)
5190
+ if (isAnonymousActor(LRS.actor)) {
5191
+ var sharedActor = loadSharedActor();
5192
+ if (sharedActor) {
5193
+ LRS.actor = sharedActor;
5194
+ log('Loaded actor from cross-frame storage at init:', sharedActor.name);
5195
+ }
5196
+ }
5197
+
5094
5198
  log('Bridge initialized in mode:', LRS.mode);
5095
5199
  log('Actor:', LRS.actor);
5096
5200
  log('Course:', LRS.courseInfo);
@@ -5228,12 +5332,18 @@ function generateLrsBridgeCode(options) {
5228
5332
 
5229
5333
  // Always-visible bridge summary (not gated by DEBUG)
5230
5334
  // This ensures diagnostics are available even in production builds
5335
+ // Shows current GUID (may be from Cloud Player, baked-in, or pending API override)
5336
+ var currentGuid = LRS.courseInfo ? LRS.courseInfo.guid : DOCUMENT_GUID;
5337
+ var currentVerGuid = LRS.courseInfo ? LRS.courseInfo.versionGuid : VERSION_GUID;
5338
+ var sharedToken = LRS.courseInfo ? LRS.courseInfo.sharedLinkToken : null;
5231
5339
  if (window.console && window.console.info) {
5232
5340
  console.info('[PA-LRS] Bridge v' + LRS.version + ': mode=' + LRS.mode +
5233
5341
  ', endpoint=' + (LRS_ENDPOINT ? LRS_ENDPOINT.substring(0, 30) + '...' : 'NONE') +
5234
5342
  ', proxy=' + (LRS_PROXY_ENDPOINT ? 'yes' : 'no') +
5235
- ', docGuid=' + (DOCUMENT_GUID || 'NONE') +
5236
- ', verGuid=' + (VERSION_GUID || 'NONE') +
5343
+ ', docGuid=' + (currentGuid || 'NONE') +
5344
+ (currentGuid !== DOCUMENT_GUID ? ' (overridden from baked: ' + DOCUMENT_GUID + ')' : '') +
5345
+ ', verGuid=' + (currentVerGuid || 'NONE') +
5346
+ ', sharedToken=' + (sharedToken || 'NONE') +
5237
5347
  ', actor=' + (LRS.actor ? (LRS.actor.name || 'anonymous') : 'none') +
5238
5348
  ', hasRefreshActor=' + (typeof LRS.refreshActor === 'function'));
5239
5349
  }
@@ -5304,35 +5414,30 @@ function generateLrsBridgeCode(options) {
5304
5414
  });
5305
5415
  }
5306
5416
 
5307
- if (!LRS.courseInfo.guid) {
5308
- // When we have a shared link token, use /api/shared/{token}/documents directly
5309
- // This endpoint does NOT require authentication and returns both document GUID and version GUID
5310
- if (sharedLinkToken) {
5311
- log('Have shared link token, fetching document data via shared API...');
5312
- // fetchDocumentMetadata will use /api/shared/{token}/documents when sharedLinkToken is present
5313
- fetchDocumentMetadata(null, function(docData) {
5314
- if (docData) {
5315
- updateCourseInfoFromApi(docData);
5316
- // Also update document name if available
5317
- if (docData.name) {
5318
- LRS.courseInfo.sharedLinkName = docData.name;
5319
- }
5320
- } else {
5321
- warn('Could not fetch document data from shared API - statements may fail aggregation');
5417
+ // ALWAYS fetch document metadata from the Bravais API.
5418
+ // Even if we have a baked-in GUID, we must override it with the canonical
5419
+ // Bravais GUID so our statements link to the same document as cloudplayer.
5420
+ // The baked-in GUID is a random UUID from wrap time; the Bravais GUID is
5421
+ // assigned at upload and is what Analytics uses for aggregation.
5422
+ if (sharedLinkToken) {
5423
+ log('Have shared link token, fetching document data via shared API...');
5424
+ fetchDocumentMetadata(null, function(docData) {
5425
+ if (docData) {
5426
+ updateCourseInfoFromApi(docData);
5427
+ if (docData.name) {
5428
+ LRS.courseInfo.sharedLinkName = docData.name;
5322
5429
  }
5323
- maybeEnrichAndReady();
5324
- });
5325
- } else if (documentId) {
5326
- // No shared link token, try with extracted document ID (requires auth)
5327
- log('No shared link token, fetching document data with ID:', documentId);
5328
- fetchDocDataAndReady(documentId);
5329
- } else {
5330
- // No identifiers available
5331
- warn('No shared link token or document ID - statements may fail aggregation');
5430
+ } else {
5431
+ warn('Could not fetch document data from shared API - statements may use baked-in GUID');
5432
+ }
5332
5433
  maybeEnrichAndReady();
5333
- }
5434
+ });
5435
+ } else if (documentId) {
5436
+ log('Fetching document data with ID:', documentId);
5437
+ fetchDocDataAndReady(documentId);
5334
5438
  } else {
5335
- // Already have GUID \u2014 still try to enrich packageName if missing
5439
+ // No identifiers available
5440
+ warn('No shared link token or document ID - statements will use baked-in GUID');
5336
5441
  maybeEnrichAndReady();
5337
5442
  }
5338
5443