@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
@@ -92,9 +92,9 @@ var init_internalLogger = __esm({
92
92
  }
93
93
  /** Set level from the public string name used in config (e.g. 'warn'). */
94
94
  setLevelByName(name) {
95
- const resolved = LEVEL_BY_NAME[name];
96
- if (resolved !== void 0) {
97
- this.level = resolved;
95
+ const resolved3 = LEVEL_BY_NAME[name];
96
+ if (resolved3 !== void 0) {
97
+ this.level = resolved3;
98
98
  }
99
99
  }
100
100
  /** Current numeric level — exposed for callers that branch on dev verbosity. */
@@ -600,7 +600,7 @@ var SDK_VERSION;
600
600
  var init_version = __esm({
601
601
  "lib/module/core/constants/version.js"() {
602
602
  "use strict";
603
- SDK_VERSION = "1.10.6";
603
+ SDK_VERSION = "1.11.0";
604
604
  }
605
605
  });
606
606
 
@@ -2459,22 +2459,23 @@ var init_JourneyEventPipeline = __esm({
2459
2459
  emit(type, opts) {
2460
2460
  try {
2461
2461
  const key = `${type}:${opts?.subtype ?? ""}`;
2462
- const now2 = Date.now();
2463
- if (key === this.lastEventKey && now2 - this.lastEventTs < this.config.dedupeWindowMs) {
2462
+ const receivedAt = Date.now();
2463
+ const occurredAt = opts?.timestamp ?? receivedAt;
2464
+ if (key === this.lastEventKey && receivedAt - this.lastEventTs < this.config.dedupeWindowMs) {
2464
2465
  const incomingConfidence = opts?.payload?.confidence;
2465
2466
  if (incomingConfidence === "high" && this.lastEventConfidence !== "high") {
2466
- this._replaceLastEvent(key, now2, opts);
2467
+ this._replaceLastEvent(key, receivedAt, opts);
2467
2468
  }
2468
2469
  return null;
2469
2470
  }
2470
2471
  this.lastEventKey = key;
2471
- this.lastEventTs = now2;
2472
+ this.lastEventTs = receivedAt;
2472
2473
  this.lastEventConfidence = opts?.payload?.confidence ?? null;
2473
2474
  const event = {
2474
2475
  eventId: generateEventId2(),
2475
2476
  sessionId: this.sessionId,
2476
2477
  journeyId: opts?.journeyId,
2477
- ts: now2,
2478
+ ts: occurredAt,
2478
2479
  type,
2479
2480
  subtype: opts?.subtype,
2480
2481
  severity: opts?.severity ?? inferSeverity(type),
@@ -3574,6 +3575,99 @@ var init_calibrationContext = __esm({
3574
3575
  }
3575
3576
  });
3576
3577
 
3578
+ // lib/module/analytics/automaticEvents.js
3579
+ function compactProperties(properties) {
3580
+ const out = {
3581
+ capture_source: "automatic"
3582
+ };
3583
+ for (const [key, value] of Object.entries(properties ?? {})) {
3584
+ if (value !== void 0) out[key] = value;
3585
+ }
3586
+ return out;
3587
+ }
3588
+ function emitAutomaticEvent(name, properties, timestamp) {
3589
+ const event = {
3590
+ name,
3591
+ properties: compactProperties(properties),
3592
+ timestamp
3593
+ };
3594
+ if (listeners.size === 0) {
3595
+ pending2.push(event);
3596
+ if (pending2.length > MAX_PENDING) pending2.shift();
3597
+ return;
3598
+ }
3599
+ for (const listener of listeners) {
3600
+ try {
3601
+ listener(event);
3602
+ } catch {
3603
+ }
3604
+ }
3605
+ }
3606
+ function subscribeAutomaticEvents(listener) {
3607
+ listeners.add(listener);
3608
+ if (pending2.length > 0) {
3609
+ const buffered = pending2.splice(0, pending2.length);
3610
+ for (const event of buffered) {
3611
+ try {
3612
+ listener(event);
3613
+ } catch {
3614
+ }
3615
+ }
3616
+ }
3617
+ return () => {
3618
+ listeners.delete(listener);
3619
+ };
3620
+ }
3621
+ var listeners, pending2, MAX_PENDING;
3622
+ var init_automaticEvents = __esm({
3623
+ "lib/module/analytics/automaticEvents.js"() {
3624
+ "use strict";
3625
+ listeners = /* @__PURE__ */ new Set();
3626
+ pending2 = [];
3627
+ MAX_PENDING = 50;
3628
+ }
3629
+ });
3630
+
3631
+ // lib/module/features/journey/interactionProtocol.js
3632
+ function generateInteractionId() {
3633
+ return `ixj-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
3634
+ }
3635
+ function nearestInteractionStart(starts, occurredAt, toleranceMs = 1500) {
3636
+ let best;
3637
+ let bestDelta = toleranceMs + 1;
3638
+ for (const start of starts) {
3639
+ const delta = Math.abs(start.occurredAt - occurredAt);
3640
+ if (delta < bestDelta) {
3641
+ best = start;
3642
+ bestDelta = delta;
3643
+ }
3644
+ }
3645
+ return bestDelta <= toleranceMs ? best : void 0;
3646
+ }
3647
+ function automaticInteractionProperties(payload, screenName, canonicalMirror) {
3648
+ return {
3649
+ gesture_type: payload.gestureType,
3650
+ screen_name: screenName,
3651
+ interaction_id: payload.interaction_id,
3652
+ interaction_protocol: payload.interaction_protocol,
3653
+ state_status: payload.state_status,
3654
+ ui: payload.ui,
3655
+ target_id: payload.target_id,
3656
+ normalized_x: payload.normalizedX,
3657
+ normalized_y: payload.normalizedY,
3658
+ direction: payload.direction,
3659
+ duration_ms: payload.durationMs,
3660
+ canonical_mirror: canonicalMirror || void 0
3661
+ };
3662
+ }
3663
+ var INTERACTION_PROTOCOL_VERSION;
3664
+ var init_interactionProtocol = __esm({
3665
+ "lib/module/features/journey/interactionProtocol.js"() {
3666
+ "use strict";
3667
+ INTERACTION_PROTOCOL_VERSION = 1;
3668
+ }
3669
+ });
3670
+
3577
3671
  // lib/module/features/session/SessionManager.js
3578
3672
  var SessionManager_exports = {};
3579
3673
  __export(SessionManager_exports, {
@@ -3582,7 +3676,7 @@ __export(SessionManager_exports, {
3582
3676
  function generateSessionId2() {
3583
3677
  return `ses-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`;
3584
3678
  }
3585
- var HEATMAP_MAX_INTERACTIONS_PER_WINDOW, _instance2, SessionManager;
3679
+ var _instance2, SessionManager;
3586
3680
  var init_SessionManager = __esm({
3587
3681
  "lib/module/features/session/SessionManager.js"() {
3588
3682
  "use strict";
@@ -3599,7 +3693,8 @@ var init_SessionManager = __esm({
3599
3693
  init_redaction();
3600
3694
  init_calibrationContext();
3601
3695
  init_device();
3602
- HEATMAP_MAX_INTERACTIONS_PER_WINDOW = 200;
3696
+ init_automaticEvents();
3697
+ init_interactionProtocol();
3603
3698
  _instance2 = null;
3604
3699
  SessionManager = class _SessionManager {
3605
3700
  desktopTransport = null;
@@ -3610,15 +3705,6 @@ var init_SessionManager = __esm({
3610
3705
  * lane even when no replay recording is active. Additive, opt-in (default off).
3611
3706
  */
3612
3707
  _captureInteractionHeatmap = false;
3613
- /**
3614
- * Sampling cap for the analytics-lane heatmap emission. Now that capture is
3615
- * ON by default, an unbounded one-event-per-gesture stream could materially
3616
- * inflate ingest volume. We cap emitted interactions per analytics-session
3617
- * window (finalize-scoped per foreground): the first N gestures define the
3618
- * hotspot shape; the long tail is dropped. Resets when the window changes.
3619
- */
3620
- _heatmapWindowSessionId = null;
3621
- _heatmapWindowCount = 0;
3622
3708
  session = null;
3623
3709
  active = false;
3624
3710
  timeoutTimer = null;
@@ -4099,18 +4185,11 @@ var init_SessionManager = __esm({
4099
4185
  * injected sensitive keys (e.g. a label) are stripped before transport.
4100
4186
  * - Never throws.
4101
4187
  */
4102
- _emitInteractionToAnalytics(gestureType, payload) {
4103
- if (!this._captureInteractionHeatmap) return;
4104
- if (this.active) return;
4188
+ _emitInteractionToAnalytics(gestureType, payload, occurredAt, screenName) {
4189
+ if (!this._captureInteractionHeatmap) return false;
4190
+ if (this.active) return false;
4105
4191
  const adapter = this._backendTransport;
4106
- if (!adapter) return;
4107
- const windowId = adapter.analyticsSessionId ?? "";
4108
- if (windowId !== this._heatmapWindowSessionId) {
4109
- this._heatmapWindowSessionId = windowId;
4110
- this._heatmapWindowCount = 0;
4111
- }
4112
- if (this._heatmapWindowCount >= HEATMAP_MAX_INTERACTIONS_PER_WINDOW) return;
4113
- this._heatmapWindowCount++;
4192
+ if (!adapter) return false;
4114
4193
  try {
4115
4194
  this._normalizeInteractionPayload(payload);
4116
4195
  const safePayload = redactBody(payload);
@@ -4118,16 +4197,18 @@ var init_SessionManager = __esm({
4118
4197
  eventId: generateEventId(),
4119
4198
  // sessionId is (re)stamped by the analytics lane at flush time.
4120
4199
  sessionId: adapter.analyticsSessionId ?? "",
4121
- ts: Date.now(),
4200
+ ts: occurredAt,
4122
4201
  type: "USER_ACTION",
4123
4202
  subtype: `gesture:${gestureType}`,
4124
- screen: this._lastKnownScreen ?? void 0,
4203
+ screen: screenName ?? this._lastKnownScreen ?? void 0,
4125
4204
  payload: safePayload,
4126
4205
  source: "user"
4127
4206
  };
4128
4207
  adapter.trackEvent(event);
4208
+ return true;
4129
4209
  } catch (err) {
4130
4210
  logger.error("[SessionManager] heatmap analytics emit failed:", err);
4211
+ return false;
4131
4212
  }
4132
4213
  }
4133
4214
  /**
@@ -4293,13 +4374,14 @@ var init_SessionManager = __esm({
4293
4374
  * Notify of a user interaction. Called by ScaleBunDebugRoot touch handlers.
4294
4375
  * Also triggers frame capture for desktop-initiated recordings.
4295
4376
  */
4296
- onUserAction(subtype, payload) {
4377
+ onUserAction(subtype, payload, context) {
4297
4378
  if (!this.active) return;
4298
4379
  this.emitEvent("USER_ACTION", {
4299
4380
  subtype,
4300
- screen: this._lastKnownScreen ?? void 0,
4381
+ screen: context?.screen ?? this._lastKnownScreen ?? void 0,
4301
4382
  payload,
4302
- source: "user"
4383
+ source: "user",
4384
+ timestamp: context?.timestamp
4303
4385
  });
4304
4386
  }
4305
4387
  /**
@@ -4307,7 +4389,8 @@ var init_SessionManager = __esm({
4307
4389
  * Emits a USER_ACTION event with gesture-specific subtype and payload.
4308
4390
  */
4309
4391
  onGestureDetected(gestureType, details) {
4310
- const dedupSig = `${gestureType}|${String(details?.x)}|${String(details?.y)}|${String(details?.endX)}|${String(details?.endY)}`;
4392
+ const suppliedInteractionId = details?.interactionId;
4393
+ const dedupSig = suppliedInteractionId ? `id:${suppliedInteractionId}` : `${gestureType}|${String(details?.x)}|${String(details?.y)}|${String(details?.endX)}|${String(details?.endY)}`;
4311
4394
  const nowTs = Date.now();
4312
4395
  if (dedupSig === this._lastGestureSig && nowTs - this._lastGestureTs <= _SessionManager.GESTURE_DEDUP_WINDOW_MS) {
4313
4396
  this._lastGestureTs = nowTs;
@@ -4315,7 +4398,15 @@ var init_SessionManager = __esm({
4315
4398
  }
4316
4399
  this._lastGestureSig = dedupSig;
4317
4400
  this._lastGestureTs = nowTs;
4401
+ const interactionId = suppliedInteractionId ?? generateInteractionId();
4402
+ const occurredAt = details?.occurredAt ?? Date.now();
4403
+ const stateStatus = details?.stateStatus ?? (details?.ui ? "captured_nonempty" : "not_captured");
4318
4404
  const payload = {
4405
+ interaction_id: interactionId,
4406
+ interaction_protocol: details?.interactionProtocol ?? INTERACTION_PROTOCOL_VERSION,
4407
+ state_status: stateStatus,
4408
+ ui: details?.ui,
4409
+ target_id: details?.targetId,
4319
4410
  gestureType,
4320
4411
  x: details?.x,
4321
4412
  y: details?.y,
@@ -4353,12 +4444,23 @@ var init_SessionManager = __esm({
4353
4444
  screenHeight: details?.screenHeight,
4354
4445
  platform: details?.platform
4355
4446
  };
4356
- this._emitInteractionToAnalytics(gestureType, {
4447
+ const analyticsReplayCarrier = this._emitInteractionToAnalytics(gestureType, {
4357
4448
  ...payload
4358
- });
4449
+ }, occurredAt, details?.screenName);
4450
+ const replayCarrier = !!this._backendTransport && (this.active || analyticsReplayCarrier);
4451
+ if (details?.emitAutomaticAnalytics) {
4452
+ try {
4453
+ const safePayload = redactBody(payload);
4454
+ emitAutomaticEvent("element_interacted", automaticInteractionProperties(safePayload, details?.screenName ?? this._lastKnownScreen ?? void 0, replayCarrier), occurredAt);
4455
+ } catch {
4456
+ }
4457
+ }
4359
4458
  if (!this.active) return;
4360
4459
  const target = typeof details?.target === "string" ? details.target.trim() : "";
4361
- this.onUserAction(target ? `gesture:${gestureType} \xB7 ${target}` : `gesture:${gestureType}`, payload);
4460
+ this.onUserAction(target ? `gesture:${gestureType} \xB7 ${target}` : `gesture:${gestureType}`, payload, {
4461
+ screen: details?.screenName,
4462
+ timestamp: occurredAt
4463
+ });
4362
4464
  }
4363
4465
  /**
4364
4466
  * Manual frame capture — triggered by Desktop "Capture Step" button.
@@ -6229,58 +6331,6 @@ var init_batching = __esm({
6229
6331
  }
6230
6332
  });
6231
6333
 
6232
- // lib/module/analytics/automaticEvents.js
6233
- function compactProperties(properties) {
6234
- const out = {
6235
- capture_source: "automatic"
6236
- };
6237
- for (const [key, value] of Object.entries(properties ?? {})) {
6238
- if (value !== void 0) out[key] = value;
6239
- }
6240
- return out;
6241
- }
6242
- function emitAutomaticEvent(name, properties) {
6243
- const event = {
6244
- name,
6245
- properties: compactProperties(properties)
6246
- };
6247
- if (listeners.size === 0) {
6248
- pending3.push(event);
6249
- if (pending3.length > MAX_PENDING2) pending3.shift();
6250
- return;
6251
- }
6252
- for (const listener of listeners) {
6253
- try {
6254
- listener(event);
6255
- } catch {
6256
- }
6257
- }
6258
- }
6259
- function subscribeAutomaticEvents(listener) {
6260
- listeners.add(listener);
6261
- if (pending3.length > 0) {
6262
- const buffered = pending3.splice(0, pending3.length);
6263
- for (const event of buffered) {
6264
- try {
6265
- listener(event);
6266
- } catch {
6267
- }
6268
- }
6269
- }
6270
- return () => {
6271
- listeners.delete(listener);
6272
- };
6273
- }
6274
- var listeners, pending3, MAX_PENDING2;
6275
- var init_automaticEvents = __esm({
6276
- "lib/module/analytics/automaticEvents.js"() {
6277
- "use strict";
6278
- listeners = /* @__PURE__ */ new Set();
6279
- pending3 = [];
6280
- MAX_PENDING2 = 50;
6281
- }
6282
- });
6283
-
6284
6334
  // lib/module/analytics/EventTracker.js
6285
6335
  function uuid() {
6286
6336
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
@@ -6349,7 +6399,7 @@ var init_EventTracker = __esm({
6349
6399
  this.started = true;
6350
6400
  if (this.cfg.automaticEventTracking) {
6351
6401
  this.automaticEventsUnsubscribe = subscribeAutomaticEvents((event) => {
6352
- this.track(event.name, event.properties);
6402
+ this.track(event.name, event.properties, event.timestamp);
6353
6403
  });
6354
6404
  }
6355
6405
  if (this.cfg.autoLifecycleEvents) {
@@ -6453,9 +6503,9 @@ var init_EventTracker = __esm({
6453
6503
  }
6454
6504
  }
6455
6505
  // ─── tracking ────────────────────────────────────────────────────────────
6456
- track(eventName, properties) {
6506
+ track(eventName, properties, timestamp) {
6457
6507
  try {
6458
- this.enqueue(this.buildEnvelope(eventName, properties));
6508
+ this.enqueue(this.buildEnvelope(eventName, properties, timestamp));
6459
6509
  try {
6460
6510
  this.cfg.onEvent?.(eventName);
6461
6511
  } catch {
@@ -6559,7 +6609,7 @@ var init_EventTracker = __esm({
6559
6609
  };
6560
6610
  }
6561
6611
  // ─── internals ─────────────────────────────────────────────────────────────
6562
- buildEnvelope(eventName, properties) {
6612
+ buildEnvelope(eventName, properties, timestamp) {
6563
6613
  const ctx = this.cfg.context ?? {};
6564
6614
  let canonicalSessionId;
6565
6615
  try {
@@ -6570,7 +6620,7 @@ var init_EventTracker = __esm({
6570
6620
  const env = {
6571
6621
  event_id: uuid(),
6572
6622
  event_name: eventName,
6573
- event_time: Date.now(),
6623
+ event_time: timestamp ?? Date.now(),
6574
6624
  app_id: this.cfg.appId,
6575
6625
  platform: this.cfg.platform ?? resolveEventPlatform(),
6576
6626
  installation_id: this.installationId,
@@ -6907,10 +6957,249 @@ var init_deviceAttributes = __esm({
6907
6957
  }
6908
6958
  });
6909
6959
 
6960
+ // lib/module/features/ota/crypto/loadEd25519.js
6961
+ function loadEd25519() {
6962
+ try {
6963
+ const mod = require("@noble/ed25519");
6964
+ const ed = mod?.default ?? mod;
6965
+ if (typeof ed?.verifyAsync === "function" || typeof ed?.verify === "function") {
6966
+ return ed;
6967
+ }
6968
+ return null;
6969
+ } catch {
6970
+ return null;
6971
+ }
6972
+ }
6973
+ var init_loadEd25519 = __esm({
6974
+ "lib/module/features/ota/crypto/loadEd25519.js"() {
6975
+ "use strict";
6976
+ }
6977
+ });
6978
+
6979
+ // lib/module/features/ota/crypto/loadSha512.js
6980
+ function loadSha512() {
6981
+ try {
6982
+ const mod = require("@noble/hashes/sha2.js");
6983
+ const sha512 = mod?.sha512 ?? mod?.default?.sha512;
6984
+ return typeof sha512 === "function" ? sha512 : null;
6985
+ } catch {
6986
+ return null;
6987
+ }
6988
+ }
6989
+ var init_loadSha512 = __esm({
6990
+ "lib/module/features/ota/crypto/loadSha512.js"() {
6991
+ "use strict";
6992
+ }
6993
+ });
6994
+
6995
+ // lib/module/features/ota/crypto/builtinVerifier.js
6996
+ var builtinVerifier_exports = {};
6997
+ __export(builtinVerifier_exports, {
6998
+ _resetBuiltinVerifier: () => _resetBuiltinVerifier,
6999
+ decodeSignature: () => decodeSignature,
7000
+ getBuiltinVerifier: () => getBuiltinVerifier
7001
+ });
7002
+ function hexToBytes(hex) {
7003
+ const clean2 = hex.trim().toLowerCase().replace(/^0x/, "");
7004
+ if (clean2.length === 0 || clean2.length % 2 !== 0 || /[^0-9a-f]/.test(clean2)) return null;
7005
+ const out = new Uint8Array(clean2.length / 2);
7006
+ for (let i = 0; i < out.length; i++) {
7007
+ out[i] = parseInt(clean2.slice(i * 2, i * 2 + 2), 16);
7008
+ }
7009
+ return out;
7010
+ }
7011
+ function base64ToBytes2(b64) {
7012
+ const s = b64.trim().replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
7013
+ if (s.length === 0) return null;
7014
+ const out = [];
7015
+ let buffer = 0;
7016
+ let bits = 0;
7017
+ for (const ch of s) {
7018
+ const idx = B64_ALPHABET2.indexOf(ch);
7019
+ if (idx === -1) return null;
7020
+ buffer = buffer << 6 | idx;
7021
+ bits += 6;
7022
+ if (bits >= 8) {
7023
+ bits -= 8;
7024
+ out.push(buffer >> bits & 255);
7025
+ }
7026
+ }
7027
+ return Uint8Array.from(out);
7028
+ }
7029
+ function decodeSignature(sig) {
7030
+ const trimmed = sig.trim();
7031
+ if (/^(0x)?[0-9a-fA-F]{128}$/.test(trimmed)) {
7032
+ const hex2 = hexToBytes(trimmed);
7033
+ if (hex2 && hex2.length === ED25519_SIGNATURE_BYTES) return hex2;
7034
+ }
7035
+ const b64 = base64ToBytes2(trimmed);
7036
+ if (b64 && b64.length === ED25519_SIGNATURE_BYTES) return b64;
7037
+ const hex = hexToBytes(trimmed);
7038
+ if (hex && hex.length === ED25519_SIGNATURE_BYTES) return hex;
7039
+ return null;
7040
+ }
7041
+ function wireSha512(ed, sha512) {
7042
+ const concat2 = (parts) => {
7043
+ if (parts.length === 1) return parts[0];
7044
+ let total = 0;
7045
+ for (const p of parts) total += p.length;
7046
+ const out = new Uint8Array(total);
7047
+ let off = 0;
7048
+ for (const p of parts) {
7049
+ out.set(p, off);
7050
+ off += p.length;
7051
+ }
7052
+ return out;
7053
+ };
7054
+ const sync = (...msgs) => sha512(concat2(msgs));
7055
+ const asyncFn = async (...msgs) => sha512(concat2(msgs));
7056
+ let wired = false;
7057
+ try {
7058
+ if (ed.hashes && typeof ed.hashes === "object") {
7059
+ if (typeof ed.hashes.sha512 !== "function") ed.hashes.sha512 = sync;
7060
+ if (typeof ed.hashes.sha512Async !== "function") ed.hashes.sha512Async = asyncFn;
7061
+ wired = typeof ed.hashes.sha512 === "function";
7062
+ }
7063
+ } catch {
7064
+ }
7065
+ try {
7066
+ if (ed.etc && typeof ed.etc === "object") {
7067
+ if (typeof ed.etc.sha512Sync !== "function") ed.etc.sha512Sync = sync;
7068
+ if (typeof ed.etc.sha512Async !== "function") ed.etc.sha512Async = asyncFn;
7069
+ wired = wired || typeof ed.etc.sha512Sync === "function";
7070
+ }
7071
+ } catch {
7072
+ }
7073
+ return wired;
7074
+ }
7075
+ function getBuiltinVerifier() {
7076
+ if (resolved !== void 0) return resolved;
7077
+ const ed = loadEd25519();
7078
+ const sha512 = loadSha512();
7079
+ if (!ed || !sha512) {
7080
+ resolved = null;
7081
+ return null;
7082
+ }
7083
+ if (!wireSha512(ed, sha512)) {
7084
+ 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.");
7085
+ resolved = null;
7086
+ return null;
7087
+ }
7088
+ resolved = async ({
7089
+ messageHex,
7090
+ signature,
7091
+ publicKey
7092
+ }) => {
7093
+ const message = hexToBytes(messageHex);
7094
+ const pub = hexToBytes(publicKey);
7095
+ const sig = decodeSignature(signature);
7096
+ if (!message) {
7097
+ logger.error("[OTA] Bundle SHA-256 is not valid hex \u2014 refusing to stage.");
7098
+ return false;
7099
+ }
7100
+ if (!pub || pub.length !== ED25519_PUBLIC_KEY_BYTES) {
7101
+ 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"}.`);
7102
+ return false;
7103
+ }
7104
+ if (!sig) {
7105
+ logger.error("[OTA] Bundle signature is not a 64-byte hex/base64 value.");
7106
+ return false;
7107
+ }
7108
+ try {
7109
+ if (typeof ed.verify === "function") return ed.verify(sig, message, pub);
7110
+ if (typeof ed.verifyAsync === "function") return await ed.verifyAsync(sig, message, pub);
7111
+ return false;
7112
+ } catch {
7113
+ return false;
7114
+ }
7115
+ };
7116
+ __DEV__ && logger.debug("[OTA] Using built-in ed25519 verifier (@noble/ed25519 detected).");
7117
+ return resolved;
7118
+ }
7119
+ function _resetBuiltinVerifier() {
7120
+ resolved = void 0;
7121
+ }
7122
+ var ED25519_SIGNATURE_BYTES, ED25519_PUBLIC_KEY_BYTES, B64_ALPHABET2, resolved;
7123
+ var init_builtinVerifier = __esm({
7124
+ "lib/module/features/ota/crypto/builtinVerifier.js"() {
7125
+ "use strict";
7126
+ init_loadEd25519();
7127
+ init_loadSha512();
7128
+ init_internalLogger();
7129
+ ED25519_SIGNATURE_BYTES = 64;
7130
+ ED25519_PUBLIC_KEY_BYTES = 32;
7131
+ B64_ALPHABET2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7132
+ }
7133
+ });
7134
+
7135
+ // lib/module/features/ota/crypto/nativeVerifier.js
7136
+ function bytesToHex(bytes) {
7137
+ let out = "";
7138
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
7139
+ return out;
7140
+ }
7141
+ function normalizeHex(value, expectedChars) {
7142
+ const clean2 = value.trim().toLowerCase().replace(/^0x/, "");
7143
+ if (clean2.length !== expectedChars || /[^0-9a-f]/.test(clean2)) return null;
7144
+ return clean2;
7145
+ }
7146
+ function getNativeVerifier() {
7147
+ if (resolved2 !== void 0) return resolved2;
7148
+ const native = NativeScaleBunOta_default;
7149
+ if (!native || typeof native.verifyEd25519 !== "function") {
7150
+ resolved2 = null;
7151
+ return null;
7152
+ }
7153
+ resolved2 = async ({
7154
+ messageHex,
7155
+ signature,
7156
+ publicKey
7157
+ }) => {
7158
+ const message = normalizeHex(messageHex, 64);
7159
+ const key = normalizeHex(publicKey, ED25519_PUBLIC_KEY_HEX_CHARS);
7160
+ const sigBytes = decodeSignature(signature);
7161
+ if (!message || !key || !sigBytes) {
7162
+ logger.error("[OTA] Signature inputs malformed (bundle hash, signature, or public key) \u2014 refusing to stage.");
7163
+ return false;
7164
+ }
7165
+ let verdict;
7166
+ try {
7167
+ verdict = await native.verifyEd25519(message, bytesToHex(sigBytes), key);
7168
+ } catch (err) {
7169
+ logger.warn(`[OTA] Native ed25519 verify failed to run: ${err?.message ?? err}`);
7170
+ verdict = "unavailable";
7171
+ }
7172
+ if (verdict === "valid") return true;
7173
+ if (verdict === "invalid") return false;
7174
+ const builtin = getBuiltinVerifier();
7175
+ if (builtin) {
7176
+ return builtin({
7177
+ messageHex,
7178
+ signature,
7179
+ publicKey
7180
+ });
7181
+ }
7182
+ throw new Error("ed25519 unavailable: this OS has no native implementation (Android < 13) and the optional peers @noble/ed25519 + @noble/hashes are not installed.");
7183
+ };
7184
+ __DEV__ && logger.debug("[OTA] Using native ed25519 verifier (platform crypto).");
7185
+ return resolved2;
7186
+ }
7187
+ var ED25519_PUBLIC_KEY_HEX_CHARS, resolved2;
7188
+ var init_nativeVerifier = __esm({
7189
+ "lib/module/features/ota/crypto/nativeVerifier.js"() {
7190
+ "use strict";
7191
+ init_NativeScaleBunOta();
7192
+ init_internalLogger();
7193
+ init_builtinVerifier();
7194
+ ED25519_PUBLIC_KEY_HEX_CHARS = 64;
7195
+ }
7196
+ });
7197
+
6910
7198
  // lib/module/features/ota/signature.js
6911
7199
  async function verifyBundleSignature(bundleSha256, signature, config) {
6912
- const publicKey = config?.publicKey;
6913
- if (!publicKey) {
7200
+ const rawKey = config?.publicKey;
7201
+ const keys = (Array.isArray(rawKey) ? rawKey : rawKey ? [rawKey] : []).filter((k) => typeof k === "string" && k.trim().length > 0);
7202
+ if (rawKey === void 0 || rawKey === "" || rawKey === null) {
6914
7203
  if (!warnedNotConfigured) {
6915
7204
  warnedNotConfigured = true;
6916
7205
  logger.warn("[OTA] No signing public key configured \u2014 bundles are accepted on SHA-256 integrity alone. Configure `ota.publicSigningKey` to enforce authenticity.");
@@ -6920,6 +7209,13 @@ async function verifyBundleSignature(bundleSha256, signature, config) {
6920
7209
  reason: "not_configured"
6921
7210
  };
6922
7211
  }
7212
+ if (keys.length === 0) {
7213
+ 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.");
7214
+ return {
7215
+ ok: false,
7216
+ reason: "no_keys"
7217
+ };
7218
+ }
6923
7219
  if (!signature) {
6924
7220
  logger.error("[OTA] Bundle has no signature but a signing key is configured \u2014 refusing to stage.");
6925
7221
  return {
@@ -6927,43 +7223,52 @@ async function verifyBundleSignature(bundleSha256, signature, config) {
6927
7223
  reason: "missing_signature"
6928
7224
  };
6929
7225
  }
6930
- if (typeof config?.verifier !== "function") {
6931
- 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.");
7226
+ const verifier = typeof config?.verifier === "function" ? config.verifier : getNativeVerifier() ?? getBuiltinVerifier();
7227
+ if (!verifier) {
7228
+ 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`.");
6932
7229
  return {
6933
7230
  ok: false,
6934
7231
  reason: "no_verifier"
6935
7232
  };
6936
7233
  }
6937
- try {
6938
- const valid = await config.verifier({
6939
- messageHex: bundleSha256,
6940
- signature,
6941
- publicKey
6942
- });
6943
- if (!valid) {
6944
- logger.error("[OTA] Bundle signature is INVALID \u2014 refusing to stage.");
6945
- return {
6946
- ok: false,
6947
- reason: "invalid_signature"
7234
+ let sawThrow = false;
7235
+ let sawCompletion = false;
7236
+ for (const key of keys) {
7237
+ try {
7238
+ const valid = await verifier({
7239
+ messageHex: bundleSha256,
7240
+ signature,
7241
+ publicKey: key
7242
+ });
7243
+ sawCompletion = true;
7244
+ if (valid) return {
7245
+ ok: true,
7246
+ reason: "verified"
6948
7247
  };
7248
+ } catch (err) {
7249
+ sawThrow = true;
7250
+ logger.error(`[OTA] Signature verifier threw: ${err?.message ?? err}`);
6949
7251
  }
6950
- return {
6951
- ok: true,
6952
- reason: "verified"
6953
- };
6954
- } catch (err) {
6955
- logger.error(`[OTA] Signature verifier threw: ${err?.message ?? err}`);
7252
+ }
7253
+ if (sawThrow && !sawCompletion) {
6956
7254
  return {
6957
7255
  ok: false,
6958
7256
  reason: "verifier_threw"
6959
7257
  };
6960
7258
  }
7259
+ 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.");
7260
+ return {
7261
+ ok: false,
7262
+ reason: "invalid_signature"
7263
+ };
6961
7264
  }
6962
7265
  var warnedNotConfigured;
6963
7266
  var init_signature = __esm({
6964
7267
  "lib/module/features/ota/signature.js"() {
6965
7268
  "use strict";
6966
7269
  init_internalLogger();
7270
+ init_builtinVerifier();
7271
+ init_nativeVerifier();
6967
7272
  warnedNotConfigured = false;
6968
7273
  }
6969
7274
  });
@@ -7783,6 +8088,42 @@ var init_OtaOrchestrator = __esm({
7783
8088
  }
7784
8089
  });
7785
8090
 
8091
+ // lib/module/features/journey/uiState.js
8092
+ var uiState_exports = {};
8093
+ __export(uiState_exports, {
8094
+ clearUiState: () => clearUiState,
8095
+ setUiState: () => setUiState,
8096
+ uiStateSignature: () => uiStateSignature
8097
+ });
8098
+ function setUiState(name, value) {
8099
+ const n = clean(name);
8100
+ const v = clean(value);
8101
+ if (n && v) declared.set(n, v);
8102
+ }
8103
+ function clearUiState(name) {
8104
+ if (name === void 0) declared.clear();
8105
+ else declared.delete(clean(name));
8106
+ }
8107
+ function uiStateSignature() {
8108
+ if (!declared.size) return void 0;
8109
+ const pairs = [...declared.keys()].sort().map((k) => `${k}:${declared.get(k)}`);
8110
+ let signature = "";
8111
+ for (const pair of pairs) {
8112
+ const next = signature ? `${signature};${pair}` : pair;
8113
+ if (next.length > 96) break;
8114
+ signature = next;
8115
+ }
8116
+ return signature || void 0;
8117
+ }
8118
+ var declared, clean;
8119
+ var init_uiState = __esm({
8120
+ "lib/module/features/journey/uiState.js"() {
8121
+ "use strict";
8122
+ declared = /* @__PURE__ */ new Map();
8123
+ clean = (s) => typeof s === "string" ? s.replace(/[;:|]/g, "").trim().slice(0, 32) : "";
8124
+ }
8125
+ });
8126
+
7786
8127
  // lib/module/features/engage/gestureTriggerDetector.js
7787
8128
  var gestureTriggerDetector_exports = {};
7788
8129
  __export(gestureTriggerDetector_exports, {
@@ -9750,6 +10091,14 @@ var SHAPE = {
9750
10091
  kind: "str",
9751
10092
  opt: true
9752
10093
  },
10094
+ // Rotation support: several pinned keys, any of which may verify a
10095
+ // bundle. Ship a build pinning [old, new], re-sign server-side with
10096
+ // new, drop old next release — no emergency store submission when a
10097
+ // key must be replaced. Merged with publicSigningKey by the facade.
10098
+ publicSigningKeys: {
10099
+ kind: "strArr",
10100
+ opt: true
10101
+ },
9753
10102
  mandatoryBlocksUi: bool(false)
9754
10103
  }
9755
10104
  }
@@ -9785,7 +10134,7 @@ function parseField(field, input, path, issues) {
9785
10134
  value: field.def,
9786
10135
  present: true
9787
10136
  };
9788
- 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");
10137
+ 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");
9789
10138
  }
9790
10139
  switch (field.kind) {
9791
10140
  case "any":
@@ -9811,6 +10160,27 @@ function parseField(field, input, path, issues) {
9811
10160
  value: input,
9812
10161
  present: true
9813
10162
  };
10163
+ case "strArr": {
10164
+ if (!Array.isArray(input)) return fail(`Expected array, received ${typeofName(input)}`);
10165
+ let allOk = true;
10166
+ for (let i = 0; i < input.length; i++) {
10167
+ if (typeof input[i] !== "string") {
10168
+ issues.push({
10169
+ path: [...path, i],
10170
+ message: `Expected string, received ${typeofName(input[i])}`
10171
+ });
10172
+ allOk = false;
10173
+ }
10174
+ }
10175
+ return allOk ? {
10176
+ ok: true,
10177
+ value: [...input],
10178
+ present: true
10179
+ } : {
10180
+ ok: false,
10181
+ present: true
10182
+ };
10183
+ }
9814
10184
  case "num":
9815
10185
  if (typeof input !== "number" || Number.isNaN(input)) {
9816
10186
  return fail(`Expected number, received ${typeofName(input)}`);
@@ -10700,14 +11070,14 @@ function installRejectionHandler(onRejection, deps) {
10700
11070
  allRejections: true,
10701
11071
  onUnhandled: (id, error) => {
10702
11072
  const err = toError(error);
10703
- pending2.set(id, err);
11073
+ pending3.set(id, err);
10704
11074
  onRejection(err);
10705
11075
  if (isDev) {
10706
11076
  console.warn(`Possible unhandled promise rejection (id: ${id}):`, err?.message ?? err);
10707
11077
  }
10708
11078
  },
10709
11079
  onHandled: (id) => {
10710
- pending2.delete(id);
11080
+ pending3.delete(id);
10711
11081
  if (isDev) {
10712
11082
  console.warn(`Promise rejection handled late (id: ${id}) \u2014 it was already reported.`);
10713
11083
  }
@@ -10717,7 +11087,7 @@ function installRejectionHandler(onRejection, deps) {
10717
11087
  return true;
10718
11088
  }) ?? false;
10719
11089
  }
10720
- var pending2 = /* @__PURE__ */ new Map();
11090
+ var pending3 = /* @__PURE__ */ new Map();
10721
11091
 
10722
11092
  // lib/module/features/crash/CrashFeature.js
10723
11093
  init_CrashReporter();
@@ -11034,7 +11404,7 @@ var AppLaunchCollector = class {
11034
11404
  };
11035
11405
 
11036
11406
  // lib/module/features/performance/collectors/ScreenLoadCollector.js
11037
- var MAX_PENDING = 20;
11407
+ var MAX_PENDING2 = 20;
11038
11408
  var ScreenLoadCollector = class {
11039
11409
  pending = /* @__PURE__ */ new Map();
11040
11410
  lastScreen = null;
@@ -11049,7 +11419,7 @@ var ScreenLoadCollector = class {
11049
11419
  */
11050
11420
  markStart(screenName) {
11051
11421
  if (!this.config.screenLoad) return;
11052
- if (this.pending.size >= MAX_PENDING) {
11422
+ if (this.pending.size >= MAX_PENDING2) {
11053
11423
  const oldest = this.pending.keys().next().value;
11054
11424
  if (oldest) this.pending.delete(oldest);
11055
11425
  }
@@ -12458,16 +12828,16 @@ function base64ToBytes(base64) {
12458
12828
  return decodeManual(base64);
12459
12829
  }
12460
12830
  function decodeManual(base64) {
12461
- const clean = base64.replace(/[^A-Za-z0-9+/=]/g, "");
12462
- const pad = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
12463
- const outLen = Math.floor(clean.length * 3 / 4) - pad;
12831
+ const clean2 = base64.replace(/[^A-Za-z0-9+/=]/g, "");
12832
+ const pad = clean2.endsWith("==") ? 2 : clean2.endsWith("=") ? 1 : 0;
12833
+ const outLen = Math.floor(clean2.length * 3 / 4) - pad;
12464
12834
  const out = new Uint8Array(outLen);
12465
12835
  let p = 0;
12466
- for (let i = 0; i < clean.length; i += 4) {
12467
- const c0 = B64_ALPHABET.indexOf(clean[i]);
12468
- const c1 = B64_ALPHABET.indexOf(clean[i + 1]);
12469
- const c2 = B64_ALPHABET.indexOf(clean[i + 2]);
12470
- const c3 = B64_ALPHABET.indexOf(clean[i + 3]);
12836
+ for (let i = 0; i < clean2.length; i += 4) {
12837
+ const c0 = B64_ALPHABET.indexOf(clean2[i]);
12838
+ const c1 = B64_ALPHABET.indexOf(clean2[i + 1]);
12839
+ const c2 = B64_ALPHABET.indexOf(clean2[i + 2]);
12840
+ const c3 = B64_ALPHABET.indexOf(clean2[i + 3]);
12471
12841
  const n = c0 << 18 | c1 << 12 | (c2 & 63) << 6 | c3 & 63;
12472
12842
  if (p < outLen) out[p++] = n >> 16 & 255;
12473
12843
  if (p < outLen) out[p++] = n >> 8 & 255;
@@ -16654,12 +17024,17 @@ var ScaleBunFacade = class {
16654
17024
  /**
16655
17025
  * Boot the OTA orchestrator when the init config asks for it.
16656
17026
  *
16657
- * `publicSigningKey` is honoured here so signature enforcement is reachable
16658
- * from configuration alone. The SDK ships no ed25519 implementation, so a
16659
- * host that pins a key must also supply `ota.verifySignature`; pinning a key
16660
- * without a verifier is fail-CLOSED by design (signature.ts) an update
16661
- * that cannot be verified is not installed. We say that out loud rather than
16662
- * letting the app discover it as a silent no-update condition.
17027
+ * `publicSigningKey` / `publicSigningKeys` are honoured here so signature
17028
+ * enforcement is reachable from configuration alone. The list form exists
17029
+ * for key ROTATION: a build pinning [old, new] keeps verifying while the
17030
+ * server moves to the new key, so replacing a key never needs an emergency
17031
+ * store release. Both fields merge (deduplicated) into one pinned set.
17032
+ *
17033
+ * Verification comes from `ota.verifySignature` when supplied, else the
17034
+ * built-in @noble-based verifier (optional peers). Pinning keys with
17035
+ * NEITHER available is fail-CLOSED by design (signature.ts) — an update
17036
+ * that cannot be verified is not installed. We say that out loud rather
17037
+ * than letting the app discover it as a silent no-update condition.
16663
17038
  */
16664
17039
  _maybeStartOta(rawConfig) {
16665
17040
  const ota = rawConfig?.ota;
@@ -16668,15 +17043,25 @@ var ScaleBunFacade = class {
16668
17043
  const {
16669
17044
  otaOrchestrator: otaOrchestrator2
16670
17045
  } = (init_OtaOrchestrator(), __toCommonJS(OtaOrchestrator_exports));
16671
- const publicKey = ota.publicSigningKey;
17046
+ const rawSingle = ota.publicSigningKey;
17047
+ const rawList = ota.publicSigningKeys;
17048
+ const publicKeys = Array.from(new Set([...typeof rawSingle === "string" && rawSingle ? [rawSingle] : [], ...Array.isArray(rawList) ? rawList : []].filter((k) => typeof k === "string" && k.trim().length > 0)));
17049
+ const signingRequested = publicKeys.length > 0 || Array.isArray(rawList);
16672
17050
  const verifier = ota.verifySignature;
16673
- if (publicKey && typeof verifier !== "function") {
16674
- 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.");
17051
+ if (signingRequested && typeof verifier !== "function") {
17052
+ const {
17053
+ getBuiltinVerifier: getBuiltinVerifier2
17054
+ } = (init_builtinVerifier(), __toCommonJS(builtinVerifier_exports));
17055
+ if (!getBuiltinVerifier2()) {
17056
+ 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`.");
17057
+ }
16675
17058
  }
16676
17059
  otaOrchestrator2.init({
16677
- ...publicKey ? {
17060
+ ...signingRequested ? {
16678
17061
  signature: {
16679
- publicKey,
17062
+ // Single key stays a plain string — the shape every
17063
+ // existing consumer and test already handles.
17064
+ publicKey: publicKeys.length === 1 ? publicKeys[0] : publicKeys,
16680
17065
  verifier
16681
17066
  }
16682
17067
  } : {},
@@ -16888,6 +17273,46 @@ var ScaleBunFacade = class {
16888
17273
  };
16889
17274
  }
16890
17275
  /** Convenience: emit a Phase 1 purchase event (revenue + currency + transaction_id). */
17276
+ /**
17277
+ * Declare which UI state the user is looking at, so taps are attributed to the surface they
17278
+ * happened on rather than averaged across every variant of the screen.
17279
+ *
17280
+ * ScaleBun.setUiState('filter-sheet', 'open');
17281
+ * ScaleBun.setUiState('checkout-step', 'payment');
17282
+ *
17283
+ * Call it when the state CHANGES — the value is read at tap time, so it only has to be right by
17284
+ * the time the next tap lands. On React Native this is the ONLY source of UI state: unlike the web,
17285
+ * there is no queryable accessibility tree that says a sheet is up, and guessing would produce a
17286
+ * state key that is right on some apps and silently wrong on others.
17287
+ *
17288
+ * Names and values are identifiers, not content: they become query keys in the dashboard, so a
17289
+ * person's name or a cart total does not belong in one. `;`, `:` and `|` are stripped and both
17290
+ * halves are capped at 32 characters.
17291
+ */
17292
+ setUiState(name, value) {
17293
+ try {
17294
+ const {
17295
+ setUiState: setUiState2
17296
+ } = (init_uiState(), __toCommonJS(uiState_exports));
17297
+ setUiState2(name, value);
17298
+ } catch {
17299
+ }
17300
+ }
17301
+ /**
17302
+ * Stop reporting a UI state dimension — or, with no argument, all of them.
17303
+ *
17304
+ * Clear on screen unmount. A declaration left behind follows the user onto a surface where it means
17305
+ * nothing, and every tap there is filed under a state that was not on screen.
17306
+ */
17307
+ clearUiState(name) {
17308
+ try {
17309
+ const {
17310
+ clearUiState: clearUiState2
17311
+ } = (init_uiState(), __toCommonJS(uiState_exports));
17312
+ clearUiState2(name);
17313
+ } catch {
17314
+ }
17315
+ }
16891
17316
  trackPurchase(input) {
16892
17317
  noThrow(() => this._eventTracker?.trackPurchase(input));
16893
17318
  }
@@ -20511,11 +20936,11 @@ var normalizedWheelValue = (value) => value.toLowerCase().replace(/\s+/g, " ").t
20511
20936
  function resolveWheelOutcome(segments, source) {
20512
20937
  const identity = wheelOutcomeIdentity(source);
20513
20938
  const index = findWheelOutcomeIndex(segments, identity);
20514
- const resolved = index >= 0;
20939
+ const resolved3 = index >= 0;
20515
20940
  return {
20516
- index: resolved ? index : 0,
20517
- label: identity.resultLabel || (resolved ? segments[index]?.fullLabel ?? "" : ""),
20518
- resolved
20941
+ index: resolved3 ? index : 0,
20942
+ label: identity.resultLabel || (resolved3 ? segments[index]?.fullLabel ?? "" : ""),
20943
+ resolved: resolved3
20519
20944
  };
20520
20945
  }
20521
20946
  function SpinWheelInApp(props) {
@@ -29374,8 +29799,8 @@ function EngagePromptProvider({
29374
29799
  __DEV__ && logger.debug("[Engage] no eligible in-app message to render");
29375
29800
  return false;
29376
29801
  }
29377
- const resolved = selectInAppVariant(message, getDeviceId());
29378
- activeInAppVariantRef.current = message.experiment?.enabled ? resolved : null;
29802
+ const resolved3 = selectInAppVariant(message, getDeviceId());
29803
+ activeInAppVariantRef.current = message.experiment?.enabled ? resolved3 : null;
29379
29804
  store.recordImpression(message.id, Date.now(), message.revision);
29380
29805
  setActiveInApp(message);
29381
29806
  return true;
@@ -29615,7 +30040,6 @@ function useEngagePrompt() {
29615
30040
  // lib/module/features/journey/ScaleBunDebugRoot.js
29616
30041
  var import_react9 = __toESM(require("react"));
29617
30042
  var import_react_native42 = require("react-native");
29618
- init_automaticEvents();
29619
30043
 
29620
30044
  // lib/module/features/journey/touchTarget.js
29621
30045
  var MAX_DEPTH = 12;
@@ -29687,6 +30111,7 @@ function describeTouchTarget(t) {
29687
30111
 
29688
30112
  // lib/module/features/journey/ScaleBunDebugRoot.js
29689
30113
  init_device();
30114
+ init_interactionProtocol();
29690
30115
  var import_jsx_runtime8 = require("react/jsx-runtime");
29691
30116
  var _bootstrap = null;
29692
30117
  function getBootstrap() {
@@ -29876,7 +30301,9 @@ function ScaleBunDebugRoot({
29876
30301
  }, [navigationRef]);
29877
30302
  const lastNativeTouchTsRef = (0, import_react9.useRef)(0);
29878
30303
  const nativeTrackingConfirmedRef = (0, import_react9.useRef)(false);
29879
- const pendingJsEmitRef = (0, import_react9.useRef)(null);
30304
+ const touchStartRef = (0, import_react9.useRef)(null);
30305
+ const interactionStartsRef = (0, import_react9.useRef)([]);
30306
+ const pendingJsEmitsRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
29880
30307
  (0, import_react9.useEffect)(() => {
29881
30308
  let subscription = null;
29882
30309
  try {
@@ -29887,9 +30314,12 @@ function ScaleBunDebugRoot({
29887
30314
  try {
29888
30315
  lastNativeTouchTsRef.current = Date.now();
29889
30316
  nativeTrackingConfirmedRef.current = true;
29890
- if (pendingJsEmitRef.current !== null) {
29891
- clearTimeout(pendingJsEmitRef.current);
29892
- pendingJsEmitRef.current = null;
30317
+ const nativeOccurredAt = typeof event.occurredAt === "number" ? event.occurredAt : (typeof event.timestamp === "number" ? event.timestamp : Date.now()) - (typeof event.durationMs === "number" ? event.durationMs : 0);
30318
+ const start = nearestInteractionStart(interactionStartsRef.current, nativeOccurredAt);
30319
+ if (start) {
30320
+ const pending4 = pendingJsEmitsRef.current.get(start.interactionId);
30321
+ if (pending4 !== void 0) clearTimeout(pending4);
30322
+ pendingJsEmitsRef.current.delete(start.interactionId);
29893
30323
  }
29894
30324
  const {
29895
30325
  SessionManager: SessionManager2
@@ -29908,6 +30338,15 @@ function ScaleBunDebugRoot({
29908
30338
  } catch {
29909
30339
  }
29910
30340
  sm.onGestureDetected(event.gestureType || "tap", {
30341
+ interactionId: event.interactionId || start?.interactionId || generateInteractionId(),
30342
+ interactionProtocol: event.interactionProtocol || INTERACTION_PROTOCOL_VERSION,
30343
+ occurredAt: nativeOccurredAt,
30344
+ ui: start?.ui,
30345
+ stateStatus: start?.stateStatus ?? "not_captured",
30346
+ target: start?.target,
30347
+ targetId: start?.targetId,
30348
+ screenName: start?.screenName,
30349
+ emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
29911
30350
  x: event.rawX,
29912
30351
  y: event.rawY,
29913
30352
  // Use the actual native end coords when present, not the down
@@ -29940,9 +30379,10 @@ function ScaleBunDebugRoot({
29940
30379
  subscription?.remove();
29941
30380
  } catch {
29942
30381
  }
30382
+ for (const timer of pendingJsEmitsRef.current.values()) clearTimeout(timer);
30383
+ pendingJsEmitsRef.current.clear();
29943
30384
  };
29944
- }, []);
29945
- const touchStartRef = (0, import_react9.useRef)(null);
30385
+ }, [captureAutomaticInteractions]);
29946
30386
  const rootRectRef = (0, import_react9.useRef)(null);
29947
30387
  const measureRoot = () => {
29948
30388
  try {
@@ -29974,11 +30414,39 @@ function ScaleBunDebugRoot({
29974
30414
  const handleTouchStart = (e) => {
29975
30415
  try {
29976
30416
  const touch = e.nativeEvent;
29977
- touchStartRef.current = {
30417
+ const target = resolveTouchTarget(e);
30418
+ let ui;
30419
+ let screenName;
30420
+ try {
30421
+ const {
30422
+ uiStateSignature: uiStateSignature2
30423
+ } = (init_uiState(), __toCommonJS(uiState_exports));
30424
+ ui = uiStateSignature2();
30425
+ } catch {
30426
+ }
30427
+ try {
30428
+ const {
30429
+ AutoScreenDetector: AutoScreenDetector2
30430
+ } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
30431
+ screenName = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
30432
+ } catch {
30433
+ }
30434
+ const start = {
30435
+ interactionId: generateInteractionId(),
30436
+ occurredAt: Date.now(),
29978
30437
  x: touch.pageX,
29979
30438
  y: touch.pageY,
29980
- ts: Date.now()
30439
+ locationX: touch.locationX,
30440
+ locationY: touch.locationY,
30441
+ target: describeTouchTarget(target),
30442
+ targetId: target?.testID,
30443
+ screenName,
30444
+ ui,
30445
+ stateStatus: ui ? "captured_nonempty" : "not_instrumented",
30446
+ emitAutomaticAnalytics: captureAutomaticInteractions
29981
30447
  };
30448
+ touchStartRef.current = start;
30449
+ interactionStartsRef.current = interactionStartsRef.current.filter((candidate) => start.occurredAt - candidate.occurredAt < 5e3).concat(start).slice(-8);
29982
30450
  } catch {
29983
30451
  }
29984
30452
  };
@@ -30009,21 +30477,29 @@ function ScaleBunDebugRoot({
30009
30477
  }
30010
30478
  measureRoot();
30011
30479
  const rootRect = rootRectRef.current;
30012
- const tapped = describeTouchTarget(resolveTouchTarget(e));
30480
+ const tapped = start?.target ?? describeTouchTarget(resolveTouchTarget(e));
30013
30481
  const gestureDetails = {
30014
30482
  target: tapped,
30015
- x: touch.pageX,
30016
- y: touch.pageY,
30017
- pageX: touch.pageX,
30018
- pageY: touch.pageY,
30019
- locationX: touch.locationX,
30020
- locationY: touch.locationY,
30483
+ interactionId: start?.interactionId ?? generateInteractionId(),
30484
+ interactionProtocol: INTERACTION_PROTOCOL_VERSION,
30485
+ occurredAt: start?.occurredAt ?? Date.now(),
30486
+ ui: start?.ui,
30487
+ stateStatus: start?.stateStatus ?? "not_captured",
30488
+ targetId: start?.targetId,
30489
+ screenName: start?.screenName,
30490
+ emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
30491
+ x: start?.x ?? touch.pageX,
30492
+ y: start?.y ?? touch.pageY,
30493
+ pageX: start?.x ?? touch.pageX,
30494
+ pageY: start?.y ?? touch.pageY,
30495
+ locationX: start?.locationX ?? touch.locationX,
30496
+ locationY: start?.locationY ?? touch.locationY,
30021
30497
  viewportWidth: vpW > 0 ? vpW : void 0,
30022
30498
  viewportHeight: vpH > 0 ? vpH : void 0
30023
30499
  };
30024
30500
  if (rootRect && rootRect.w > 0 && rootRect.h > 0) {
30025
- const nx = Math.max(0, Math.min(1, touch.pageX / rootRect.w));
30026
- const ny = Math.max(0, Math.min(1, touch.pageY / rootRect.h));
30501
+ const nx = Math.max(0, Math.min(1, (start?.x ?? touch.pageX) / rootRect.w));
30502
+ const ny = Math.max(0, Math.min(1, (start?.y ?? touch.pageY) / rootRect.h));
30027
30503
  gestureDetails.normalizedX = nx;
30028
30504
  gestureDetails.normalizedY = ny;
30029
30505
  gestureDetails.normalizedPrecomputed = true;
@@ -30054,7 +30530,7 @@ function ScaleBunDebugRoot({
30054
30530
  const dx = touch.pageX - start.x;
30055
30531
  const dy = touch.pageY - start.y;
30056
30532
  const dist = Math.sqrt(dx * dx + dy * dy);
30057
- const duration = Date.now() - start.ts;
30533
+ const duration = Date.now() - start.occurredAt;
30058
30534
  gestureDetails.duration = duration;
30059
30535
  if (dist >= 15) {
30060
30536
  gestureDetails.endX = touch.pageX;
@@ -30072,47 +30548,16 @@ function ScaleBunDebugRoot({
30072
30548
  gestureType = "long_press";
30073
30549
  }
30074
30550
  }
30075
- const targetInfo = _extractTarget(e);
30076
- gestureDetails.target = targetInfo?.testId || targetInfo?.accessibilityLabel;
30077
- if (captureAutomaticInteractions) {
30078
- let screen;
30079
- try {
30080
- const {
30081
- AutoScreenDetector: AutoScreenDetector2
30082
- } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
30083
- screen = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
30084
- } catch {
30085
- }
30086
- emitAutomaticEvent("element_interacted", {
30087
- gesture_type: gestureType,
30088
- screen_name: screen,
30089
- // testID/nativeID is an author-controlled stable identifier.
30090
- // Accessibility labels and rendered text are deliberately omitted.
30091
- target_id: targetInfo?.testId,
30092
- normalized_x: gestureDetails.normalizedX,
30093
- normalized_y: gestureDetails.normalizedY,
30094
- direction: gestureDetails.direction,
30095
- duration_ms: gestureDetails.duration
30096
- });
30097
- }
30098
30551
  if (gestureType === "tap") {
30099
30552
  try {
30100
30553
  const {
30101
30554
  gestureTriggerDetector: gestureTriggerDetector2
30102
30555
  } = (init_gestureTriggerDetector(), __toCommonJS(gestureTriggerDetector_exports));
30103
- let screen;
30104
- try {
30105
- const {
30106
- AutoScreenDetector: AutoScreenDetector2
30107
- } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
30108
- screen = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
30109
- } catch {
30110
- }
30111
30556
  gestureTriggerDetector2.recordTap({
30112
- x: touch.pageX,
30113
- y: touch.pageY,
30114
- hasTarget: !!(targetInfo?.testId || targetInfo?.accessibilityLabel),
30115
- screen
30557
+ x: start?.x ?? touch.pageX,
30558
+ y: start?.y ?? touch.pageY,
30559
+ hasTarget: !!(start?.targetId || start?.target),
30560
+ screen: start?.screenName
30116
30561
  });
30117
30562
  } catch {
30118
30563
  }
@@ -30125,11 +30570,9 @@ function ScaleBunDebugRoot({
30125
30570
  const capturedDetails = {
30126
30571
  ...gestureDetails
30127
30572
  };
30128
- if (pendingJsEmitRef.current !== null) {
30129
- clearTimeout(pendingJsEmitRef.current);
30130
- }
30131
- pendingJsEmitRef.current = setTimeout(() => {
30132
- pendingJsEmitRef.current = null;
30573
+ const interactionId = capturedDetails.interactionId;
30574
+ const timer = setTimeout(() => {
30575
+ pendingJsEmitsRef.current.delete(interactionId);
30133
30576
  if (nativeTrackingConfirmedRef.current) return;
30134
30577
  try {
30135
30578
  const {
@@ -30146,6 +30589,7 @@ function ScaleBunDebugRoot({
30146
30589
  } catch {
30147
30590
  }
30148
30591
  }, 600);
30592
+ pendingJsEmitsRef.current.set(interactionId, timer);
30149
30593
  }
30150
30594
  touchStartRef.current = null;
30151
30595
  } catch {
@@ -30221,20 +30665,6 @@ function useScaleBunScreen(name) {
30221
30665
  }
30222
30666
  }, [name, manager]);
30223
30667
  }
30224
- function _extractTarget(e) {
30225
- try {
30226
- const target = e?.target;
30227
- if (!target) return void 0;
30228
- const props = target._internalFiberInstanceHandleDEV?.memoizedProps ?? {};
30229
- return {
30230
- testId: props.testID || props.nativeID || void 0,
30231
- accessibilityLabel: props.accessibilityLabel || void 0,
30232
- text: void 0
30233
- };
30234
- } catch {
30235
- return void 0;
30236
- }
30237
- }
30238
30668
  var styles4 = {
30239
30669
  root: {
30240
30670
  flex: 1