@scalebun/react-native 1.10.6 → 1.11.0

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.
Files changed (75) hide show
  1. package/android/src/main/java/com/scalebun/replaysdk/tracking/InteractionTracker.kt +25 -24
  2. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +75 -0
  3. package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +12 -0
  4. package/dist/scalebun.full.js +653 -223
  5. package/dist/scalebun.slim.js +652 -222
  6. package/ios/Capture/InteractionTracker.swift +8 -4
  7. package/ios/Ota/ScaleBunOtaBridge.mm +6 -0
  8. package/ios/Ota/ScaleBunOtaModule.swift +67 -0
  9. package/lib/commonjs/analytics/EventTracker.js +5 -5
  10. package/lib/commonjs/analytics/automaticEvents.js +3 -2
  11. package/lib/commonjs/core/config/schema.js +34 -1
  12. package/lib/commonjs/core/constants/version.js +7 -2
  13. package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +80 -79
  14. package/lib/commonjs/features/journey/interactionProtocol.js +47 -0
  15. package/lib/commonjs/features/journey/uiState.js +94 -0
  16. package/lib/commonjs/features/ota/crypto/builtinVerifier.js +248 -0
  17. package/lib/commonjs/features/ota/crypto/loadEd25519.js +40 -0
  18. package/lib/commonjs/features/ota/crypto/loadSha512.js +40 -0
  19. package/lib/commonjs/features/ota/crypto/nativeVerifier.js +121 -0
  20. package/lib/commonjs/features/ota/signature.js +87 -27
  21. package/lib/commonjs/features/session/JourneyEventPipeline.js +6 -5
  22. package/lib/commonjs/features/session/SessionManager.js +37 -38
  23. package/lib/commonjs/metro/serializerCompose.js +32 -0
  24. package/lib/commonjs/public/ScaleBunFacade.js +74 -11
  25. package/lib/module/analytics/EventTracker.js +5 -5
  26. package/lib/module/analytics/automaticEvents.js +3 -2
  27. package/lib/module/core/config/schema.js +34 -1
  28. package/lib/module/core/constants/version.js +7 -2
  29. package/lib/module/features/journey/ScaleBunDebugRoot.js +80 -79
  30. package/lib/module/features/journey/interactionProtocol.js +38 -0
  31. package/lib/module/features/journey/uiState.js +86 -0
  32. package/lib/module/features/ota/crypto/builtinVerifier.js +240 -0
  33. package/lib/module/features/ota/crypto/loadEd25519.js +34 -0
  34. package/lib/module/features/ota/crypto/loadSha512.js +34 -0
  35. package/lib/module/features/ota/crypto/nativeVerifier.js +113 -0
  36. package/lib/module/features/ota/signature.js +87 -27
  37. package/lib/module/features/session/JourneyEventPipeline.js +6 -5
  38. package/lib/module/features/session/SessionManager.js +37 -38
  39. package/lib/module/metro/serializerCompose.js +32 -0
  40. package/lib/module/public/ScaleBunFacade.js +74 -11
  41. package/lib/typescript/analytics/EventTracker.d.ts +1 -1
  42. package/lib/typescript/analytics/automaticEvents.d.ts +3 -1
  43. package/lib/typescript/core/config/schema.d.ts +2 -0
  44. package/lib/typescript/core/constants/version.d.ts +7 -2
  45. package/lib/typescript/features/journey/interactionProtocol.d.ts +21 -0
  46. package/lib/typescript/features/journey/uiState.d.ts +53 -0
  47. package/lib/typescript/features/ota/OtaTypes.d.ts +50 -0
  48. package/lib/typescript/features/ota/crypto/builtinVerifier.d.ts +53 -0
  49. package/lib/typescript/features/ota/crypto/loadEd25519.d.ts +30 -0
  50. package/lib/typescript/features/ota/crypto/loadSha512.d.ts +15 -0
  51. package/lib/typescript/features/ota/crypto/nativeVerifier.d.ts +35 -0
  52. package/lib/typescript/features/ota/signature.d.ts +22 -7
  53. package/lib/typescript/features/session/JourneyEventPipeline.d.ts +1 -0
  54. package/lib/typescript/features/session/SessionManager.d.ts +15 -10
  55. package/lib/typescript/public/ScaleBunFacade.d.ts +35 -6
  56. package/lib/typescript/specs/NativeScaleBunOta.d.ts +23 -0
  57. package/package.json +19 -3
  58. package/src/analytics/EventTracker.ts +5 -5
  59. package/src/analytics/automaticEvents.ts +4 -0
  60. package/src/core/config/schema.ts +30 -3
  61. package/src/core/constants/version.ts +7 -2
  62. package/src/features/journey/ScaleBunDebugRoot.tsx +96 -75
  63. package/src/features/journey/interactionProtocol.ts +65 -0
  64. package/src/features/journey/uiState.ts +89 -0
  65. package/src/features/ota/OtaTypes.ts +51 -0
  66. package/src/features/ota/crypto/builtinVerifier.ts +257 -0
  67. package/src/features/ota/crypto/loadEd25519.ts +41 -0
  68. package/src/features/ota/crypto/loadSha512.ts +35 -0
  69. package/src/features/ota/crypto/nativeVerifier.ts +117 -0
  70. package/src/features/ota/signature.ts +108 -25
  71. package/src/features/session/JourneyEventPipeline.ts +7 -5
  72. package/src/features/session/SessionManager.ts +75 -38
  73. package/src/metro/serializerCompose.ts +38 -2
  74. package/src/public/ScaleBunFacade.ts +87 -13
  75. package/src/specs/NativeScaleBunOta.ts +24 -0
