@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.
- package/android/src/main/java/com/scalebun/replaysdk/tracking/InteractionTracker.kt +25 -24
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +75 -0
- package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +12 -0
- package/dist/scalebun.full.js +653 -223
- package/dist/scalebun.slim.js +652 -222
- package/ios/Capture/InteractionTracker.swift +8 -4
- package/ios/Ota/ScaleBunOtaBridge.mm +6 -0
- package/ios/Ota/ScaleBunOtaModule.swift +67 -0
- package/lib/commonjs/analytics/EventTracker.js +5 -5
- package/lib/commonjs/analytics/automaticEvents.js +3 -2
- package/lib/commonjs/core/config/schema.js +34 -1
- package/lib/commonjs/core/constants/version.js +7 -2
- package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +80 -79
- package/lib/commonjs/features/journey/interactionProtocol.js +47 -0
- package/lib/commonjs/features/journey/uiState.js +94 -0
- package/lib/commonjs/features/ota/crypto/builtinVerifier.js +248 -0
- package/lib/commonjs/features/ota/crypto/loadEd25519.js +40 -0
- package/lib/commonjs/features/ota/crypto/loadSha512.js +40 -0
- package/lib/commonjs/features/ota/crypto/nativeVerifier.js +121 -0
- package/lib/commonjs/features/ota/signature.js +87 -27
- package/lib/commonjs/features/session/JourneyEventPipeline.js +6 -5
- package/lib/commonjs/features/session/SessionManager.js +37 -38
- package/lib/commonjs/metro/serializerCompose.js +32 -0
- package/lib/commonjs/public/ScaleBunFacade.js +74 -11
- package/lib/module/analytics/EventTracker.js +5 -5
- package/lib/module/analytics/automaticEvents.js +3 -2
- package/lib/module/core/config/schema.js +34 -1
- package/lib/module/core/constants/version.js +7 -2
- package/lib/module/features/journey/ScaleBunDebugRoot.js +80 -79
- package/lib/module/features/journey/interactionProtocol.js +38 -0
- package/lib/module/features/journey/uiState.js +86 -0
- package/lib/module/features/ota/crypto/builtinVerifier.js +240 -0
- package/lib/module/features/ota/crypto/loadEd25519.js +34 -0
- package/lib/module/features/ota/crypto/loadSha512.js +34 -0
- package/lib/module/features/ota/crypto/nativeVerifier.js +113 -0
- package/lib/module/features/ota/signature.js +87 -27
- package/lib/module/features/session/JourneyEventPipeline.js +6 -5
- package/lib/module/features/session/SessionManager.js +37 -38
- package/lib/module/metro/serializerCompose.js +32 -0
- package/lib/module/public/ScaleBunFacade.js +74 -11
- package/lib/typescript/analytics/EventTracker.d.ts +1 -1
- package/lib/typescript/analytics/automaticEvents.d.ts +3 -1
- package/lib/typescript/core/config/schema.d.ts +2 -0
- package/lib/typescript/core/constants/version.d.ts +7 -2
- package/lib/typescript/features/journey/interactionProtocol.d.ts +21 -0
- package/lib/typescript/features/journey/uiState.d.ts +53 -0
- package/lib/typescript/features/ota/OtaTypes.d.ts +50 -0
- package/lib/typescript/features/ota/crypto/builtinVerifier.d.ts +53 -0
- package/lib/typescript/features/ota/crypto/loadEd25519.d.ts +30 -0
- package/lib/typescript/features/ota/crypto/loadSha512.d.ts +15 -0
- package/lib/typescript/features/ota/crypto/nativeVerifier.d.ts +35 -0
- package/lib/typescript/features/ota/signature.d.ts +22 -7
- package/lib/typescript/features/session/JourneyEventPipeline.d.ts +1 -0
- package/lib/typescript/features/session/SessionManager.d.ts +15 -10
- package/lib/typescript/public/ScaleBunFacade.d.ts +35 -6
- package/lib/typescript/specs/NativeScaleBunOta.d.ts +23 -0
- package/package.json +19 -3
- package/src/analytics/EventTracker.ts +5 -5
- package/src/analytics/automaticEvents.ts +4 -0
- package/src/core/config/schema.ts +30 -3
- package/src/core/constants/version.ts +7 -2
- package/src/features/journey/ScaleBunDebugRoot.tsx +96 -75
- package/src/features/journey/interactionProtocol.ts +65 -0
- package/src/features/journey/uiState.ts +89 -0
- package/src/features/ota/OtaTypes.ts +51 -0
- package/src/features/ota/crypto/builtinVerifier.ts +257 -0
- package/src/features/ota/crypto/loadEd25519.ts +41 -0
- package/src/features/ota/crypto/loadSha512.ts +35 -0
- package/src/features/ota/crypto/nativeVerifier.ts +117 -0
- package/src/features/ota/signature.ts +108 -25
- package/src/features/session/JourneyEventPipeline.ts +7 -5
- package/src/features/session/SessionManager.ts +75 -38
- package/src/metro/serializerCompose.ts +38 -2
- package/src/public/ScaleBunFacade.ts +87 -13
- package/src/specs/NativeScaleBunOta.ts +24 -0
|
@@ -125,7 +125,7 @@ export declare class EventTracker {
|
|
|
125
125
|
* Persisted so it survives the click→install→open gap. Call BEFORE start() ideally.
|
|
126
126
|
*/
|
|
127
127
|
setAttributionClickId(clickId: string): void;
|
|
128
|
-
track(eventName: string, properties?: Record<string, any
|
|
128
|
+
track(eventName: string, properties?: Record<string, any>, timestamp?: number): void;
|
|
129
129
|
trackPurchase(input: {
|
|
130
130
|
revenue: number;
|
|
131
131
|
currency: string;
|
|
@@ -18,9 +18,11 @@ export type AutomaticEventName = 'app_foregrounded' | 'app_backgrounded' | 'scre
|
|
|
18
18
|
export interface AutomaticEvent {
|
|
19
19
|
name: AutomaticEventName;
|
|
20
20
|
properties: Record<string, unknown>;
|
|
21
|
+
/** Original observation time. Delivery can be delayed while native coordinates resolve. */
|
|
22
|
+
timestamp?: number;
|
|
21
23
|
}
|
|
22
24
|
type AutomaticEventListener = (event: AutomaticEvent) => void;
|
|
23
|
-
export declare function emitAutomaticEvent(name: AutomaticEventName, properties?: Record<string, unknown
|
|
25
|
+
export declare function emitAutomaticEvent(name: AutomaticEventName, properties?: Record<string, unknown>, timestamp?: number): void;
|
|
24
26
|
export declare function subscribeAutomaticEvents(listener: AutomaticEventListener): () => void;
|
|
25
27
|
/** Test-only reset; intentionally not exported from the package entry point. */
|
|
26
28
|
export declare function resetAutomaticEventsForTests(): void;
|
|
@@ -93,6 +93,8 @@ export interface ScaleBunConfig {
|
|
|
93
93
|
checkOnForeground: boolean;
|
|
94
94
|
channelOverride?: string;
|
|
95
95
|
publicSigningKey?: string;
|
|
96
|
+
/** Additional pinned keys for rotation; unioned with publicSigningKey. */
|
|
97
|
+
publicSigningKeys?: string[];
|
|
96
98
|
mandatoryBlocksUi: boolean;
|
|
97
99
|
};
|
|
98
100
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ScaleBun SDK version. Sent with the session-start envelope so the dashboard
|
|
3
3
|
* can attribute telemetry to the SDK build that produced it.
|
|
4
|
-
* Keep in sync with package.json "version".
|
|
4
|
+
* Keep in sync with package.json "version" — `version.test.ts` fails when they drift.
|
|
5
|
+
*
|
|
6
|
+
* Why the test matters: the 1.11.0 bump missed this line, so the build would have reported itself as
|
|
7
|
+
* 1.10.6. Every "is the release live, and on what share of traffic" question is answered from this
|
|
8
|
+
* value, so a stale one makes a rollout unobservable — which is the exact problem sending an SDK
|
|
9
|
+
* version was introduced to solve.
|
|
5
10
|
*/
|
|
6
|
-
export declare const SDK_VERSION = "1.
|
|
11
|
+
export declare const SDK_VERSION = "1.11.0";
|
|
7
12
|
//# sourceMappingURL=version.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** One physical interaction, shared by replay and analytics projections. */
|
|
2
|
+
export declare const INTERACTION_PROTOCOL_VERSION = 1;
|
|
3
|
+
export type InteractionStateStatus = 'captured_empty' | 'captured_nonempty' | 'not_captured' | 'not_instrumented';
|
|
4
|
+
export interface InteractionStartContext {
|
|
5
|
+
interactionId: string;
|
|
6
|
+
occurredAt: number;
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
target?: string;
|
|
10
|
+
targetId?: string;
|
|
11
|
+
screenName?: string;
|
|
12
|
+
ui?: string;
|
|
13
|
+
stateStatus: InteractionStateStatus;
|
|
14
|
+
emitAutomaticAnalytics: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function generateInteractionId(): string;
|
|
17
|
+
/** Match a native bridge event to the JS evidence sampled at the same finger-down. */
|
|
18
|
+
export declare function nearestInteractionStart<T extends InteractionStartContext>(starts: readonly T[], occurredAt: number, toleranceMs?: number): T | undefined;
|
|
19
|
+
/** Analytics is a projection of the same evidence; no second click is invented. */
|
|
20
|
+
export declare function automaticInteractionProperties(payload: Record<string, unknown>, screenName: string | undefined, canonicalMirror: boolean): Record<string, unknown>;
|
|
21
|
+
//# sourceMappingURL=interactionProtocol.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI STATE AT INTERACTION TIME — React Native.
|
|
3
|
+
*
|
|
4
|
+
* A screen is not just `Cart`. It is `Cart` with the filter sheet down, or up; with the address form
|
|
5
|
+
* expanded, or collapsed. Those are different interaction surfaces and a tap on one must never be
|
|
6
|
+
* counted on another — heat from the open sheet painted over the closed layout is a picture of
|
|
7
|
+
* something that never existed.
|
|
8
|
+
*
|
|
9
|
+
* HOW THIS DIFFERS FROM THE WEB, and the difference is not an omission:
|
|
10
|
+
*
|
|
11
|
+
* The web SDK ALSO detects state automatically, by reading `aria-expanded` and open `<dialog>`
|
|
12
|
+
* elements out of the live DOM. Those are platform-defined, so reading them is a measurement.
|
|
13
|
+
*
|
|
14
|
+
* React Native has no equivalent. There is no queryable tree of accessibility state that says a
|
|
15
|
+
* bottom sheet is up; a sheet is a component with a boolean in someone's store, and the only place
|
|
16
|
+
* that boolean exists is the host's own code. Guessing at it — from a modal's presence in the tree, or
|
|
17
|
+
* from a component name — would produce a state key that is right on some apps and silently wrong on
|
|
18
|
+
* others, and a wrong state key partitions taps into buckets corresponding to nothing.
|
|
19
|
+
*
|
|
20
|
+
* So on RN, state is DECLARED and never inferred. `ScaleBun.setUiState('filter-sheet', 'open')` is the
|
|
21
|
+
* whole mechanism, and its absence means "not captured" rather than "nothing was open". That
|
|
22
|
+
* distinction is carried all the way to the dashboard: a tap with no declared state is NULL, not '',
|
|
23
|
+
* and cannot be selected as a state or counted as one. Reporting it as "nothing was open" would be
|
|
24
|
+
* asserting a measurement nobody took.
|
|
25
|
+
*
|
|
26
|
+
* PAIRED WITH: whatzbug-web-sdk/packages/web/src/features/journey/uiState.ts — same wire format
|
|
27
|
+
* (`name:value` pairs, sorted, `;`-joined, <= 96 chars), same separator stripping, same explicit-wins
|
|
28
|
+
* precedence, so one dashboard control queries both platforms.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Declare the state of one UI dimension.
|
|
32
|
+
*
|
|
33
|
+
* Call it when the state CHANGES, not on every render: the value is read at interaction time, so it
|
|
34
|
+
* only has to be correct by the time the next tap lands.
|
|
35
|
+
*/
|
|
36
|
+
export declare function setUiState(name: string, value: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* Stop reporting a dimension — or, with no name, all of them.
|
|
39
|
+
*
|
|
40
|
+
* Later taps carry no value for it, which is not the same as carrying a value. Clearing all is what a
|
|
41
|
+
* screen unmount wants: declarations from the previous screen would otherwise follow the user onto a
|
|
42
|
+
* surface where they mean nothing, and every tap there would be filed under a state that was not on
|
|
43
|
+
* screen.
|
|
44
|
+
*/
|
|
45
|
+
export declare function clearUiState(name?: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* The signature for the interaction happening RIGHT NOW, or undefined when nothing is declared.
|
|
48
|
+
*
|
|
49
|
+
* undefined rather than '' deliberately — see the header. '' is the web's "we looked and nothing was
|
|
50
|
+
* open"; on RN there is nothing to look at, so the honest answer is silence.
|
|
51
|
+
*/
|
|
52
|
+
export declare function uiStateSignature(): string | undefined;
|
|
53
|
+
//# sourceMappingURL=uiState.d.ts.map
|
|
@@ -5,6 +5,49 @@
|
|
|
5
5
|
* Golden JSON fixtures in packages/protocol/fixtures/ota/ are the SINGLE source
|
|
6
6
|
* of truth; conformance tests in both repos assert against them to prevent drift.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
+
* OTA-TELEMETRY-SPEC — remaining telemetry fixes (from the 2026-08 audit)
|
|
11
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
12
|
+
* These close the SDK half of spec §84-86. All are ADDITIVE + backward-compatible
|
|
13
|
+
* (new optional fields, new enum members, new no-throw emit calls); old installed
|
|
14
|
+
* clients omit them and the backend ingests nullable/unknown fields. Each ALTERS
|
|
15
|
+
* DEVICE RUNTIME BEHAVIOR, so land them behind a real RN build + device/kill-test
|
|
16
|
+
* (Metro cannot exercise the boot-guard) — this file only carries the contract
|
|
17
|
+
* types + this spec, not the runtime wiring.
|
|
18
|
+
*
|
|
19
|
+
* 1. releaseId end-to-end (CRITICAL — root of the dashboard funnel mismatch)
|
|
20
|
+
* Backend already serves it: OtaBundlePayload.releaseId (done, this file +
|
|
21
|
+
* ota-check.service.ts). SDK TODO:
|
|
22
|
+
* - store payload.releaseId on the downloaded/installed bundle state, and
|
|
23
|
+
* - set `releaseId: bundle.releaseId` in the delivery mapper
|
|
24
|
+
* OtaOrchestrator.ts deliverOtaEvents (~:105) — currently only bundleId.
|
|
25
|
+
* Then ota_events carry releaseId and the funnel keys by release.
|
|
26
|
+
*
|
|
27
|
+
* 2. Emit CHECK + OFFERED (funnel top is currently unmeasurable)
|
|
28
|
+
* - CHECK: emit at the start of checkForUpdate (OtaOrchestrator.ts ~:480),
|
|
29
|
+
* before the fetch. ('CHECK' type already exists — no emit site today.)
|
|
30
|
+
* - OFFERED: add 'OFFERED' to OtaEventType, emit when checkRes.action ===
|
|
31
|
+
* 'DOWNLOAD' (~:619) before download begins.
|
|
32
|
+
*
|
|
33
|
+
* 3. Emit BOOT_SUCCESS (honest activation signal)
|
|
34
|
+
* Add 'BOOT_SUCCESS' to OtaEventType and emit it from the boot-guard
|
|
35
|
+
* markHealthy path (OtaOrchestrator.ts ~:863). INSTALLED (~:794) is emitted
|
|
36
|
+
* optimistically BEFORE the bundle boots; keep it (it means "staged+swapped")
|
|
37
|
+
* but let the dashboard measure real activation on BOOT_SUCCESS. Optionally
|
|
38
|
+
* add 'VERIFIED' after signature check (~:635).
|
|
39
|
+
*
|
|
40
|
+
* 4. Stamp the running OTA bundle onto session/crash telemetry
|
|
41
|
+
* SessionMetadata.bundleId is the NATIVE app package id, not the OTA bundle.
|
|
42
|
+
* Add optional otaBundleId?/otaBundleVersion? (distinct fields — do NOT
|
|
43
|
+
* overload bundleId) sourced from otaOrchestrator.getCurrentBundle() at
|
|
44
|
+
* session start (SessionManager.ts ~:451), so crashes attribute to the
|
|
45
|
+
* running bundle/version (release-health crash impact).
|
|
46
|
+
*
|
|
47
|
+
* Do NOT repurpose errorCode (it already collapses failure-error vs rollback-
|
|
48
|
+
* reason); add a new optional field if the two must be distinguished.
|
|
49
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
50
|
+
*/
|
|
8
51
|
export interface OtaCheckRequest {
|
|
9
52
|
appVersion: string;
|
|
10
53
|
platform: 'android' | 'ios';
|
|
@@ -43,6 +86,13 @@ export interface OtaCheckResponse {
|
|
|
43
86
|
export interface OtaBundlePayload {
|
|
44
87
|
id: string;
|
|
45
88
|
version: number;
|
|
89
|
+
/**
|
|
90
|
+
* The release this bundle is served AS (the backend now includes it — see
|
|
91
|
+
* ota.contracts.ts / ota-check.service.ts). Optional, for backward-compat with
|
|
92
|
+
* older backends. Carry it onto the emitted OtaEventItem.releaseId so telemetry
|
|
93
|
+
* is attributable by RELEASE, not just bundle. See OTA-TELEMETRY-SPEC below.
|
|
94
|
+
*/
|
|
95
|
+
releaseId?: string;
|
|
46
96
|
url: string;
|
|
47
97
|
size: number;
|
|
48
98
|
sha256: string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in ed25519 verifier, assembled from OPTIONAL peer dependencies.
|
|
3
|
+
*
|
|
4
|
+
* WHAT THIS REPLACES. Pinning `ota.publicSigningKey` used to require the host
|
|
5
|
+
* app to write its own `verifySignature` — roughly a hundred lines of hex and
|
|
6
|
+
* base64 decoding wrapped around a crypto library. That put the single most
|
|
7
|
+
* security-critical operation in the product, the one deciding whether remote
|
|
8
|
+
* code is authentic before it executes, in code the SDK could neither test nor
|
|
9
|
+
* audit. Three failure modes all landed on the app author:
|
|
10
|
+
*
|
|
11
|
+
* - A verifier that always returns `true` silently disables the feature, and
|
|
12
|
+
* nothing can detect it.
|
|
13
|
+
* - @noble's hash-provider property moved between major versions, so a
|
|
14
|
+
* routine dependency bump made every update fail closed with no signal
|
|
15
|
+
* pointing at the cause.
|
|
16
|
+
* - Hand-written base64 decoders tend to omit base64url, so a signature
|
|
17
|
+
* containing `-` or `_` is rejected as a forgery.
|
|
18
|
+
*
|
|
19
|
+
* All three are now the SDK's problem, which is where they belong. A host that
|
|
20
|
+
* installs `@noble/ed25519` and `@noble/hashes` gets verification by pinning a
|
|
21
|
+
* key and writing no code at all.
|
|
22
|
+
*
|
|
23
|
+
* WHY OPTIONAL AND NOT A HARD DEPENDENCY. Most apps never adopt bundle
|
|
24
|
+
* signing, and they should not carry curve arithmetic they will not run. The
|
|
25
|
+
* `verifySignature` hook remains supported and still WINS over this, for teams
|
|
26
|
+
* with their own crypto policy or a native implementation to delegate to.
|
|
27
|
+
*
|
|
28
|
+
* WHAT THIS DOES NOT FIX. Verification still happens in JS, inside the very
|
|
29
|
+
* bundle it protects. An attacker who lands one malicious bundle by other means
|
|
30
|
+
* can neuter the check for every update after it. Closing that needs native
|
|
31
|
+
* verification — CryptoKit on iOS (13.4+, already the deployment target) and
|
|
32
|
+
* `Signature.getInstance("Ed25519")` on Android API 33+, with a fallback below.
|
|
33
|
+
*/
|
|
34
|
+
import type { SignatureVerifier } from '../signature';
|
|
35
|
+
/**
|
|
36
|
+
* Decode a detached signature that may arrive hex- or base64-encoded.
|
|
37
|
+
*
|
|
38
|
+
* Tries the unambiguous case first: 128 hex characters is exactly 64 bytes and
|
|
39
|
+
* cannot be anything else. Otherwise base64, then hex as a last resort. Only a
|
|
40
|
+
* result of exactly 64 bytes is accepted, so a string that decodes under the
|
|
41
|
+
* wrong scheme is rejected rather than fed to the curve code as garbage.
|
|
42
|
+
*/
|
|
43
|
+
export declare function decodeSignature(sig: string): Uint8Array | null;
|
|
44
|
+
/**
|
|
45
|
+
* The built-in verifier, or null when the optional deps are not installed.
|
|
46
|
+
* Resolution is cached, including the negative result — a missing dependency
|
|
47
|
+
* does not become present at runtime, and retrying the require on every update
|
|
48
|
+
* check would be pure overhead.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getBuiltinVerifier(): SignatureVerifier | null;
|
|
51
|
+
/** Test seam — clears the cached resolution. */
|
|
52
|
+
export declare function _resetBuiltinVerifier(): void;
|
|
53
|
+
//# sourceMappingURL=builtinVerifier.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isolated lazy loader for the OPTIONAL `@noble/ed25519` dep.
|
|
3
|
+
*
|
|
4
|
+
* WHY ITS OWN FILE: Metro's dependency collector emits a dependency map one
|
|
5
|
+
* entry short for a module containing TWO OR MORE different-string inline
|
|
6
|
+
* `require()` calls, so the require indices desync and `_dependencyMap[N]`
|
|
7
|
+
* reads `undefined` ("Requiring unknown module 'undefined'"). Exactly ONE
|
|
8
|
+
* inline require per module avoids the miscount. `loadSha512` is a sibling file
|
|
9
|
+
* for the same reason — see `push/adapters/loadNotifee.ts`, which hit this first.
|
|
10
|
+
*
|
|
11
|
+
* The string must still be STATICALLY resolvable at bundle time; hosts that do
|
|
12
|
+
* not install it stub it via `withScaleBun`.
|
|
13
|
+
*/
|
|
14
|
+
/** The subset of the @noble/ed25519 surface this SDK uses. */
|
|
15
|
+
export interface NobleEd25519 {
|
|
16
|
+
verifyAsync?: (sig: Uint8Array, msg: Uint8Array, pub: Uint8Array) => Promise<boolean>;
|
|
17
|
+
verify?: (sig: Uint8Array, msg: Uint8Array, pub: Uint8Array) => boolean;
|
|
18
|
+
/** v3 shape: `ed.hashes.sha512` */
|
|
19
|
+
hashes?: {
|
|
20
|
+
sha512?: unknown;
|
|
21
|
+
sha512Async?: unknown;
|
|
22
|
+
};
|
|
23
|
+
/** v2 shape: `ed.etc.sha512Sync` */
|
|
24
|
+
etc?: {
|
|
25
|
+
sha512Sync?: unknown;
|
|
26
|
+
sha512Async?: unknown;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare function loadEd25519(): NobleEd25519 | null;
|
|
30
|
+
//# sourceMappingURL=loadEd25519.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isolated lazy loader for the OPTIONAL `@noble/hashes` SHA-512.
|
|
3
|
+
*
|
|
4
|
+
* Separate file from `loadEd25519` because Metro miscounts a module holding two
|
|
5
|
+
* different-string inline `require()` calls — see the comment there.
|
|
6
|
+
*
|
|
7
|
+
* ed25519 is defined in terms of SHA-512, and React Native has no native
|
|
8
|
+
* SHA-512, so @noble/ed25519 cannot verify anything until a hash provider is
|
|
9
|
+
* wired into it. Which property it expects differs by major version, so the
|
|
10
|
+
* wiring lives in `builtinVerifier`, not here; this module only obtains the
|
|
11
|
+
* function.
|
|
12
|
+
*/
|
|
13
|
+
export type Sha512Fn = (msg: Uint8Array) => Uint8Array;
|
|
14
|
+
export declare function loadSha512(): Sha512Fn | null;
|
|
15
|
+
//# sourceMappingURL=loadSha512.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native ed25519 verifier — platform crypto wrapped as a SignatureVerifier.
|
|
3
|
+
*
|
|
4
|
+
* WHY IT EXISTS. The built-in @noble verifier runs in JS, inside the very
|
|
5
|
+
* bundle it protects: an attacker who lands one malicious bundle can neuter a
|
|
6
|
+
* JS check for every update after it. Platform crypto (CryptoKit on iOS,
|
|
7
|
+
* Android's conscrypt on API 33+) sits outside the bundle's reach — and using
|
|
8
|
+
* it also drops the runtime dependency on the optional @noble peers wherever
|
|
9
|
+
* the OS provides ed25519.
|
|
10
|
+
*
|
|
11
|
+
* WHAT IT DOES NOT FIX, stated plainly: the ORCHESTRATION still lives in JS.
|
|
12
|
+
* A hostile bundle can skip calling any verifier and drive the native staging
|
|
13
|
+
* methods directly. Moving the crypto native shrinks the attack surface (no
|
|
14
|
+
* more tampering with a bundled crypto lib to flip a verdict) but the full
|
|
15
|
+
* close needs native-ENFORCED staging with a natively-pinned key — an
|
|
16
|
+
* architectural change tracked separately, not smuggled into this one.
|
|
17
|
+
*
|
|
18
|
+
* VERDICT CONTRACT (mirrors the spec): the native side resolves
|
|
19
|
+
* 'valid' | 'invalid' | 'unavailable'. 'invalid' is a definitive NO.
|
|
20
|
+
* 'unavailable' (Android < 33, malformed input, machinery failure) means this
|
|
21
|
+
* source cannot answer — the composed verifier below then delegates to the
|
|
22
|
+
* @noble builtin, and when that is absent too it THROWS, which the policy
|
|
23
|
+
* layer converts to a rejection. Every path that cannot verify refuses.
|
|
24
|
+
*/
|
|
25
|
+
import type { SignatureVerifier } from '../signature';
|
|
26
|
+
/**
|
|
27
|
+
* The native-first verifier, or null when the native module (or its
|
|
28
|
+
* verifyEd25519 method — an app running new JS against an old binary) is
|
|
29
|
+
* absent. Cached like the builtin: module availability does not change at
|
|
30
|
+
* runtime, and this is consulted on every update check.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getNativeVerifier(): SignatureVerifier | null;
|
|
33
|
+
/** Test seam — clears the cached resolution. */
|
|
34
|
+
export declare function _resetNativeVerifier(): void;
|
|
35
|
+
//# sourceMappingURL=nativeVerifier.d.ts.map
|
|
@@ -11,10 +11,17 @@
|
|
|
11
11
|
* download and nothing else. On a platform whose entire purpose is remote code
|
|
12
12
|
* delivery, that is the control that matters most.
|
|
13
13
|
*
|
|
14
|
-
* WHY IT IS SHAPED LIKE THIS.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* WHY IT IS SHAPED LIKE THIS. React Native has no built-in ed25519, so the
|
|
15
|
+
* verification primitive has to come from somewhere. It is resolved in order:
|
|
16
|
+
* a host-supplied verifier, else the built-in one assembled from the OPTIONAL
|
|
17
|
+
* `@noble/ed25519` + `@noble/hashes` peers, else nothing — and the SDK's job is
|
|
18
|
+
* to decide, unambiguously, what happens in that last case.
|
|
19
|
+
*
|
|
20
|
+
* The built-in path exists because requiring every adopter to hand-write a
|
|
21
|
+
* verifier put the most security-critical operation in the product in code the
|
|
22
|
+
* SDK could neither test nor audit, and made a routine dependency bump able to
|
|
23
|
+
* silently stop all updates. See `crypto/builtinVerifier.ts`. Signing stays
|
|
24
|
+
* opt-in, and apps that never adopt it carry no curve arithmetic.
|
|
18
25
|
*
|
|
19
26
|
* THE POLICY, which is the important part:
|
|
20
27
|
*
|
|
@@ -40,8 +47,16 @@ export type SignatureVerifier = (input: {
|
|
|
40
47
|
publicKey: string;
|
|
41
48
|
}) => boolean | Promise<boolean>;
|
|
42
49
|
export interface SignatureConfig {
|
|
43
|
-
/**
|
|
44
|
-
|
|
50
|
+
/**
|
|
51
|
+
* ed25519 public key(s) shipped in the app binary. Absent = signing not
|
|
52
|
+
* adopted. An ARRAY pins several keys at once and a bundle is accepted when
|
|
53
|
+
* ANY of them verifies — this is what makes key rotation possible without an
|
|
54
|
+
* app-store release: ship a build pinning [old, new], start signing with new,
|
|
55
|
+
* drop old from the next build. With a single pinnable key, losing the
|
|
56
|
+
* private key meant no OTA capability until a new binary cleared review —
|
|
57
|
+
* the exact emergency OTA exists to solve.
|
|
58
|
+
*/
|
|
59
|
+
publicKey?: string | string[];
|
|
45
60
|
/** Host-provided ed25519 verification function. */
|
|
46
61
|
verifier?: SignatureVerifier;
|
|
47
62
|
}
|
|
@@ -50,7 +65,7 @@ export type SignatureOutcome = {
|
|
|
50
65
|
reason: 'verified' | 'not_configured';
|
|
51
66
|
} | {
|
|
52
67
|
ok: false;
|
|
53
|
-
reason: 'no_verifier' | 'invalid_signature' | 'missing_signature' | 'verifier_threw';
|
|
68
|
+
reason: 'no_verifier' | 'invalid_signature' | 'missing_signature' | 'verifier_threw' | 'no_keys';
|
|
54
69
|
};
|
|
55
70
|
/**
|
|
56
71
|
* Decide whether a bundle may be staged.
|
|
@@ -50,6 +50,7 @@ export declare class JourneyEventPipeline {
|
|
|
50
50
|
journeyId?: string;
|
|
51
51
|
/** Frame that was on screen when this happened. Resolved by the caller; see sessionTypes. */
|
|
52
52
|
frameId?: string;
|
|
53
|
+
timestamp?: number;
|
|
53
54
|
}): JourneyEvent | null;
|
|
54
55
|
/**
|
|
55
56
|
* Anchor a crash event — protect nearby events from eviction.
|
|
@@ -17,6 +17,7 @@ import type { DebugTransport } from '../../debug/transport';
|
|
|
17
17
|
import type { SessionMetadata, SessionReplayConfig, JourneyEventType, JourneyEventSeverity, JourneyEventSource, CrashAnchor, NativeSyncMetadata, SyncDecisionCallback } from './sessionTypes';
|
|
18
18
|
import { type NativeCaptureFn } from './ReplayCaptureManager';
|
|
19
19
|
import type { BackendSessionAdapter } from './BackendSessionAdapter';
|
|
20
|
+
import { type InteractionStateStatus } from '../journey/interactionProtocol';
|
|
20
21
|
export interface SessionManagerConfig {
|
|
21
22
|
sessionReplay?: Partial<SessionReplayConfig>;
|
|
22
23
|
syncPolicy?: SyncDecisionCallback;
|
|
@@ -43,15 +44,6 @@ export declare class SessionManager {
|
|
|
43
44
|
* lane even when no replay recording is active. Additive, opt-in (default off).
|
|
44
45
|
*/
|
|
45
46
|
private _captureInteractionHeatmap;
|
|
46
|
-
/**
|
|
47
|
-
* Sampling cap for the analytics-lane heatmap emission. Now that capture is
|
|
48
|
-
* ON by default, an unbounded one-event-per-gesture stream could materially
|
|
49
|
-
* inflate ingest volume. We cap emitted interactions per analytics-session
|
|
50
|
-
* window (finalize-scoped per foreground): the first N gestures define the
|
|
51
|
-
* hotspot shape; the long tail is dropped. Resets when the window changes.
|
|
52
|
-
*/
|
|
53
|
-
private _heatmapWindowSessionId;
|
|
54
|
-
private _heatmapWindowCount;
|
|
55
47
|
private session;
|
|
56
48
|
private active;
|
|
57
49
|
private timeoutTimer;
|
|
@@ -207,6 +199,8 @@ export declare class SessionManager {
|
|
|
207
199
|
traceId?: string;
|
|
208
200
|
source?: JourneyEventSource;
|
|
209
201
|
journeyId?: string;
|
|
202
|
+
/** Original observation time; interaction capture may resolve asynchronously. */
|
|
203
|
+
timestamp?: number;
|
|
210
204
|
}): void;
|
|
211
205
|
/**
|
|
212
206
|
* Anchor a crash in the session context.
|
|
@@ -223,7 +217,10 @@ export declare class SessionManager {
|
|
|
223
217
|
* Notify of a user interaction. Called by ScaleBunDebugRoot touch handlers.
|
|
224
218
|
* Also triggers frame capture for desktop-initiated recordings.
|
|
225
219
|
*/
|
|
226
|
-
onUserAction(subtype: string, payload?: Record<string, unknown
|
|
220
|
+
onUserAction(subtype: string, payload?: Record<string, unknown>, context?: {
|
|
221
|
+
screen?: string;
|
|
222
|
+
timestamp?: number;
|
|
223
|
+
}): void;
|
|
227
224
|
/**
|
|
228
225
|
* Detailed gesture detection — called by ScaleBunDebugRoot touch handlers.
|
|
229
226
|
* Emits a USER_ACTION event with gesture-specific subtype and payload.
|
|
@@ -263,6 +260,14 @@ export declare class SessionManager {
|
|
|
263
260
|
screenWidth?: number;
|
|
264
261
|
screenHeight?: number;
|
|
265
262
|
platform?: string;
|
|
263
|
+
interactionId?: string;
|
|
264
|
+
interactionProtocol?: number;
|
|
265
|
+
occurredAt?: number;
|
|
266
|
+
ui?: string;
|
|
267
|
+
stateStatus?: InteractionStateStatus;
|
|
268
|
+
targetId?: string;
|
|
269
|
+
screenName?: string;
|
|
270
|
+
emitAutomaticAnalytics?: boolean;
|
|
266
271
|
}): void;
|
|
267
272
|
/**
|
|
268
273
|
* Manual frame capture — triggered by Desktop "Capture Step" button.
|
|
@@ -69,12 +69,17 @@ declare class ScaleBunFacade {
|
|
|
69
69
|
/**
|
|
70
70
|
* Boot the OTA orchestrator when the init config asks for it.
|
|
71
71
|
*
|
|
72
|
-
* `publicSigningKey`
|
|
73
|
-
* from configuration alone. The
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
72
|
+
* `publicSigningKey` / `publicSigningKeys` are honoured here so signature
|
|
73
|
+
* enforcement is reachable from configuration alone. The list form exists
|
|
74
|
+
* for key ROTATION: a build pinning [old, new] keeps verifying while the
|
|
75
|
+
* server moves to the new key, so replacing a key never needs an emergency
|
|
76
|
+
* store release. Both fields merge (deduplicated) into one pinned set.
|
|
77
|
+
*
|
|
78
|
+
* Verification comes from `ota.verifySignature` when supplied, else the
|
|
79
|
+
* built-in @noble-based verifier (optional peers). Pinning keys with
|
|
80
|
+
* NEITHER available is fail-CLOSED by design (signature.ts) — an update
|
|
81
|
+
* that cannot be verified is not installed. We say that out loud rather
|
|
82
|
+
* than letting the app discover it as a silent no-update condition.
|
|
78
83
|
*/
|
|
79
84
|
private _maybeStartOta;
|
|
80
85
|
private _autoEnableDebug;
|
|
@@ -148,6 +153,30 @@ declare class ScaleBunFacade {
|
|
|
148
153
|
variant: (key: string) => string | null;
|
|
149
154
|
};
|
|
150
155
|
/** Convenience: emit a Phase 1 purchase event (revenue + currency + transaction_id). */
|
|
156
|
+
/**
|
|
157
|
+
* Declare which UI state the user is looking at, so taps are attributed to the surface they
|
|
158
|
+
* happened on rather than averaged across every variant of the screen.
|
|
159
|
+
*
|
|
160
|
+
* ScaleBun.setUiState('filter-sheet', 'open');
|
|
161
|
+
* ScaleBun.setUiState('checkout-step', 'payment');
|
|
162
|
+
*
|
|
163
|
+
* Call it when the state CHANGES — the value is read at tap time, so it only has to be right by
|
|
164
|
+
* the time the next tap lands. On React Native this is the ONLY source of UI state: unlike the web,
|
|
165
|
+
* there is no queryable accessibility tree that says a sheet is up, and guessing would produce a
|
|
166
|
+
* state key that is right on some apps and silently wrong on others.
|
|
167
|
+
*
|
|
168
|
+
* Names and values are identifiers, not content: they become query keys in the dashboard, so a
|
|
169
|
+
* person's name or a cart total does not belong in one. `;`, `:` and `|` are stripped and both
|
|
170
|
+
* halves are capped at 32 characters.
|
|
171
|
+
*/
|
|
172
|
+
setUiState(name: string, value: string): void;
|
|
173
|
+
/**
|
|
174
|
+
* Stop reporting a UI state dimension — or, with no argument, all of them.
|
|
175
|
+
*
|
|
176
|
+
* Clear on screen unmount. A declaration left behind follows the user onto a surface where it means
|
|
177
|
+
* nothing, and every tap there is filed under a state that was not on screen.
|
|
178
|
+
*/
|
|
179
|
+
clearUiState(name?: string): void;
|
|
151
180
|
trackPurchase(input: {
|
|
152
181
|
revenue: number;
|
|
153
182
|
currency: string;
|
|
@@ -76,6 +76,29 @@ export interface Spec extends TurboModule {
|
|
|
76
76
|
* `recreateReactContextInBackground()` (bridge). iOS: RCTReloadCommand.
|
|
77
77
|
*/
|
|
78
78
|
restartApp(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Verify a detached ed25519 signature in NATIVE code.
|
|
81
|
+
*
|
|
82
|
+
* Exists because the JS verifier runs inside the very bundle it protects —
|
|
83
|
+
* an attacker who lands one malicious bundle can neuter a JS check for every
|
|
84
|
+
* update after it. Platform crypto is outside the bundle's reach, and it also
|
|
85
|
+
* removes the runtime dependency on the optional @noble peers wherever the OS
|
|
86
|
+
* provides ed25519 (iOS 13+ CryptoKit everywhere; Android API 33+).
|
|
87
|
+
*
|
|
88
|
+
* All three arguments are lowercase hex, pre-validated by the JS caller:
|
|
89
|
+
* the 32-byte bundle SHA-256 (the signature is over its RAW BYTES, matching
|
|
90
|
+
* the CLI's signing contract), the 64-byte signature, the 32-byte public key.
|
|
91
|
+
*
|
|
92
|
+
* Resolves a verdict STRING, not a boolean, because "the signature is wrong"
|
|
93
|
+
* and "this OS cannot check" must never collapse into one value:
|
|
94
|
+
* 'valid' — signature verifies under the key
|
|
95
|
+
* 'invalid' — it does not; the caller must refuse the bundle
|
|
96
|
+
* 'unavailable' — this OS level has no ed25519 (Android < 33); the caller
|
|
97
|
+
* falls back to the JS verifier
|
|
98
|
+
* A rejected promise is machinery failure and the caller treats it as
|
|
99
|
+
* 'unavailable' — never as 'valid'.
|
|
100
|
+
*/
|
|
101
|
+
verifyEd25519(messageHex: string, signatureHex: string, publicKeyHex: string): Promise<string>;
|
|
79
102
|
}
|
|
80
103
|
/**
|
|
81
104
|
* Resolved via `get` (not `getEnforcing`) so the SDK degrades gracefully when
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalebun/react-native",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "React Native SDK for ScaleBun",
|
|
5
5
|
"main": "lib/commonjs/index",
|
|
6
6
|
"module": "lib/module/index",
|
|
@@ -69,11 +69,15 @@
|
|
|
69
69
|
"registry": "https://registry.npmjs.org/"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
+
"@babel/preset-env": "^8.0.2",
|
|
73
|
+
"@noble/ed25519": "^3.1.0",
|
|
74
|
+
"@noble/hashes": "^2.3.0",
|
|
72
75
|
"@react-native-community/eslint-config": "^3.2.0",
|
|
73
76
|
"@types/jest": "^29.5.12",
|
|
74
77
|
"@types/node": "^25.3.3",
|
|
75
78
|
"@types/react": "^18.2.0",
|
|
76
79
|
"@types/react-native": "^0.73.0",
|
|
80
|
+
"babel-jest": "^30.4.1",
|
|
77
81
|
"del-cli": "^5.1.0",
|
|
78
82
|
"esbuild": "0.25.0",
|
|
79
83
|
"eslint": "^8.57.0",
|
|
@@ -92,7 +96,9 @@
|
|
|
92
96
|
"expo-router": ">=1.0.0",
|
|
93
97
|
"react": ">=18.0.0",
|
|
94
98
|
"react-native": ">=0.74.0",
|
|
95
|
-
"react-native-view-shot": ">=3.0.0"
|
|
99
|
+
"react-native-view-shot": ">=3.0.0",
|
|
100
|
+
"@noble/ed25519": ">=2.0.0",
|
|
101
|
+
"@noble/hashes": ">=2.0.0"
|
|
96
102
|
},
|
|
97
103
|
"peerDependenciesMeta": {
|
|
98
104
|
"react-native-view-shot": {
|
|
@@ -103,6 +109,12 @@
|
|
|
103
109
|
},
|
|
104
110
|
"expo-router": {
|
|
105
111
|
"optional": true
|
|
112
|
+
},
|
|
113
|
+
"@noble/ed25519": {
|
|
114
|
+
"optional": true
|
|
115
|
+
},
|
|
116
|
+
"@noble/hashes": {
|
|
117
|
+
"optional": true
|
|
106
118
|
}
|
|
107
119
|
},
|
|
108
120
|
"dependencies": {
|
|
@@ -139,6 +151,10 @@
|
|
|
139
151
|
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
140
152
|
"clean": "del-cli lib dist",
|
|
141
153
|
"build": "bob build && node scripts/build-dist.mjs",
|
|
142
|
-
"watch": "bob build --watch"
|
|
154
|
+
"watch": "bob build --watch",
|
|
155
|
+
"test:screens": "node --experimental-transform-types --import ./scripts/rn-globals.mjs --import ./scripts/register-ts-ext.mjs --test scripts/screen-detection.test.ts",
|
|
156
|
+
"test:uistate": "node --experimental-transform-types --import ./scripts/rn-globals.mjs --import ./scripts/register-ts-ext.mjs --test scripts/ui-state.test.ts",
|
|
157
|
+
"typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json",
|
|
158
|
+
"test:protocol": "node --experimental-transform-types --import ./scripts/rn-globals.mjs --import ./scripts/register-ts-ext.mjs --test scripts/interaction-protocol.test.ts"
|
|
143
159
|
}
|
|
144
160
|
}
|
|
@@ -172,7 +172,7 @@ export class EventTracker {
|
|
|
172
172
|
this.started = true;
|
|
173
173
|
if (this.cfg.automaticEventTracking) {
|
|
174
174
|
this.automaticEventsUnsubscribe = subscribeAutomaticEvents((event) => {
|
|
175
|
-
this.track(event.name, event.properties);
|
|
175
|
+
this.track(event.name, event.properties, event.timestamp);
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
178
|
if (this.cfg.autoLifecycleEvents) {
|
|
@@ -284,9 +284,9 @@ export class EventTracker {
|
|
|
284
284
|
|
|
285
285
|
// ─── tracking ────────────────────────────────────────────────────────────
|
|
286
286
|
|
|
287
|
-
track(eventName: string, properties?: Record<string, any
|
|
287
|
+
track(eventName: string, properties?: Record<string, any>, timestamp?: number): void {
|
|
288
288
|
try {
|
|
289
|
-
this.enqueue(this.buildEnvelope(eventName, properties));
|
|
289
|
+
this.enqueue(this.buildEnvelope(eventName, properties, timestamp));
|
|
290
290
|
try { this.cfg.onEvent?.(eventName); } catch { /* no-throw */ }
|
|
291
291
|
} catch (err) {
|
|
292
292
|
logger.warn(`[ScaleBun.events] track failed: ${(err as Error)?.message}`);
|
|
@@ -408,7 +408,7 @@ export class EventTracker {
|
|
|
408
408
|
|
|
409
409
|
// ─── internals ─────────────────────────────────────────────────────────────
|
|
410
410
|
|
|
411
|
-
private buildEnvelope(eventName: string, properties?: Record<string, any
|
|
411
|
+
private buildEnvelope(eventName: string, properties?: Record<string, any>, timestamp?: number): Envelope {
|
|
412
412
|
const ctx = this.cfg.context ?? {};
|
|
413
413
|
let canonicalSessionId: string | null | undefined;
|
|
414
414
|
try {
|
|
@@ -419,7 +419,7 @@ export class EventTracker {
|
|
|
419
419
|
const env: Envelope = {
|
|
420
420
|
event_id: uuid(),
|
|
421
421
|
event_name: eventName,
|
|
422
|
-
event_time: Date.now(),
|
|
422
|
+
event_time: timestamp ?? Date.now(),
|
|
423
423
|
app_id: this.cfg.appId,
|
|
424
424
|
platform: this.cfg.platform ?? resolveEventPlatform(),
|
|
425
425
|
installation_id: this.installationId,
|