@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.cjs CHANGED
@@ -1932,6 +1932,101 @@ function generateLrsBridgeCode(options) {
1932
1932
  return hasValidMbox || hasValidAccount;
1933
1933
  }
1934
1934
 
1935
+ // ========================================================================
1936
+ // CROSS-FRAME ACTOR SHARING
1937
+ // PA-Patcher injects the bridge into ALL HTML files in a SCORM package.
1938
+ // Rise courses have multiple HTML files (index.html, scormcontent/index.html),
1939
+ // each getting their own bridge instance with its own LRS.actor.
1940
+ // The skin overlay (email gate) only runs in one frame and sets the actor there.
1941
+ // Without sharing, other bridge instances keep the Anonymous Learner actor.
1942
+ // Solution: persist actor to localStorage so all bridge instances can use it.
1943
+ // ========================================================================
1944
+ var ACTOR_STORAGE_KEY = 'pa_lrs_shared_actor';
1945
+
1946
+ function isAnonymousActor(actor) {
1947
+ if (!actor) return true;
1948
+ if (!actor.name) return true;
1949
+ var n = actor.name.toLowerCase();
1950
+ return n === 'anonymous learner' || n === 'unknown learner' || n === 'anonymous' || n === 'unknown';
1951
+ }
1952
+
1953
+ /**
1954
+ * Persist actor to localStorage for cross-frame sharing.
1955
+ * Only stores non-anonymous actors.
1956
+ */
1957
+ function persistActor(actor) {
1958
+ if (!actor || isAnonymousActor(actor)) return;
1959
+ try {
1960
+ localStorage.setItem(ACTOR_STORAGE_KEY, JSON.stringify(actor));
1961
+ log('Actor persisted to localStorage:', actor.name);
1962
+ } catch (e) { /* localStorage unavailable */ }
1963
+ }
1964
+
1965
+ /**
1966
+ * Load shared actor from localStorage.
1967
+ * Returns the stored actor or null.
1968
+ */
1969
+ function loadSharedActor() {
1970
+ try {
1971
+ var stored = localStorage.getItem(ACTOR_STORAGE_KEY);
1972
+ if (stored) {
1973
+ var actor = JSON.parse(stored);
1974
+ if (actor && !isAnonymousActor(actor)) {
1975
+ return actor;
1976
+ }
1977
+ }
1978
+ } catch (e) { /* localStorage unavailable or parse error */ }
1979
+ return null;
1980
+ }
1981
+
1982
+ /**
1983
+ * Get the best available actor for statement building.
1984
+ * Priority: LRS.actor (if non-anonymous) > localStorage shared actor > LRS.actor > extractActor()
1985
+ */
1986
+ function getActor() {
1987
+ // If current actor is non-anonymous, use it
1988
+ if (LRS.actor && !isAnonymousActor(LRS.actor)) {
1989
+ return LRS.actor;
1990
+ }
1991
+ // Check localStorage for actor set by another frame (e.g., skin overlay)
1992
+ var shared = loadSharedActor();
1993
+ if (shared) {
1994
+ // Update local actor so future calls are fast
1995
+ LRS.actor = shared;
1996
+ log('Actor loaded from cross-frame storage:', shared.name);
1997
+ return shared;
1998
+ }
1999
+ // Fallback to current actor or re-extract
2000
+ return LRS.actor || extractActor();
2001
+ }
2002
+
2003
+ /**
2004
+ * Set actor on the bridge with cross-frame persistence.
2005
+ * Called by skins and external code via window.pa_patcher.lrs.setActor(actor)
2006
+ */
2007
+ LRS.setActor = function(actor) {
2008
+ LRS.actor = actor;
2009
+ persistActor(actor);
2010
+ // Also try to propagate to other frames in the hierarchy
2011
+ try {
2012
+ var w = window;
2013
+ for (var i = 0; i < 10; i++) {
2014
+ try {
2015
+ if (w !== window && w.pa_patcher && w.pa_patcher.lrs) {
2016
+ w.pa_patcher.lrs.actor = actor;
2017
+ }
2018
+ } catch (e) { /* cross-origin */ }
2019
+ if (w === w.parent) break;
2020
+ w = w.parent;
2021
+ }
2022
+ } catch (e) {}
2023
+ if (window.console && window.console.info) {
2024
+ console.info('[PA-LRS] Actor set:', actor.name,
2025
+ actor.mbox ? '(' + actor.mbox + ')' : '',
2026
+ '\u2014 shared across frames');
2027
+ }
2028
+ };
2029
+
1935
2030
  /**
1936
2031
  * Async version that fetches Bravais session if needed
1937
2032
  * Also re-checks all sources in case they became available
@@ -1943,6 +2038,7 @@ function generateLrsBridgeCode(options) {
1943
2038
  // Try to enhance actor with email if missing
1944
2039
  enhanceActorWithEmail(actor, function(enhancedActor) {
1945
2040
  LRS.actor = enhancedActor;
2041
+ persistActor(enhancedActor);
1946
2042
  callback(enhancedActor);
1947
2043
  });
1948
2044
  }
@@ -2288,12 +2384,12 @@ function generateLrsBridgeCode(options) {
2288
2384
  // Without this override, our statements have a different object.id than cloudplayer
2289
2385
  // statements, so Analytics treats them as unrelated to the document.
2290
2386
  if (docData.guid) {
2291
- if (LRS.courseInfo.guid && LRS.courseInfo.guid !== docData.guid) {
2292
- log('Overriding baked-in GUID with Bravais GUID:', LRS.courseInfo.guid, '->', docData.guid);
2293
- }
2387
+ var oldGuid = LRS.courseInfo.guid;
2294
2388
  LRS.courseInfo.guid = docData.guid;
2295
2389
  LRS.courseInfo.id = 'http://xyleme.com/bravais/document/' + docData.guid;
2296
- log('Document GUID from API:', docData.guid);
2390
+ // Always-visible log (not behind DEBUG) so we can confirm override in production
2391
+ console.info('[PA-LRS] Document GUID set from API:', docData.guid,
2392
+ oldGuid && oldGuid !== docData.guid ? '(was: ' + oldGuid + ')' : '');
2297
2393
  }
2298
2394
 
2299
2395
  // Version GUID \u2014 same principle: Bravais version GUID must override baked-in
@@ -2602,21 +2698,20 @@ function generateLrsBridgeCode(options) {
2602
2698
  info.sharedLinkName = launchInfo.sharedLinkName || info.sharedLinkName;
2603
2699
  }
2604
2700
 
2605
- // 3b. Try to extract GUIDs from Xyleme Cloud Player's internal data
2606
- // This is more reliable than API calls which may fail with 401
2607
- if (!info.guid || !info.versionGuid) {
2608
- var cloudPlayerData = extractFromXylemeCloudPlayer();
2609
- if (cloudPlayerData) {
2610
- info.guid = cloudPlayerData.guid || info.guid;
2611
- info.versionGuid = cloudPlayerData.versionGuid || info.versionGuid;
2612
- info.documentId = cloudPlayerData.documentId || info.documentId;
2613
- info.title = cloudPlayerData.title || info.title;
2614
- log('Updated course info from Cloud Player:', {
2615
- guid: info.guid,
2616
- versionGuid: info.versionGuid,
2617
- documentId: info.documentId
2618
- });
2619
- }
2701
+ // 3b. Always try to extract GUIDs from Xyleme Cloud Player's internal data.
2702
+ // Cloud Player data is authoritative \u2014 it has the canonical Bravais GUID.
2703
+ // This overrides any baked-in GUID which is just a random UUID from wrap time.
2704
+ var cloudPlayerData = extractFromXylemeCloudPlayer();
2705
+ if (cloudPlayerData) {
2706
+ if (cloudPlayerData.guid) info.guid = cloudPlayerData.guid;
2707
+ if (cloudPlayerData.versionGuid) info.versionGuid = cloudPlayerData.versionGuid;
2708
+ info.documentId = cloudPlayerData.documentId || info.documentId;
2709
+ info.title = cloudPlayerData.title || info.title;
2710
+ log('Updated course info from Cloud Player:', {
2711
+ guid: info.guid,
2712
+ versionGuid: info.versionGuid,
2713
+ documentId: info.documentId
2714
+ });
2620
2715
  }
2621
2716
 
2622
2717
  // 4. Try getCourseTitle() from preloadIntegrity.js
@@ -2950,7 +3045,7 @@ function generateLrsBridgeCode(options) {
2950
3045
 
2951
3046
  var statement = {
2952
3047
  id: generateUUID(),
2953
- actor: LRS.actor || extractActor(),
3048
+ actor: getActor(),
2954
3049
  verb: typeof verb === 'string' ? (VERBS[verb] || { id: verb, display: { 'en-US': verb } }) : verb,
2955
3050
  object: courseObj,
2956
3051
  timestamp: new Date().toISOString()
@@ -3372,7 +3467,7 @@ function generateLrsBridgeCode(options) {
3372
3467
  function buildStatementXyleme(verb, activityObject, result, additionalContext) {
3373
3468
  var statement = {
3374
3469
  id: generateUUID(),
3375
- actor: LRS.actor || extractActor(),
3470
+ actor: getActor(),
3376
3471
  verb: typeof verb === 'string' ? (VERBS[verb] || { id: verb, display: { 'en-US': verb } }) : verb,
3377
3472
  object: activityObject,
3378
3473
  timestamp: new Date().toISOString()
@@ -5101,6 +5196,15 @@ function generateLrsBridgeCode(options) {
5101
5196
  // Extract actor (sync first for immediate use)
5102
5197
  extractActor();
5103
5198
 
5199
+ // Check localStorage for actor set by another frame (e.g., skin overlay in a different HTML file)
5200
+ if (isAnonymousActor(LRS.actor)) {
5201
+ var sharedActor = loadSharedActor();
5202
+ if (sharedActor) {
5203
+ LRS.actor = sharedActor;
5204
+ log('Loaded actor from cross-frame storage at init:', sharedActor.name);
5205
+ }
5206
+ }
5207
+
5104
5208
  log('Bridge initialized in mode:', LRS.mode);
5105
5209
  log('Actor:', LRS.actor);
5106
5210
  log('Course:', LRS.courseInfo);
@@ -5238,12 +5342,18 @@ function generateLrsBridgeCode(options) {
5238
5342
 
5239
5343
  // Always-visible bridge summary (not gated by DEBUG)
5240
5344
  // This ensures diagnostics are available even in production builds
5345
+ // Shows current GUID (may be from Cloud Player, baked-in, or pending API override)
5346
+ var currentGuid = LRS.courseInfo ? LRS.courseInfo.guid : DOCUMENT_GUID;
5347
+ var currentVerGuid = LRS.courseInfo ? LRS.courseInfo.versionGuid : VERSION_GUID;
5348
+ var sharedToken = LRS.courseInfo ? LRS.courseInfo.sharedLinkToken : null;
5241
5349
  if (window.console && window.console.info) {
5242
5350
  console.info('[PA-LRS] Bridge v' + LRS.version + ': mode=' + LRS.mode +
5243
5351
  ', endpoint=' + (LRS_ENDPOINT ? LRS_ENDPOINT.substring(0, 30) + '...' : 'NONE') +
5244
5352
  ', proxy=' + (LRS_PROXY_ENDPOINT ? 'yes' : 'no') +
5245
- ', docGuid=' + (DOCUMENT_GUID || 'NONE') +
5246
- ', verGuid=' + (VERSION_GUID || 'NONE') +
5353
+ ', docGuid=' + (currentGuid || 'NONE') +
5354
+ (currentGuid !== DOCUMENT_GUID ? ' (overridden from baked: ' + DOCUMENT_GUID + ')' : '') +
5355
+ ', verGuid=' + (currentVerGuid || 'NONE') +
5356
+ ', sharedToken=' + (sharedToken || 'NONE') +
5247
5357
  ', actor=' + (LRS.actor ? (LRS.actor.name || 'anonymous') : 'none') +
5248
5358
  ', hasRefreshActor=' + (typeof LRS.refreshActor === 'function'));
5249
5359
  }
@@ -5314,35 +5424,30 @@ function generateLrsBridgeCode(options) {
5314
5424
  });
5315
5425
  }
5316
5426
 
5317
- if (!LRS.courseInfo.guid) {
5318
- // When we have a shared link token, use /api/shared/{token}/documents directly
5319
- // This endpoint does NOT require authentication and returns both document GUID and version GUID
5320
- if (sharedLinkToken) {
5321
- log('Have shared link token, fetching document data via shared API...');
5322
- // fetchDocumentMetadata will use /api/shared/{token}/documents when sharedLinkToken is present
5323
- fetchDocumentMetadata(null, function(docData) {
5324
- if (docData) {
5325
- updateCourseInfoFromApi(docData);
5326
- // Also update document name if available
5327
- if (docData.name) {
5328
- LRS.courseInfo.sharedLinkName = docData.name;
5329
- }
5330
- } else {
5331
- warn('Could not fetch document data from shared API - statements may fail aggregation');
5427
+ // ALWAYS fetch document metadata from the Bravais API.
5428
+ // Even if we have a baked-in GUID, we must override it with the canonical
5429
+ // Bravais GUID so our statements link to the same document as cloudplayer.
5430
+ // The baked-in GUID is a random UUID from wrap time; the Bravais GUID is
5431
+ // assigned at upload and is what Analytics uses for aggregation.
5432
+ if (sharedLinkToken) {
5433
+ log('Have shared link token, fetching document data via shared API...');
5434
+ fetchDocumentMetadata(null, function(docData) {
5435
+ if (docData) {
5436
+ updateCourseInfoFromApi(docData);
5437
+ if (docData.name) {
5438
+ LRS.courseInfo.sharedLinkName = docData.name;
5332
5439
  }
5333
- maybeEnrichAndReady();
5334
- });
5335
- } else if (documentId) {
5336
- // No shared link token, try with extracted document ID (requires auth)
5337
- log('No shared link token, fetching document data with ID:', documentId);
5338
- fetchDocDataAndReady(documentId);
5339
- } else {
5340
- // No identifiers available
5341
- warn('No shared link token or document ID - statements may fail aggregation');
5440
+ } else {
5441
+ warn('Could not fetch document data from shared API - statements may use baked-in GUID');
5442
+ }
5342
5443
  maybeEnrichAndReady();
5343
- }
5444
+ });
5445
+ } else if (documentId) {
5446
+ log('Fetching document data with ID:', documentId);
5447
+ fetchDocDataAndReady(documentId);
5344
5448
  } else {
5345
- // Already have GUID \u2014 still try to enrich packageName if missing
5449
+ // No identifiers available
5450
+ warn('No shared link token or document ID - statements will use baked-in GUID');
5346
5451
  maybeEnrichAndReady();
5347
5452
  }
5348
5453