@@ -89,9 +89,9 @@ var init_internalLogger = __esm({
89
89
  }
90
90
  /** Set level from the public string name used in config (e.g. 'warn'). */
91
91
  setLevelByName(name) {
92
- const resolved = LEVEL_BY_NAME[name];
93
- if (resolved !== void 0) {
94
- this.level = resolved;
92
+ const resolved3 = LEVEL_BY_NAME[name];
93
+ if (resolved3 !== void 0) {
94
+ this.level = resolved3;
95
95
  }
96
96
  }
97
97
  /** Current numeric level — exposed for callers that branch on dev verbosity. */
@@ -605,7 +605,7 @@ var SDK_VERSION;
605
605
  var init_version = __esm({
606
606
  "lib/module/core/constants/version.js"() {
607
607
  "use strict";
608
- SDK_VERSION = "1.10.6";
608
+ SDK_VERSION = "1.11.0";
609
609
  }
610
610
  });
611
611
 
@@ -4567,22 +4567,23 @@ var init_JourneyEventPipeline = __esm({
4567
4567
  emit(type, opts) {
4568
4568
  try {
4569
4569
  const key = `${type}:${opts?.subtype ?? ""}`;
4570
- const now2 = Date.now();
4571
- if (key === this.lastEventKey && now2 - this.lastEventTs < this.config.dedupeWindowMs) {
4570
+ const receivedAt = Date.now();
4571
+ const occurredAt = opts?.timestamp ?? receivedAt;
4572
+ if (key === this.lastEventKey && receivedAt - this.lastEventTs < this.config.dedupeWindowMs) {
4572
4573
  const incomingConfidence = opts?.payload?.confidence;
4573
4574
  if (incomingConfidence === "high" && this.lastEventConfidence !== "high") {
4574
- this._replaceLastEvent(key, now2, opts);
4575
+ this._replaceLastEvent(key, receivedAt, opts);
4575
4576
  }
4576
4577
  return null;
4577
4578
  }
4578
4579
  this.lastEventKey = key;
4579
- this.lastEventTs = now2;
4580
+ this.lastEventTs = receivedAt;
4580
4581
  this.lastEventConfidence = opts?.payload?.confidence ?? null;
4581
4582
  const event = {
4582
4583
  eventId: generateEventId2(),
4583
4584
  sessionId: this.sessionId,
4584
4585
  journeyId: opts?.journeyId,
4585
- ts: now2,
4586
+ ts: occurredAt,
4586
4587
  type,
4587
4588
  subtype: opts?.subtype,
4588
4589
  severity: opts?.severity ?? inferSeverity(type),
@@ -5682,6 +5683,99 @@ var init_calibrationContext = __esm({
5682
5683
  }
5683
5684
  });
5684
5685
 
5686
+ // lib/module/analytics/automaticEvents.js
5687
+ function compactProperties(properties) {
5688
+ const out = {
5689
+ capture_source: "automatic"
5690
+ };
5691
+ for (const [key, value] of Object.entries(properties ?? {})) {
5692
+ if (value !== void 0) out[key] = value;
5693
+ }
5694
+ return out;
5695
+ }
5696
+ function emitAutomaticEvent(name, properties, timestamp) {
5697
+ const event = {
5698
+ name,
5699
+ properties: compactProperties(properties),
5700
+ timestamp
5701
+ };
5702
+ if (listeners.size === 0) {
5703
+ pending2.push(event);
5704
+ if (pending2.length > MAX_PENDING) pending2.shift();
5705
+ return;
5706
+ }
5707
+ for (const listener of listeners) {
5708
+ try {
5709
+ listener(event);
5710
+ } catch {
5711
+ }
5712
+ }
5713
+ }
5714
+ function subscribeAutomaticEvents(listener) {
5715
+ listeners.add(listener);
5716
+ if (pending2.length > 0) {
5717
+ const buffered = pending2.splice(0, pending2.length);
5718
+ for (const event of buffered) {
5719
+ try {
5720
+ listener(event);
5721
+ } catch {
5722
+ }
5723
+ }
5724
+ }
5725
+ return () => {
5726
+ listeners.delete(listener);
5727
+ };
5728
+ }
5729
+ var listeners, pending2, MAX_PENDING;
5730
+ var init_automaticEvents = __esm({
5731
+ "lib/module/analytics/automaticEvents.js"() {
5732
+ "use strict";
5733
+ listeners = /* @__PURE__ */ new Set();
5734
+ pending2 = [];
5735
+ MAX_PENDING = 50;
5736
+ }
5737
+ });
5738
+
5739
+ // lib/module/features/journey/interactionProtocol.js
5740
+ function generateInteractionId() {
5741
+ return `ixj-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
5742
+ }
5743
+ function nearestInteractionStart(starts, occurredAt, toleranceMs = 1500) {
5744
+ let best;
5745
+ let bestDelta = toleranceMs + 1;
5746
+ for (const start of starts) {
5747
+ const delta = Math.abs(start.occurredAt - occurredAt);
5748
+ if (delta < bestDelta) {
5749
+ best = start;
5750
+ bestDelta = delta;
5751
+ }
5752
+ }
5753
+ return bestDelta <= toleranceMs ? best : void 0;
5754
+ }
5755
+ function automaticInteractionProperties(payload, screenName, canonicalMirror) {
5756
+ return {
5757
+ gesture_type: payload.gestureType,
5758
+ screen_name: screenName,
5759
+ interaction_id: payload.interaction_id,
5760
+ interaction_protocol: payload.interaction_protocol,
5761
+ state_status: payload.state_status,
5762
+ ui: payload.ui,
5763
+ target_id: payload.target_id,
5764
+ normalized_x: payload.normalizedX,
5765
+ normalized_y: payload.normalizedY,
5766
+ direction: payload.direction,
5767
+ duration_ms: payload.durationMs,
5768
+ canonical_mirror: canonicalMirror || void 0
5769
+ };
5770
+ }
5771
+ var INTERACTION_PROTOCOL_VERSION;
5772
+ var init_interactionProtocol = __esm({
5773
+ "lib/module/features/journey/interactionProtocol.js"() {
5774
+ "use strict";
5775
+ INTERACTION_PROTOCOL_VERSION = 1;
5776
+ }
5777
+ });
5778
+
5685
5779
  // lib/module/features/session/SessionManager.js
5686
5780
  var SessionManager_exports = {};
5687
5781
  __export(SessionManager_exports, {
@@ -5690,7 +5784,7 @@ __export(SessionManager_exports, {
5690
5784
  function generateSessionId2() {
5691
5785
  return `ses-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`;
5692
5786
  }
5693
- var HEATMAP_MAX_INTERACTIONS_PER_WINDOW, _instance2, SessionManager;
5787
+ var _instance2, SessionManager;
5694
5788
  var init_SessionManager = __esm({
5695
5789
  "lib/module/features/session/SessionManager.js"() {
5696
5790
  "use strict";
@@ -5707,7 +5801,8 @@ var init_SessionManager = __esm({
5707
5801
  init_redaction();
5708
5802
  init_calibrationContext();
5709
5803
  init_device();
5710
- HEATMAP_MAX_INTERACTIONS_PER_WINDOW = 200;
5804
+ init_automaticEvents();
5805
+ init_interactionProtocol();
5711
5806
  _instance2 = null;
5712
5807
  SessionManager = class _SessionManager {
5713
5808
  desktopTransport = null;
@@ -5718,15 +5813,6 @@ var init_SessionManager = __esm({
5718
5813
  * lane even when no replay recording is active. Additive, opt-in (default off).
5719
5814
  */
5720
5815
  _captureInteractionHeatmap = false;
5721
- /**
5722
- * Sampling cap for the analytics-lane heatmap emission. Now that capture is
5723
- * ON by default, an unbounded one-event-per-gesture stream could materially
5724
- * inflate ingest volume. We cap emitted interactions per analytics-session
5725
- * window (finalize-scoped per foreground): the first N gestures define the
5726
- * hotspot shape; the long tail is dropped. Resets when the window changes.
5727
- */
5728
- _heatmapWindowSessionId = null;
5729
- _heatmapWindowCount = 0;
5730
5816
  session = null;
5731
5817
  active = false;
5732
5818
  timeoutTimer = null;
@@ -6207,18 +6293,11 @@ var init_SessionManager = __esm({
6207
6293
  * injected sensitive keys (e.g. a label) are stripped before transport.
6208
6294
  * - Never throws.
6209
6295
  */
6210
- _emitInteractionToAnalytics(gestureType, payload) {
6211
- if (!this._captureInteractionHeatmap) return;
6212
- if (this.active) return;
6296
+ _emitInteractionToAnalytics(gestureType, payload, occurredAt, screenName) {
6297
+ if (!this._captureInteractionHeatmap) return false;
6298
+ if (this.active) return false;
6213
6299
  const adapter = this._backendTransport;
6214
- if (!adapter) return;
6215
- const windowId = adapter.analyticsSessionId ?? "";
6216
- if (windowId !== this._heatmapWindowSessionId) {
6217
- this._heatmapWindowSessionId = windowId;
6218
- this._heatmapWindowCount = 0;
6219
- }
6220
- if (this._heatmapWindowCount >= HEATMAP_MAX_INTERACTIONS_PER_WINDOW) return;
6221
- this._heatmapWindowCount++;
6300
+ if (!adapter) return false;
6222
6301
  try {
6223
6302
  this._normalizeInteractionPayload(payload);
6224
6303
  const safePayload = redactBody(payload);
@@ -6226,16 +6305,18 @@ var init_SessionManager = __esm({
6226
6305
  eventId: generateEventId(),
6227
6306
  // sessionId is (re)stamped by the analytics lane at flush time.
6228
6307
  sessionId: adapter.analyticsSessionId ?? "",
6229
- ts: Date.now(),
6308
+ ts: occurredAt,
6230
6309
  type: "USER_ACTION",
6231
6310
  subtype: `gesture:${gestureType}`,
6232
- screen: this._lastKnownScreen ?? void 0,
6311
+ screen: screenName ?? this._lastKnownScreen ?? void 0,
6233
6312
  payload: safePayload,
6234
6313
  source: "user"
6235
6314
  };
6236
6315
  adapter.trackEvent(event);
6316
+ return true;
6237
6317
  } catch (err) {
6238
6318
  logger.error("[SessionManager] heatmap analytics emit failed:", err);
6319
+ return false;
6239
6320
  }
6240
6321
  }
6241
6322
  /**
@@ -6401,13 +6482,14 @@ var init_SessionManager = __esm({
6401
6482
  * Notify of a user interaction. Called by ScaleBunDebugRoot touch handlers.
6402
6483
  * Also triggers frame capture for desktop-initiated recordings.
6403
6484
  */
6404
- onUserAction(subtype, payload) {
6485
+ onUserAction(subtype, payload, context) {
6405
6486
  if (!this.active) return;
6406
6487
  this.emitEvent("USER_ACTION", {
6407
6488
  subtype,
6408
- screen: this._lastKnownScreen ?? void 0,
6489
+ screen: context?.screen ?? this._lastKnownScreen ?? void 0,
6409
6490
  payload,
6410
- source: "user"
6491
+ source: "user",
6492
+ timestamp: context?.timestamp
6411
6493
  });
6412
6494
  }
6413
6495
  /**
@@ -6415,7 +6497,8 @@ var init_SessionManager = __esm({
6415
6497
  * Emits a USER_ACTION event with gesture-specific subtype and payload.
6416
6498
  */
6417
6499
  onGestureDetected(gestureType, details) {
6418
- const dedupSig = `${gestureType}|${String(details?.x)}|${String(details?.y)}|${String(details?.endX)}|${String(details?.endY)}`;
6500
+ const suppliedInteractionId = details?.interactionId;
6501
+ const dedupSig = suppliedInteractionId ? `id:${suppliedInteractionId}` : `${gestureType}|${String(details?.x)}|${String(details?.y)}|${String(details?.endX)}|${String(details?.endY)}`;
6419
6502
  const nowTs = Date.now();
6420
6503
  if (dedupSig === this._lastGestureSig && nowTs - this._lastGestureTs <= _SessionManager.GESTURE_DEDUP_WINDOW_MS) {
6421
6504
  this._lastGestureTs = nowTs;
@@ -6423,7 +6506,15 @@ var init_SessionManager = __esm({
6423
6506
  }
6424
6507
  this._lastGestureSig = dedupSig;
6425
6508
  this._lastGestureTs = nowTs;
6509
+ const interactionId = suppliedInteractionId ?? generateInteractionId();
6510
+ const occurredAt = details?.occurredAt ?? Date.now();
6511
+ const stateStatus = details?.stateStatus ?? (details?.ui ? "captured_nonempty" : "not_captured");
6426
6512
  const payload = {
6513
+ interaction_id: interactionId,
6514
+ interaction_protocol: details?.interactionProtocol ?? INTERACTION_PROTOCOL_VERSION,
6515
+ state_status: stateStatus,
6516
+ ui: details?.ui,
6517
+ target_id: details?.targetId,
6427
6518
  gestureType,
6428
6519
  x: details?.x,
6429
6520
  y: details?.y,
@@ -6461,12 +6552,23 @@ var init_SessionManager = __esm({
6461
6552
  screenHeight: details?.screenHeight,
6462
6553
  platform: details?.platform
6463
6554
  };
6464
- this._emitInteractionToAnalytics(gestureType, {
6555
+ const analyticsReplayCarrier = this._emitInteractionToAnalytics(gestureType, {
6465
6556
  ...payload
6466
- });
6557
+ }, occurredAt, details?.screenName);
6558
+ const replayCarrier = !!this._backendTransport && (this.active || analyticsReplayCarrier);
6559
+ if (details?.emitAutomaticAnalytics) {
6560
+ try {
6561
+ const safePayload = redactBody(payload);
6562
+ emitAutomaticEvent("element_interacted", automaticInteractionProperties(safePayload, details?.screenName ?? this._lastKnownScreen ?? void 0, replayCarrier), occurredAt);
6563
+ } catch {
6564
+ }
6565
+ }
6467
6566
  if (!this.active) return;
6468
6567
  const target = typeof details?.target === "string" ? details.target.trim() : "";
6469
- this.onUserAction(target ? `gesture:${gestureType} \xB7 ${target}` : `gesture:${gestureType}`, payload);
6568
+ this.onUserAction(target ? `gesture:${gestureType} \xB7 ${target}` : `gesture:${gestureType}`, payload, {
6569
+ screen: details?.screenName,
6570
+ timestamp: occurredAt
6571
+ });
6470
6572
  }
6471
6573
  /**
6472
6574
  * Manual frame capture — triggered by Desktop "Capture Step" button.
@@ -7781,12 +7883,12 @@ var init_AppLaunchCollector = __esm({
7781
7883
  });
7782
7884
 
7783
7885
  // lib/module/features/performance/collectors/ScreenLoadCollector.js
7784
- var MAX_PENDING, ScreenLoadCollector;
7886
+ var MAX_PENDING2, ScreenLoadCollector;
7785
7887
  var init_ScreenLoadCollector = __esm({
7786
7888
  "lib/module/features/performance/collectors/ScreenLoadCollector.js"() {
7787
7889
  "use strict";
7788
7890
  init_models();
7789
- MAX_PENDING = 20;
7891
+ MAX_PENDING2 = 20;
7790
7892
  ScreenLoadCollector = class {
7791
7893
  pending = /* @__PURE__ */ new Map();
7792
7894
  lastScreen = null;
@@ -7801,7 +7903,7 @@ var init_ScreenLoadCollector = __esm({
7801
7903
  */
7802
7904
  markStart(screenName) {
7803
7905
  if (!this.config.screenLoad) return;
7804
- if (this.pending.size >= MAX_PENDING) {
7906
+ if (this.pending.size >= MAX_PENDING2) {
7805
7907
  const oldest = this.pending.keys().next().value;
7806
7908
  if (oldest) this.pending.delete(oldest);
7807
7909
  }
@@ -10933,58 +11035,6 @@ var init_batching = __esm({
10933
11035
  }
10934
11036
  });
10935
11037
 
10936
- // lib/module/analytics/automaticEvents.js
10937
- function compactProperties(properties) {
10938
- const out = {
10939
- capture_source: "automatic"
10940
- };
10941
- for (const [key, value] of Object.entries(properties ?? {})) {
10942
- if (value !== void 0) out[key] = value;
10943
- }
10944
- return out;
10945
- }
10946
- function emitAutomaticEvent(name, properties) {
10947
- const event = {
10948
- name,
10949
- properties: compactProperties(properties)
10950
- };
10951
- if (listeners.size === 0) {
10952
- pending3.push(event);
10953
- if (pending3.length > MAX_PENDING2) pending3.shift();
10954
- return;
10955
- }
10956
- for (const listener of listeners) {
10957
- try {
10958
- listener(event);
10959
- } catch {
10960
- }
10961
- }
10962
- }
10963
- function subscribeAutomaticEvents(listener) {
10964
- listeners.add(listener);
10965
- if (pending3.length > 0) {
10966
- const buffered = pending3.splice(0, pending3.length);
10967
- for (const event of buffered) {
10968
- try {
10969
- listener(event);
10970
- } catch {
10971
- }
10972
- }
10973
- }
10974
- return () => {
10975
- listeners.delete(listener);
10976
- };
10977
- }
10978
- var listeners, pending3, MAX_PENDING2;
10979
- var init_automaticEvents = __esm({
10980
- "lib/module/analytics/automaticEvents.js"() {
10981
- "use strict";
10982
- listeners = /* @__PURE__ */ new Set();
10983
- pending3 = [];
10984
- MAX_PENDING2 = 50;
10985
- }
10986
- });
10987
-
10988
11038
  // lib/module/analytics/EventTracker.js
10989
11039
  function uuid() {
10990
11040
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
@@ -11053,7 +11103,7 @@ var init_EventTracker = __esm({
11053
11103
  this.started = true;
11054
11104
  if (this.cfg.automaticEventTracking) {
11055
11105
  this.automaticEventsUnsubscribe = subscribeAutomaticEvents((event) => {
11056
- this.track(event.name, event.properties);
11106
+ this.track(event.name, event.properties, event.timestamp);
11057
11107
  });
11058
11108
  }
11059
11109
  if (this.cfg.autoLifecycleEvents) {
@@ -11157,9 +11207,9 @@ var init_EventTracker = __esm({
11157
11207
  }
11158
11208
  }
11159
11209
  // ─── tracking ────────────────────────────────────────────────────────────
11160
- track(eventName, properties) {
11210
+ track(eventName, properties, timestamp) {
11161
11211
  try {
11162
- this.enqueue(this.buildEnvelope(eventName, properties));
11212
+ this.enqueue(this.buildEnvelope(eventName, properties, timestamp));
11163
11213
  try {
11164
11214
  this.cfg.onEvent?.(eventName);
11165
11215
  } catch {
@@ -11263,7 +11313,7 @@ var init_EventTracker = __esm({
11263
11313
  };
11264
11314
  }
11265
11315
  // ─── internals ─────────────────────────────────────────────────────────────
11266
- buildEnvelope(eventName, properties) {
11316
+ buildEnvelope(eventName, properties, timestamp) {
11267
11317
  const ctx = this.cfg.context ?? {};
11268
11318
  let canonicalSessionId;
11269
11319
  try {
@@ -11274,7 +11324,7 @@ var init_EventTracker = __esm({
11274
11324
  const env = {
11275
11325
  event_id: uuid(),
11276
11326
  event_name: eventName,
11277
- event_time: Date.now(),
11327
+ event_time: timestamp ?? Date.now(),
11278
11328
  app_id: this.cfg.appId,
11279
11329
  platform: this.cfg.platform ?? resolveEventPlatform(),
11280
11330
  installation_id: this.installationId,
@@ -11611,10 +11661,249 @@ var init_deviceAttributes = __esm({
11611
11661
  }
11612
11662
  });
11613
11663
 
11664
+ // lib/module/features/ota/crypto/loadEd25519.js
11665
+ function loadEd25519() {
11666
+ try {
11667
+ const mod = require("@noble/ed25519");
11668
+ const ed = mod?.default ?? mod;
11669
+ if (typeof ed?.verifyAsync === "function" || typeof ed?.verify === "function") {
11670
+ return ed;
11671
+ }
11672
+ return null;
11673
+ } catch {
11674
+ return null;
11675
+ }
11676
+ }
11677
+ var init_loadEd25519 = __esm({
11678
+ "lib/module/features/ota/crypto/loadEd25519.js"() {
11679
+ "use strict";
11680
+ }
11681
+ });
11682
+
11683
+ // lib/module/features/ota/crypto/loadSha512.js
11684
+ function loadSha512() {
11685
+ try {
11686
+ const mod = require("@noble/hashes/sha2.js");
11687
+ const sha512 = mod?.sha512 ?? mod?.default?.sha512;
11688
+ return typeof sha512 === "function" ? sha512 : null;
11689
+ } catch {
11690
+ return null;
11691
+ }
11692
+ }
11693
+ var init_loadSha512 = __esm({
11694
+ "lib/module/features/ota/crypto/loadSha512.js"() {
11695
+ "use strict";
11696
+ }
11697
+ });
11698
+
11699
+ // lib/module/features/ota/crypto/builtinVerifier.js
11700
+ var builtinVerifier_exports = {};
11701
+ __export(builtinVerifier_exports, {
11702
+ _resetBuiltinVerifier: () => _resetBuiltinVerifier,
11703
+ decodeSignature: () => decodeSignature,
11704
+ getBuiltinVerifier: () => getBuiltinVerifier
11705
+ });
11706
+ function hexToBytes(hex) {
11707
+ const clean2 = hex.trim().toLowerCase().replace(/^0x/, "");
11708
+ if (clean2.length === 0 || clean2.length % 2 !== 0 || /[^0-9a-f]/.test(clean2)) return null;
11709
+ const out = new Uint8Array(clean2.length / 2);
11710
+ for (let i = 0; i < out.length; i++) {
11711
+ out[i] = parseInt(clean2.slice(i * 2, i * 2 + 2), 16);
11712
+ }
11713
+ return out;
11714
+ }
11715
+ function base64ToBytes2(b64) {
11716
+ const s = b64.trim().replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
11717
+ if (s.length === 0) return null;
11718
+ const out = [];
11719
+ let buffer = 0;
11720
+ let bits = 0;
11721
+ for (const ch of s) {
11722
+ const idx = B64_ALPHABET2.indexOf(ch);
11723
+ if (idx === -1) return null;
11724
+ buffer = buffer << 6 | idx;
11725
+ bits += 6;
11726
+ if (bits >= 8) {
11727
+ bits -= 8;
11728
+ out.push(buffer >> bits & 255);
11729
+ }
11730
+ }
11731
+ return Uint8Array.from(out);
11732
+ }
11733
+ function decodeSignature(sig) {
11734
+ const trimmed = sig.trim();
11735
+ if (/^(0x)?[0-9a-fA-F]{128}$/.test(trimmed)) {
11736
+ const hex2 = hexToBytes(trimmed);
11737
+ if (hex2 && hex2.length === ED25519_SIGNATURE_BYTES) return hex2;
11738
+ }
11739
+ const b64 = base64ToBytes2(trimmed);
11740
+ if (b64 && b64.length === ED25519_SIGNATURE_BYTES) return b64;
11741
+ const hex = hexToBytes(trimmed);
11742
+ if (hex && hex.length === ED25519_SIGNATURE_BYTES) return hex;
11743
+ return null;
11744
+ }
11745
+ function wireSha512(ed, sha512) {
11746
+ const concat2 = (parts) => {
11747
+ if (parts.length === 1) return parts[0];
11748
+ let total = 0;
11749
+ for (const p of parts) total += p.length;
11750
+ const out = new Uint8Array(total);
11751
+ let off = 0;
11752
+ for (const p of parts) {
11753
+ out.set(p, off);
11754
+ off += p.length;
11755
+ }
11756
+ return out;
11757
+ };
11758
+ const sync = (...msgs) => sha512(concat2(msgs));
11759
+ const asyncFn = async (...msgs) => sha512(concat2(msgs));
11760
+ let wired = false;
11761
+ try {
11762
+ if (ed.hashes && typeof ed.hashes === "object") {
11763
+ if (typeof ed.hashes.sha512 !== "function") ed.hashes.sha512 = sync;
11764
+ if (typeof ed.hashes.sha512Async !== "function") ed.hashes.sha512Async = asyncFn;
11765
+ wired = typeof ed.hashes.sha512 === "function";
11766
+ }
11767
+ } catch {
11768
+ }
11769
+ try {
11770
+ if (ed.etc && typeof ed.etc === "object") {
11771
+ if (typeof ed.etc.sha512Sync !== "function") ed.etc.sha512Sync = sync;
11772
+ if (typeof ed.etc.sha512Async !== "function") ed.etc.sha512Async = asyncFn;
11773
+ wired = wired || typeof ed.etc.sha512Sync === "function";
11774
+ }
11775
+ } catch {
11776
+ }
11777
+ return wired;
11778
+ }
11779
+ function getBuiltinVerifier() {
11780
+ if (resolved !== void 0) return resolved;
11781
+ const ed = loadEd25519();
11782
+ const sha512 = loadSha512();
11783
+ if (!ed || !sha512) {
11784
+ resolved = null;
11785
+ return null;
11786
+ }
11787
+ if (!wireSha512(ed, sha512)) {
11788
+ logger.error("[OTA] @noble/ed25519 is installed but its SHA-512 provider could not be wired (unrecognised version shape). Supply `ota.verifySignature` yourself, or pin a @noble/ed25519 version this SDK supports.");
11789
+ resolved = null;
11790
+ return null;
11791
+ }
11792
+ resolved = async ({
11793
+ messageHex,
11794
+ signature,
11795
+ publicKey
11796
+ }) => {
11797
+ const message = hexToBytes(messageHex);
11798
+ const pub = hexToBytes(publicKey);
11799
+ const sig = decodeSignature(signature);
11800
+ if (!message) {
11801
+ logger.error("[OTA] Bundle SHA-256 is not valid hex \u2014 refusing to stage.");
11802
+ return false;
11803
+ }
11804
+ if (!pub || pub.length !== ED25519_PUBLIC_KEY_BYTES) {
11805
+ logger.error(`[OTA] ota.publicSigningKey must be ${ED25519_PUBLIC_KEY_BYTES} bytes of hex (${ED25519_PUBLIC_KEY_BYTES * 2} characters) \u2014 got ${pub ? `${pub.length} bytes` : "a value that is not hex"}.`);
11806
+ return false;
11807
+ }
11808
+ if (!sig) {
11809
+ logger.error("[OTA] Bundle signature is not a 64-byte hex/base64 value.");
11810
+ return false;
11811
+ }
11812
+ try {
11813
+ if (typeof ed.verify === "function") return ed.verify(sig, message, pub);
11814
+ if (typeof ed.verifyAsync === "function") return await ed.verifyAsync(sig, message, pub);
11815
+ return false;
11816
+ } catch {
11817
+ return false;
11818
+ }
11819
+ };
11820
+ __DEV__ && logger.debug("[OTA] Using built-in ed25519 verifier (@noble/ed25519 detected).");
11821
+ return resolved;
11822
+ }
11823
+ function _resetBuiltinVerifier() {
11824
+ resolved = void 0;
11825
+ }
11826
+ var ED25519_SIGNATURE_BYTES, ED25519_PUBLIC_KEY_BYTES, B64_ALPHABET2, resolved;
11827
+ var init_builtinVerifier = __esm({
11828
+ "lib/module/features/ota/crypto/builtinVerifier.js"() {
11829
+ "use strict";
11830
+ init_loadEd25519();
11831
+ init_loadSha512();
11832
+ init_internalLogger();
11833
+ ED25519_SIGNATURE_BYTES = 64;
11834
+ ED25519_PUBLIC_KEY_BYTES = 32;
11835
+ B64_ALPHABET2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
11836
+ }
11837
+ });
11838
+
11839
+ // lib/module/features/ota/crypto/nativeVerifier.js
11840
+ function bytesToHex(bytes) {
11841
+ let out = "";
11842
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
11843
+ return out;
11844
+ }
11845
+ function normalizeHex(value, expectedChars) {
11846
+ const clean2 = value.trim().toLowerCase().replace(/^0x/, "");
11847
+ if (clean2.length !== expectedChars || /[^0-9a-f]/.test(clean2)) return null;
11848
+ return clean2;
11849
+ }
11850
+ function getNativeVerifier() {
11851
+ if (resolved2 !== void 0) return resolved2;
11852
+ const native = NativeScaleBunOta_default;
11853
+ if (!native || typeof native.verifyEd25519 !== "function") {
11854
+ resolved2 = null;
11855
+ return null;
11856
+ }
11857
+ resolved2 = async ({
11858
+ messageHex,
11859
+ signature,
11860
+ publicKey
11861
+ }) => {
11862
+ const message = normalizeHex(messageHex, 64);
11863
+ const key = normalizeHex(publicKey, ED25519_PUBLIC_KEY_HEX_CHARS);
11864
+ const sigBytes = decodeSignature(signature);
11865
+ if (!message || !key || !sigBytes) {
11866
+ logger.error("[OTA] Signature inputs malformed (bundle hash, signature, or public key) \u2014 refusing to stage.");
11867
+ return false;
11868
+ }
11869
+ let verdict;
11870
+ try {
11871
+ verdict = await native.verifyEd25519(message, bytesToHex(sigBytes), key);
11872
+ } catch (err) {
11873
+ logger.warn(`[OTA] Native ed25519 verify failed to run: ${err?.message ?? err}`);
11874
+ verdict = "unavailable";
11875
+ }
11876
+ if (verdict === "valid") return true;
11877
+ if (verdict === "invalid") return false;
11878
+ const builtin = getBuiltinVerifier();
11879
+ if (builtin) {
11880
+ return builtin({
11881
+ messageHex,
11882
+ signature,
11883
+ publicKey
11884
+ });
11885
+ }
11886
+ throw new Error("ed25519 unavailable: this OS has no native implementation (Android < 13) and the optional peers @noble/ed25519 + @noble/hashes are not installed.");
11887
+ };
11888
+ __DEV__ && logger.debug("[OTA] Using native ed25519 verifier (platform crypto).");
11889
+ return resolved2;
11890
+ }
11891
+ var ED25519_PUBLIC_KEY_HEX_CHARS, resolved2;
11892
+ var init_nativeVerifier = __esm({
11893
+ "lib/module/features/ota/crypto/nativeVerifier.js"() {
11894
+ "use strict";
11895
+ init_NativeScaleBunOta();
11896
+ init_internalLogger();
11897
+ init_builtinVerifier();
11898
+ ED25519_PUBLIC_KEY_HEX_CHARS = 64;
11899
+ }
11900
+ });
11901
+
11614
11902
  // lib/module/features/ota/signature.js
11615
11903
  async function verifyBundleSignature(bundleSha256, signature, config) {
11616
- const publicKey = config?.publicKey;
11617
- if (!publicKey) {
11904
+ const rawKey = config?.publicKey;
11905
+ const keys = (Array.isArray(rawKey) ? rawKey : rawKey ? [rawKey] : []).filter((k) => typeof k === "string" && k.trim().length > 0);
11906
+ if (rawKey === void 0 || rawKey === "" || rawKey === null) {
11618
11907
  if (!warnedNotConfigured) {
11619
11908
  warnedNotConfigured = true;
11620
11909
  logger.warn("[OTA] No signing public key configured \u2014 bundles are accepted on SHA-256 integrity alone. Configure `ota.publicSigningKey` to enforce authenticity.");
@@ -11624,6 +11913,13 @@ async function verifyBundleSignature(bundleSha256, signature, config) {
11624
11913
  reason: "not_configured"
11625
11914
  };
11626
11915
  }
11916
+ if (keys.length === 0) {
11917
+ logger.error("[OTA] Signing key config resolves to zero usable keys (empty array or blank strings) \u2014 refusing to stage. Remove the config to disable signing, or pin at least one real key.");
11918
+ return {
11919
+ ok: false,
11920
+ reason: "no_keys"
11921
+ };
11922
+ }
11627
11923
  if (!signature) {
11628
11924
  logger.error("[OTA] Bundle has no signature but a signing key is configured \u2014 refusing to stage.");
11629
11925
  return {
@@ -11631,43 +11927,52 @@ async function verifyBundleSignature(bundleSha256, signature, config) {
11631
11927
  reason: "missing_signature"
11632
11928
  };
11633
11929
  }
11634
- if (typeof config?.verifier !== "function") {
11635
- logger.error("[OTA] A signing key is configured but no signature verifier is available \u2014 refusing to stage. Provide `ota.verifySignature` so signatures can be checked.");
11930
+ const verifier = typeof config?.verifier === "function" ? config.verifier : getNativeVerifier() ?? getBuiltinVerifier();
11931
+ if (!verifier) {
11932
+ logger.error("[OTA] A signing key is configured but no signature verifier is available \u2014 refusing to stage. Either install the optional peers `@noble/ed25519` and `@noble/hashes` (the SDK then verifies with no further code), or provide your own `ota.verifySignature`.");
11636
11933
  return {
11637
11934
  ok: false,
11638
11935
  reason: "no_verifier"
11639
11936
  };
11640
11937
  }
11641
- try {
11642
- const valid = await config.verifier({
11643
- messageHex: bundleSha256,
11644
- signature,
11645
- publicKey
11646
- });
11647
- if (!valid) {
11648
- logger.error("[OTA] Bundle signature is INVALID \u2014 refusing to stage.");
11649
- return {
11650
- ok: false,
11651
- reason: "invalid_signature"
11938
+ let sawThrow = false;
11939
+ let sawCompletion = false;
11940
+ for (const key of keys) {
11941
+ try {
11942
+ const valid = await verifier({
11943
+ messageHex: bundleSha256,
11944
+ signature,
11945
+ publicKey: key
11946
+ });
11947
+ sawCompletion = true;
11948
+ if (valid) return {
11949
+ ok: true,
11950
+ reason: "verified"
11652
11951
  };
11952
+ } catch (err) {
11953
+ sawThrow = true;
11954
+ logger.error(`[OTA] Signature verifier threw: ${err?.message ?? err}`);
11653
11955
  }
11654
- return {
11655
- ok: true,
11656
- reason: "verified"
11657
- };
11658
- } catch (err) {
11659
- logger.error(`[OTA] Signature verifier threw: ${err?.message ?? err}`);
11956
+ }
11957
+ if (sawThrow && !sawCompletion) {
11660
11958
  return {
11661
11959
  ok: false,
11662
11960
  reason: "verifier_threw"
11663
11961
  };
11664
11962
  }
11963
+ logger.error(keys.length > 1 ? `[OTA] Bundle signature is INVALID under all ${keys.length} pinned keys \u2014 refusing to stage.` : "[OTA] Bundle signature is INVALID \u2014 refusing to stage.");
11964
+ return {
11965
+ ok: false,
11966
+ reason: "invalid_signature"
11967
+ };
11665
11968
  }
11666
11969
  var warnedNotConfigured;
11667
11970
  var init_signature = __esm({
11668
11971
  "lib/module/features/ota/signature.js"() {
11669
11972
  "use strict";
11670
11973
  init_internalLogger();
11974
+ init_builtinVerifier();
11975
+ init_nativeVerifier();
11671
11976
  warnedNotConfigured = false;
11672
11977
  }
11673
11978
  });
@@ -12487,6 +12792,42 @@ var init_OtaOrchestrator = __esm({
12487
12792
  }
12488
12793
  });
12489
12794
 
12795
+ // lib/module/features/journey/uiState.js
12796
+ var uiState_exports = {};
12797
+ __export(uiState_exports, {
12798
+ clearUiState: () => clearUiState,
12799
+ setUiState: () => setUiState,
12800
+ uiStateSignature: () => uiStateSignature
12801
+ });
12802
+ function setUiState(name, value) {
12803
+ const n = clean(name);
12804
+ const v = clean(value);
12805
+ if (n && v) declared.set(n, v);
12806
+ }
12807
+ function clearUiState(name) {
12808
+ if (name === void 0) declared.clear();
12809
+ else declared.delete(clean(name));
12810
+ }
12811
+ function uiStateSignature() {
12812
+ if (!declared.size) return void 0;
12813
+ const pairs = [...declared.keys()].sort().map((k) => `${k}:${declared.get(k)}`);
12814
+ let signature = "";
12815
+ for (const pair of pairs) {
12816
+ const next = signature ? `${signature};${pair}` : pair;
12817
+ if (next.length > 96) break;
12818
+ signature = next;
12819
+ }
12820
+ return signature || void 0;
12821
+ }
12822
+ var declared, clean;
12823
+ var init_uiState = __esm({
12824
+ "lib/module/features/journey/uiState.js"() {
12825
+ "use strict";
12826
+ declared = /* @__PURE__ */ new Map();
12827
+ clean = (s) => typeof s === "string" ? s.replace(/[;:|]/g, "").trim().slice(0, 32) : "";
12828
+ }
12829
+ });
12830
+
12490
12831
  // lib/module/features/engage/gestureTriggerDetector.js
12491
12832
  var gestureTriggerDetector_exports = {};
12492
12833
  __export(gestureTriggerDetector_exports, {
@@ -14404,6 +14745,14 @@ var SHAPE = {
14404
14745
  kind: "str",
14405
14746
  opt: true
14406
14747
  },
14748
+ // Rotation support: several pinned keys, any of which may verify a
14749
+ // bundle. Ship a build pinning [old, new], re-sign server-side with
14750
+ // new, drop old next release — no emergency store submission when a
14751
+ // key must be replaced. Merged with publicSigningKey by the facade.
14752
+ publicSigningKeys: {
14753
+ kind: "strArr",
14754
+ opt: true
14755
+ },
14407
14756
  mandatoryBlocksUi: bool(false)
14408
14757
  }
14409
14758
  }
@@ -14439,7 +14788,7 @@ function parseField(field, input, path, issues) {
14439
14788
  value: field.def,
14440
14789
  present: true
14441
14790
  };
14442
- return fail(field.kind === "bool" ? "Expected boolean, received undefined" : field.kind === "num" ? "Expected number, received undefined" : field.kind === "str" ? "Expected string, received undefined" : field.kind === "enum" ? "Invalid enum value. Expected " + field.values.map((v) => `'${v}'`).join(" | ") : "Expected object, received undefined");
14791
+ return fail(field.kind === "bool" ? "Expected boolean, received undefined" : field.kind === "num" ? "Expected number, received undefined" : field.kind === "str" ? "Expected string, received undefined" : field.kind === "strArr" ? "Expected array, received undefined" : field.kind === "enum" ? "Invalid enum value. Expected " + field.values.map((v) => `'${v}'`).join(" | ") : "Expected object, received undefined");
14443
14792
  }
14444
14793
  switch (field.kind) {
14445
14794
  case "any":
@@ -14465,6 +14814,27 @@ function parseField(field, input, path, issues) {
14465
14814
  value: input,
14466
14815
  present: true
14467
14816
  };
14817
+ case "strArr": {
14818
+ if (!Array.isArray(input)) return fail(`Expected array, received ${typeofName2(input)}`);
14819
+ let allOk = true;
14820
+ for (let i = 0; i < input.length; i++) {
14821
+ if (typeof input[i] !== "string") {
14822
+ issues.push({
14823
+ path: [...path, i],
14824
+ message: `Expected string, received ${typeofName2(input[i])}`
14825
+ });
14826
+ allOk = false;
14827
+ }
14828
+ }
14829
+ return allOk ? {
14830
+ ok: true,
14831
+ value: [...input],
14832
+ present: true
14833
+ } : {
14834
+ ok: false,
14835
+ present: true
14836
+ };
14837
+ }
14468
14838
  case "num":
14469
14839
  if (typeof input !== "number" || Number.isNaN(input)) {
14470
14840
  return fail(`Expected number, received ${typeofName2(input)}`);
@@ -15355,14 +15725,14 @@ function installRejectionHandler(onRejection, deps) {
15355
15725
  allRejections: true,
15356
15726
  onUnhandled: (id, error) => {
15357
15727
  const err = toError(error);
15358
- pending2.set(id, err);
15728
+ pending3.set(id, err);
15359
15729
  onRejection(err);
15360
15730
  if (isDev) {
15361
15731
  console.warn(`Possible unhandled promise rejection (id: ${id}):`, err?.message ?? err);
15362
15732
  }
15363
15733
  },
15364
15734
  onHandled: (id) => {
15365
- pending2.delete(id);
15735
+ pending3.delete(id);
15366
15736
  if (isDev) {
15367
15737
  console.warn(`Promise rejection handled late (id: ${id}) \u2014 it was already reported.`);
15368
15738
  }
@@ -15372,7 +15742,7 @@ function installRejectionHandler(onRejection, deps) {
15372
15742
  return true;
15373
15743
  }) ?? false;
15374
15744
  }
15375
- var pending2 = /* @__PURE__ */ new Map();
15745
+ var pending3 = /* @__PURE__ */ new Map();
15376
15746
 
15377
15747
  // lib/module/features/crash/CrashFeature.js
15378
15748
  init_CrashReporter();
@@ -15668,16 +16038,16 @@ function base64ToBytes(base64) {
15668
16038
  return decodeManual(base64);
15669
16039
  }
15670
16040
  function decodeManual(base64) {
15671
- const clean = base64.replace(/[^A-Za-z0-9+/=]/g, "");
15672
- const pad = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
15673
- const outLen = Math.floor(clean.length * 3 / 4) - pad;
16041
+ const clean2 = base64.replace(/[^A-Za-z0-9+/=]/g, "");
16042
+ const pad = clean2.endsWith("==") ? 2 : clean2.endsWith("=") ? 1 : 0;
16043
+ const outLen = Math.floor(clean2.length * 3 / 4) - pad;
15674
16044
  const out = new Uint8Array(outLen);
15675
16045
  let p = 0;
15676
- for (let i = 0; i < clean.length; i += 4) {
15677
- const c0 = B64_ALPHABET.indexOf(clean[i]);
15678
- const c1 = B64_ALPHABET.indexOf(clean[i + 1]);
15679
- const c2 = B64_ALPHABET.indexOf(clean[i + 2]);
15680
- const c3 = B64_ALPHABET.indexOf(clean[i + 3]);
16046
+ for (let i = 0; i < clean2.length; i += 4) {
16047
+ const c0 = B64_ALPHABET.indexOf(clean2[i]);
16048
+ const c1 = B64_ALPHABET.indexOf(clean2[i + 1]);
16049
+ const c2 = B64_ALPHABET.indexOf(clean2[i + 2]);
16050
+ const c3 = B64_ALPHABET.indexOf(clean2[i + 3]);
15681
16051
  const n = c0 << 18 | c1 << 12 | (c2 & 63) << 6 | c3 & 63;
15682
16052
  if (p < outLen) out[p++] = n >> 16 & 255;
15683
16053
  if (p < outLen) out[p++] = n >> 8 & 255;
@@ -19865,12 +20235,17 @@ var ScaleBunFacade = class {
19865
20235
  /**
19866
20236
  * Boot the OTA orchestrator when the init config asks for it.
19867
20237
  *
19868
- * `publicSigningKey` is honoured here so signature enforcement is reachable
19869
- * from configuration alone. The SDK ships no ed25519 implementation, so a
19870
- * host that pins a key must also supply `ota.verifySignature`; pinning a key
19871
- * without a verifier is fail-CLOSED by design (signature.ts) an update
19872
- * that cannot be verified is not installed. We say that out loud rather than
19873
- * letting the app discover it as a silent no-update condition.
20238
+ * `publicSigningKey` / `publicSigningKeys` are honoured here so signature
20239
+ * enforcement is reachable from configuration alone. The list form exists
20240
+ * for key ROTATION: a build pinning [old, new] keeps verifying while the
20241
+ * server moves to the new key, so replacing a key never needs an emergency
20242
+ * store release. Both fields merge (deduplicated) into one pinned set.
20243
+ *
20244
+ * Verification comes from `ota.verifySignature` when supplied, else the
20245
+ * built-in @noble-based verifier (optional peers). Pinning keys with
20246
+ * NEITHER available is fail-CLOSED by design (signature.ts) — an update
20247
+ * that cannot be verified is not installed. We say that out loud rather
20248
+ * than letting the app discover it as a silent no-update condition.
19874
20249
  */
19875
20250
  _maybeStartOta(rawConfig) {
19876
20251
  const ota = rawConfig?.ota;
@@ -19879,15 +20254,25 @@ var ScaleBunFacade = class {
19879
20254
  const {
19880
20255
  otaOrchestrator: otaOrchestrator2
19881
20256
  } = (init_OtaOrchestrator(), __toCommonJS(OtaOrchestrator_exports));
19882
- const publicKey = ota.publicSigningKey;
20257
+ const rawSingle = ota.publicSigningKey;
20258
+ const rawList = ota.publicSigningKeys;
20259
+ const publicKeys = Array.from(new Set([...typeof rawSingle === "string" && rawSingle ? [rawSingle] : [], ...Array.isArray(rawList) ? rawList : []].filter((k) => typeof k === "string" && k.trim().length > 0)));
20260
+ const signingRequested = publicKeys.length > 0 || Array.isArray(rawList);
19883
20261
  const verifier = ota.verifySignature;
19884
- if (publicKey && typeof verifier !== "function") {
19885
- logger.warn("[ScaleBun] ota.publicSigningKey is set but ota.verifySignature is not a function. Signature checking is fail-closed: updates will be REJECTED until a verifier is supplied.");
20262
+ if (signingRequested && typeof verifier !== "function") {
20263
+ const {
20264
+ getBuiltinVerifier: getBuiltinVerifier2
20265
+ } = (init_builtinVerifier(), __toCommonJS(builtinVerifier_exports));
20266
+ if (!getBuiltinVerifier2()) {
20267
+ logger.warn("[ScaleBun] A signing key is pinned but no signature verifier is available. Signature checking is fail-closed: updates will be REJECTED until one exists. Install the optional peers `@noble/ed25519` + `@noble/hashes` (no further code needed), or supply `ota.verifySignature`.");
20268
+ }
19886
20269
  }
19887
20270
  otaOrchestrator2.init({
19888
- ...publicKey ? {
20271
+ ...signingRequested ? {
19889
20272
  signature: {
19890
- publicKey,
20273
+ // Single key stays a plain string — the shape every
20274
+ // existing consumer and test already handles.
20275
+ publicKey: publicKeys.length === 1 ? publicKeys[0] : publicKeys,
19891
20276
  verifier
19892
20277
  }
19893
20278
  } : {},
@@ -20099,6 +20484,46 @@ var ScaleBunFacade = class {
20099
20484
  };
20100
20485
  }
20101
20486
  /** Convenience: emit a Phase 1 purchase event (revenue + currency + transaction_id). */
20487
+ /**
20488
+ * Declare which UI state the user is looking at, so taps are attributed to the surface they
20489
+ * happened on rather than averaged across every variant of the screen.
20490
+ *
20491
+ * ScaleBun.setUiState('filter-sheet', 'open');
20492
+ * ScaleBun.setUiState('checkout-step', 'payment');
20493
+ *
20494
+ * Call it when the state CHANGES — the value is read at tap time, so it only has to be right by
20495
+ * the time the next tap lands. On React Native this is the ONLY source of UI state: unlike the web,
20496
+ * there is no queryable accessibility tree that says a sheet is up, and guessing would produce a
20497
+ * state key that is right on some apps and silently wrong on others.
20498
+ *
20499
+ * Names and values are identifiers, not content: they become query keys in the dashboard, so a
20500
+ * person's name or a cart total does not belong in one. `;`, `:` and `|` are stripped and both
20501
+ * halves are capped at 32 characters.
20502
+ */
20503
+ setUiState(name, value) {
20504
+ try {
20505
+ const {
20506
+ setUiState: setUiState2
20507
+ } = (init_uiState(), __toCommonJS(uiState_exports));
20508
+ setUiState2(name, value);
20509
+ } catch {
20510
+ }
20511
+ }
20512
+ /**
20513
+ * Stop reporting a UI state dimension — or, with no argument, all of them.
20514
+ *
20515
+ * Clear on screen unmount. A declaration left behind follows the user onto a surface where it means
20516
+ * nothing, and every tap there is filed under a state that was not on screen.
20517
+ */
20518
+ clearUiState(name) {
20519
+ try {
20520
+ const {
20521
+ clearUiState: clearUiState2
20522
+ } = (init_uiState(), __toCommonJS(uiState_exports));
20523
+ clearUiState2(name);
20524
+ } catch {
20525
+ }
20526
+ }
20102
20527
  trackPurchase(input) {
20103
20528
  noThrow(() => this._eventTracker?.trackPurchase(input));
20104
20529
  }
@@ -23722,11 +24147,11 @@ var normalizedWheelValue = (value) => value.toLowerCase().replace(/\s+/g, " ").t
23722
24147
  function resolveWheelOutcome(segments, source) {
23723
24148
  const identity = wheelOutcomeIdentity(source);
23724
24149
  const index = findWheelOutcomeIndex(segments, identity);
23725
- const resolved = index >= 0;
24150
+ const resolved3 = index >= 0;
23726
24151
  return {
23727
- index: resolved ? index : 0,
23728
- label: identity.resultLabel || (resolved ? segments[index]?.fullLabel ?? "" : ""),
23729
- resolved
24152
+ index: resolved3 ? index : 0,
24153
+ label: identity.resultLabel || (resolved3 ? segments[index]?.fullLabel ?? "" : ""),
24154
+ resolved: resolved3
23730
24155
  };
23731
24156
  }
23732
24157
  function SpinWheelInApp(props) {
@@ -32585,8 +33010,8 @@ function EngagePromptProvider({
32585
33010
  __DEV__ && logger.debug("[Engage] no eligible in-app message to render");
32586
33011
  return false;
32587
33012
  }
32588
- const resolved = selectInAppVariant(message, getDeviceId());
32589
- activeInAppVariantRef.current = message.experiment?.enabled ? resolved : null;
33013
+ const resolved3 = selectInAppVariant(message, getDeviceId());
33014
+ activeInAppVariantRef.current = message.experiment?.enabled ? resolved3 : null;
32590
33015
  store.recordImpression(message.id, Date.now(), message.revision);
32591
33016
  setActiveInApp(message);
32592
33017
  return true;
@@ -32826,7 +33251,6 @@ function useEngagePrompt() {
32826
33251
  // lib/module/features/journey/ScaleBunDebugRoot.js
32827
33252
  var import_react9 = __toESM(require("react"));
32828
33253
  var import_react_native43 = require("react-native");
32829
- init_automaticEvents();
32830
33254
 
32831
33255
  // lib/module/features/journey/touchTarget.js
32832
33256
  var MAX_DEPTH = 12;
@@ -32898,6 +33322,7 @@ function describeTouchTarget(t) {
32898
33322
 
32899
33323
  // lib/module/features/journey/ScaleBunDebugRoot.js
32900
33324
  init_device();
33325
+ init_interactionProtocol();
32901
33326
  var import_jsx_runtime8 = require("react/jsx-runtime");
32902
33327
  var _bootstrap = null;
32903
33328
  function getBootstrap() {
@@ -33087,7 +33512,9 @@ function ScaleBunDebugRoot({
33087
33512
  }, [navigationRef]);
33088
33513
  const lastNativeTouchTsRef = (0, import_react9.useRef)(0);
33089
33514
  const nativeTrackingConfirmedRef = (0, import_react9.useRef)(false);
33090
- const pendingJsEmitRef = (0, import_react9.useRef)(null);
33515
+ const touchStartRef = (0, import_react9.useRef)(null);
33516
+ const interactionStartsRef = (0, import_react9.useRef)([]);
33517
+ const pendingJsEmitsRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
33091
33518
  (0, import_react9.useEffect)(() => {
33092
33519
  let subscription = null;
33093
33520
  try {
@@ -33098,9 +33525,12 @@ function ScaleBunDebugRoot({
33098
33525
  try {
33099
33526
  lastNativeTouchTsRef.current = Date.now();
33100
33527
  nativeTrackingConfirmedRef.current = true;
33101
- if (pendingJsEmitRef.current !== null) {
33102
- clearTimeout(pendingJsEmitRef.current);
33103
- pendingJsEmitRef.current = null;
33528
+ const nativeOccurredAt = typeof event.occurredAt === "number" ? event.occurredAt : (typeof event.timestamp === "number" ? event.timestamp : Date.now()) - (typeof event.durationMs === "number" ? event.durationMs : 0);
33529
+ const start = nearestInteractionStart(interactionStartsRef.current, nativeOccurredAt);
33530
+ if (start) {
33531
+ const pending4 = pendingJsEmitsRef.current.get(start.interactionId);
33532
+ if (pending4 !== void 0) clearTimeout(pending4);
33533
+ pendingJsEmitsRef.current.delete(start.interactionId);
33104
33534
  }
33105
33535
  const {
33106
33536
  SessionManager: SessionManager2
@@ -33119,6 +33549,15 @@ function ScaleBunDebugRoot({
33119
33549
  } catch {
33120
33550
  }
33121
33551
  sm.onGestureDetected(event.gestureType || "tap", {
33552
+ interactionId: event.interactionId || start?.interactionId || generateInteractionId(),
33553
+ interactionProtocol: event.interactionProtocol || INTERACTION_PROTOCOL_VERSION,
33554
+ occurredAt: nativeOccurredAt,
33555
+ ui: start?.ui,
33556
+ stateStatus: start?.stateStatus ?? "not_captured",
33557
+ target: start?.target,
33558
+ targetId: start?.targetId,
33559
+ screenName: start?.screenName,
33560
+ emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
33122
33561
  x: event.rawX,
33123
33562
  y: event.rawY,
33124
33563
  // Use the actual native end coords when present, not the down
@@ -33151,9 +33590,10 @@ function ScaleBunDebugRoot({
33151
33590
  subscription?.remove();
33152
33591
  } catch {
33153
33592
  }
33593
+ for (const timer of pendingJsEmitsRef.current.values()) clearTimeout(timer);
33594
+ pendingJsEmitsRef.current.clear();
33154
33595
  };
33155
- }, []);
33156
- const touchStartRef = (0, import_react9.useRef)(null);
33596
+ }, [captureAutomaticInteractions]);
33157
33597
  const rootRectRef = (0, import_react9.useRef)(null);
33158
33598
  const measureRoot = () => {
33159
33599
  try {
@@ -33185,11 +33625,39 @@ function ScaleBunDebugRoot({
33185
33625
  const handleTouchStart = (e) => {
33186
33626
  try {
33187
33627
  const touch = e.nativeEvent;
33188
- touchStartRef.current = {
33628
+ const target = resolveTouchTarget(e);
33629
+ let ui;
33630
+ let screenName;
33631
+ try {
33632
+ const {
33633
+ uiStateSignature: uiStateSignature2
33634
+ } = (init_uiState(), __toCommonJS(uiState_exports));
33635
+ ui = uiStateSignature2();
33636
+ } catch {
33637
+ }
33638
+ try {
33639
+ const {
33640
+ AutoScreenDetector: AutoScreenDetector2
33641
+ } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
33642
+ screenName = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
33643
+ } catch {
33644
+ }
33645
+ const start = {
33646
+ interactionId: generateInteractionId(),
33647
+ occurredAt: Date.now(),
33189
33648
  x: touch.pageX,
33190
33649
  y: touch.pageY,
33191
- ts: Date.now()
33650
+ locationX: touch.locationX,
33651
+ locationY: touch.locationY,
33652
+ target: describeTouchTarget(target),
33653
+ targetId: target?.testID,
33654
+ screenName,
33655
+ ui,
33656
+ stateStatus: ui ? "captured_nonempty" : "not_instrumented",
33657
+ emitAutomaticAnalytics: captureAutomaticInteractions
33192
33658
  };
33659
+ touchStartRef.current = start;
33660
+ interactionStartsRef.current = interactionStartsRef.current.filter((candidate) => start.occurredAt - candidate.occurredAt < 5e3).concat(start).slice(-8);
33193
33661
  } catch {
33194
33662
  }
33195
33663
  };
@@ -33220,21 +33688,29 @@ function ScaleBunDebugRoot({
33220
33688
  }
33221
33689
  measureRoot();
33222
33690
  const rootRect = rootRectRef.current;
33223
- const tapped = describeTouchTarget(resolveTouchTarget(e));
33691
+ const tapped = start?.target ?? describeTouchTarget(resolveTouchTarget(e));
33224
33692
  const gestureDetails = {
33225
33693
  target: tapped,
33226
- x: touch.pageX,
33227
- y: touch.pageY,
33228
- pageX: touch.pageX,
33229
- pageY: touch.pageY,
33230
- locationX: touch.locationX,
33231
- locationY: touch.locationY,
33694
+ interactionId: start?.interactionId ?? generateInteractionId(),
33695
+ interactionProtocol: INTERACTION_PROTOCOL_VERSION,
33696
+ occurredAt: start?.occurredAt ?? Date.now(),
33697
+ ui: start?.ui,
33698
+ stateStatus: start?.stateStatus ?? "not_captured",
33699
+ targetId: start?.targetId,
33700
+ screenName: start?.screenName,
33701
+ emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
33702
+ x: start?.x ?? touch.pageX,
33703
+ y: start?.y ?? touch.pageY,
33704
+ pageX: start?.x ?? touch.pageX,
33705
+ pageY: start?.y ?? touch.pageY,
33706
+ locationX: start?.locationX ?? touch.locationX,
33707
+ locationY: start?.locationY ?? touch.locationY,
33232
33708
  viewportWidth: vpW > 0 ? vpW : void 0,
33233
33709
  viewportHeight: vpH > 0 ? vpH : void 0
33234
33710
  };
33235
33711
  if (rootRect && rootRect.w > 0 && rootRect.h > 0) {
33236
- const nx = Math.max(0, Math.min(1, touch.pageX / rootRect.w));
33237
- const ny = Math.max(0, Math.min(1, touch.pageY / rootRect.h));
33712
+ const nx = Math.max(0, Math.min(1, (start?.x ?? touch.pageX) / rootRect.w));
33713
+ const ny = Math.max(0, Math.min(1, (start?.y ?? touch.pageY) / rootRect.h));
33238
33714
  gestureDetails.normalizedX = nx;
33239
33715
  gestureDetails.normalizedY = ny;
33240
33716
  gestureDetails.normalizedPrecomputed = true;
@@ -33265,7 +33741,7 @@ function ScaleBunDebugRoot({
33265
33741
  const dx = touch.pageX - start.x;
33266
33742
  const dy = touch.pageY - start.y;
33267
33743
  const dist = Math.sqrt(dx * dx + dy * dy);
33268
- const duration = Date.now() - start.ts;
33744
+ const duration = Date.now() - start.occurredAt;
33269
33745
  gestureDetails.duration = duration;
33270
33746
  if (dist >= 15) {
33271
33747
  gestureDetails.endX = touch.pageX;
@@ -33283,47 +33759,16 @@ function ScaleBunDebugRoot({
33283
33759
  gestureType = "long_press";
33284
33760
  }
33285
33761
  }
33286
- const targetInfo = _extractTarget(e);
33287
- gestureDetails.target = targetInfo?.testId || targetInfo?.accessibilityLabel;
33288
- if (captureAutomaticInteractions) {
33289
- let screen;
33290
- try {
33291
- const {
33292
- AutoScreenDetector: AutoScreenDetector2
33293
- } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
33294
- screen = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
33295
- } catch {
33296
- }
33297
- emitAutomaticEvent("element_interacted", {
33298
- gesture_type: gestureType,
33299
- screen_name: screen,
33300
- // testID/nativeID is an author-controlled stable identifier.
33301
- // Accessibility labels and rendered text are deliberately omitted.
33302
- target_id: targetInfo?.testId,
33303
- normalized_x: gestureDetails.normalizedX,
33304
- normalized_y: gestureDetails.normalizedY,
33305
- direction: gestureDetails.direction,
33306
- duration_ms: gestureDetails.duration
33307
- });
33308
- }
33309
33762
  if (gestureType === "tap") {
33310
33763
  try {
33311
33764
  const {
33312
33765
  gestureTriggerDetector: gestureTriggerDetector2
33313
33766
  } = (init_gestureTriggerDetector(), __toCommonJS(gestureTriggerDetector_exports));
33314
- let screen;
33315
- try {
33316
- const {
33317
- AutoScreenDetector: AutoScreenDetector2
33318
- } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
33319
- screen = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
33320
- } catch {
33321
- }
33322
33767
  gestureTriggerDetector2.recordTap({
33323
- x: touch.pageX,
33324
- y: touch.pageY,
33325
- hasTarget: !!(targetInfo?.testId || targetInfo?.accessibilityLabel),
33326
- screen
33768
+ x: start?.x ?? touch.pageX,
33769
+ y: start?.y ?? touch.pageY,
33770
+ hasTarget: !!(start?.targetId || start?.target),
33771
+ screen: start?.screenName
33327
33772
  });
33328
33773
  } catch {
33329
33774
  }
@@ -33336,11 +33781,9 @@ function ScaleBunDebugRoot({
33336
33781
  const capturedDetails = {
33337
33782
  ...gestureDetails
33338
33783
  };
33339
- if (pendingJsEmitRef.current !== null) {
33340
- clearTimeout(pendingJsEmitRef.current);
33341
- }
33342
- pendingJsEmitRef.current = setTimeout(() => {
33343
- pendingJsEmitRef.current = null;
33784
+ const interactionId = capturedDetails.interactionId;
33785
+ const timer = setTimeout(() => {
33786
+ pendingJsEmitsRef.current.delete(interactionId);
33344
33787
  if (nativeTrackingConfirmedRef.current) return;
33345
33788
  try {
33346
33789
  const {
@@ -33357,6 +33800,7 @@ function ScaleBunDebugRoot({
33357
33800
  } catch {
33358
33801
  }
33359
33802
  }, 600);
33803
+ pendingJsEmitsRef.current.set(interactionId, timer);
33360
33804
  }
33361
33805
  touchStartRef.current = null;
33362
33806
  } catch {
@@ -33432,20 +33876,6 @@ function useScaleBunScreen(name) {
33432
33876
  }
33433
33877
  }, [name, manager]);
33434
33878
  }
33435
- function _extractTarget(e) {
33436
- try {
33437
- const target = e?.target;
33438
- if (!target) return void 0;
33439
- const props = target._internalFiberInstanceHandleDEV?.memoizedProps ?? {};
33440
- return {
33441
- testId: props.testID || props.nativeID || void 0,
33442
- accessibilityLabel: props.accessibilityLabel || void 0,
33443
- text: void 0
33444
- };
33445
- } catch {
33446
- return void 0;
33447
- }
33448
- }
33449
33879
  var styles4 = {
33450
33880
  root: {
33451
33881
  flex: 1