@scalebun/react-native 1.4.0 → 1.6.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/README.md +33 -0
- package/android/src/main/java/com/scalebun/rn/ota/BsPatch.kt +182 -0
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +88 -13
- package/android/src/oldarch/java/com/scalebun/rn/crash/ScaleBunCrashSpec.kt +16 -0
- package/bin/lib/androidCodemod.js +370 -0
- package/bin/scalebun.js +305 -13
- package/dist/scalebun.full.js +129 -97
- package/dist/scalebun.full.js.map +1 -1
- package/dist/scalebun.slim.js +129 -97
- package/dist/scalebun.slim.js.map +1 -1
- package/ios/Ota/BsPatch.swift +180 -0
- package/ios/Ota/OtaSlotManager.swift +59 -2
- package/ios/Ota/ScaleBunOtaModule.swift +89 -10
- package/lib/commonjs/analytics/EventTracker.js +21 -1
- package/lib/commonjs/analytics/EventTracker.js.map +1 -1
- package/lib/commonjs/core/constants/version.js +1 -1
- package/lib/commonjs/core/id/installationId.js +55 -0
- package/lib/commonjs/core/id/installationId.js.map +1 -0
- package/lib/commonjs/debug/bootstrap.js +12 -0
- package/lib/commonjs/debug/bootstrap.js.map +1 -1
- package/lib/commonjs/features/ota/OtaOrchestrator.js +2 -30
- package/lib/commonjs/features/ota/OtaOrchestrator.js.map +1 -1
- package/lib/commonjs/features/session/BackendSessionAdapter.js +8 -0
- package/lib/commonjs/features/session/BackendSessionAdapter.js.map +1 -1
- package/lib/module/analytics/EventTracker.js +21 -1
- package/lib/module/analytics/EventTracker.js.map +1 -1
- package/lib/module/core/constants/version.js +1 -1
- package/lib/module/core/id/installationId.js +50 -0
- package/lib/module/core/id/installationId.js.map +1 -0
- package/lib/module/debug/bootstrap.js +12 -0
- package/lib/module/debug/bootstrap.js.map +1 -1
- package/lib/module/features/ota/OtaOrchestrator.js +1 -29
- package/lib/module/features/ota/OtaOrchestrator.js.map +1 -1
- package/lib/module/features/session/BackendSessionAdapter.js +8 -0
- package/lib/module/features/session/BackendSessionAdapter.js.map +1 -1
- package/lib/typescript/analytics/EventTracker.d.ts.map +1 -1
- package/lib/typescript/core/constants/version.d.ts +1 -1
- package/lib/typescript/core/id/installationId.d.ts +20 -0
- package/lib/typescript/core/id/installationId.d.ts.map +1 -0
- package/lib/typescript/debug/bootstrap.d.ts +18 -0
- package/lib/typescript/debug/bootstrap.d.ts.map +1 -1
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts.map +1 -1
- package/lib/typescript/features/session/BackendSessionAdapter.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/analytics/EventTracker.ts +21 -1
- package/src/core/constants/version.ts +1 -1
- package/src/core/id/installationId.ts +52 -0
- package/src/debug/bootstrap.ts +36 -0
- package/src/features/ota/OtaOrchestrator.ts +1 -30
- package/src/features/session/BackendSessionAdapter.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalebun/react-native",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Production-grade React Native SDK for ScaleBun",
|
|
5
5
|
"main": "lib/commonjs/index",
|
|
6
6
|
"module": "lib/module/index",
|
|
@@ -104,7 +104,8 @@
|
|
|
104
104
|
"dependencies": {
|
|
105
105
|
"@babel/runtime": "^7.25.0",
|
|
106
106
|
"@jridgewell/sourcemap-codec": "1.5.5",
|
|
107
|
-
"@jridgewell/trace-mapping": "0.3.31"
|
|
107
|
+
"@jridgewell/trace-mapping": "0.3.31",
|
|
108
|
+
"@scalebun/cli": "^1.6.0"
|
|
108
109
|
},
|
|
109
110
|
"codegenConfig": {
|
|
110
111
|
"name": "ScaleBunSpec",
|
|
@@ -317,7 +317,27 @@ export class EventTracker {
|
|
|
317
317
|
const effectiveBatch = clampBatchSize(this.cfg.batchSize, DEFAULT_BATCH);
|
|
318
318
|
const batch = this.queue.slice(0, effectiveBatch);
|
|
319
319
|
try {
|
|
320
|
-
//
|
|
320
|
+
// WHAT THIS SIGNATURE IS, AND WHAT IT IS NOT.
|
|
321
|
+
//
|
|
322
|
+
// This used to be described as letting "the backend tamper-detect
|
|
323
|
+
// the payload", which overstates it to the point of being wrong.
|
|
324
|
+
// The HMAC key is `clientKey` — the same value sent beside it in
|
|
325
|
+
// `x-scalebun-client-key`, and a value that ships inside every
|
|
326
|
+
// installed app, extractable by anyone who unzips an APK. A MAC
|
|
327
|
+
// whose key travels with the message and is public by construction
|
|
328
|
+
// proves nothing about who sent it: anyone can compute a valid one.
|
|
329
|
+
//
|
|
330
|
+
// What it DOES buy is integrity in transit against non-adversarial
|
|
331
|
+
// corruption, and a cheap filter for junk traffic. Treat `clientKey`
|
|
332
|
+
// as a publishable identifier (like a Stripe publishable key), never
|
|
333
|
+
// as a trust boundary. Authenticity that actually holds has to come
|
|
334
|
+
// from something the client cannot forge — server-side rate limits
|
|
335
|
+
// and anomaly detection per key, short-lived device tokens, or
|
|
336
|
+
// platform attestation (Play Integrity / App Attest).
|
|
337
|
+
//
|
|
338
|
+
// Contrast with OTA bundle signing (features/ota/signature.ts),
|
|
339
|
+
// which is asymmetric: the private half never leaves the publisher,
|
|
340
|
+
// so a valid signature there IS proof of origin.
|
|
321
341
|
const payload = JSON.stringify({ events: batch });
|
|
322
342
|
// Wave 2 (§2F.2): a stable idempotency key over the batch's event ids so a
|
|
323
343
|
// retry after a timeout/disconnect replays the first response instead of
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createStorageBackend } from '../../storage/StorageBackend';
|
|
2
|
+
import { K_INSTALLATION_ID } from '../../analytics/EventTracker';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The SDK's canonical per-install id.
|
|
6
|
+
*
|
|
7
|
+
* ONE install, ONE id, every lane. This is deliberately NOT `getDeviceId()` —
|
|
8
|
+
* that is a separate value under a separate storage key, and the backend schema
|
|
9
|
+
* annotates the two as different ("stable per-install id from the SDK (≠
|
|
10
|
+
* deviceId)").
|
|
11
|
+
*
|
|
12
|
+
* It matters because the backend joins a bundle's OTA installs to that device's
|
|
13
|
+
* sessions through this value. OTA events have always been keyed by it; the
|
|
14
|
+
* session lane did not send it at all, so on the server the two could only be
|
|
15
|
+
* connected for devices that happened to have registered for push. Release
|
|
16
|
+
* health then reported "no data" for bundles that were running perfectly well —
|
|
17
|
+
* or crashing badly.
|
|
18
|
+
*
|
|
19
|
+
* Lives here rather than inside one feature so the OTA orchestrator and the
|
|
20
|
+
* session adapter cannot drift to two different ids.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveInstallationId(): string {
|
|
23
|
+
try {
|
|
24
|
+
const storage = createStorageBackend();
|
|
25
|
+
const existing = storage.get(K_INSTALLATION_ID);
|
|
26
|
+
if (existing) return existing;
|
|
27
|
+
const id = `inst-${randomSegment()}${randomSegment()}`;
|
|
28
|
+
storage.set(K_INSTALLATION_ID, id);
|
|
29
|
+
return id;
|
|
30
|
+
} catch {
|
|
31
|
+
// Storage unavailable (tests, exotic hosts): a per-process id keeps the
|
|
32
|
+
// lanes functional; it stabilizes on the first run where storage works.
|
|
33
|
+
return `inst-ephemeral-${randomSegment()}`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 8 chars of base36 entropy, crypto-backed when the runtime offers it. */
|
|
38
|
+
function randomSegment(): string {
|
|
39
|
+
try {
|
|
40
|
+
const g = globalThis as { crypto?: { getRandomValues?: (a: Uint8Array) => Uint8Array } };
|
|
41
|
+
if (g.crypto?.getRandomValues) {
|
|
42
|
+
const bytes = g.crypto.getRandomValues(new Uint8Array(6));
|
|
43
|
+
return Array.from(bytes)
|
|
44
|
+
.map((b) => b.toString(36).padStart(2, '0'))
|
|
45
|
+
.join('')
|
|
46
|
+
.slice(0, 8);
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// fall through to Math.random
|
|
50
|
+
}
|
|
51
|
+
return Math.random().toString(36).slice(2, 10).padEnd(8, '0');
|
|
52
|
+
}
|
package/src/debug/bootstrap.ts
CHANGED
|
@@ -67,6 +67,24 @@ export interface DebugConfig {
|
|
|
67
67
|
/** @internal */
|
|
68
68
|
_performanceDisabled?: boolean;
|
|
69
69
|
enableProfiler?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Permit the debug channel in a RELEASE build. Default false, and you
|
|
72
|
+
* almost certainly want it that way.
|
|
73
|
+
*
|
|
74
|
+
* The transport is an unencrypted `ws://` socket that carries captured
|
|
75
|
+
* console output, network traffic and the shared `secret` itself in
|
|
76
|
+
* cleartext, readable by anyone on the same Wi-Fi. That is an acceptable
|
|
77
|
+
* trade for a developer on their own machine and not acceptable in an app
|
|
78
|
+
* on a user's phone.
|
|
79
|
+
*
|
|
80
|
+
* Until now the only thing standing between those two cases was that the
|
|
81
|
+
* desktop tooling is excluded from bundles by default — a BUILD-time
|
|
82
|
+
* opt-out, not a release check. Anyone who set `devTools: true` to debug
|
|
83
|
+
* locally and shipped without reverting it published an app that opens
|
|
84
|
+
* that socket in production. This flag makes that a deliberate act rather
|
|
85
|
+
* than an oversight.
|
|
86
|
+
*/
|
|
87
|
+
allowInRelease?: boolean;
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
// ─── Singleton State ────────────────────────────────────────────────────────
|
|
@@ -115,6 +133,24 @@ export function enableDebug(config: DebugConfig): void {
|
|
|
115
133
|
return;
|
|
116
134
|
}
|
|
117
135
|
|
|
136
|
+
// Refuse in release builds. See `allowInRelease` on DebugConfig: this
|
|
137
|
+
// channel is plaintext ws:// carrying console output, network traffic
|
|
138
|
+
// and the shared secret, and the only previous guard was that the
|
|
139
|
+
// tooling is excluded from bundles by default — which a developer
|
|
140
|
+
// disables to debug locally and then forgets. Warn rather than fail
|
|
141
|
+
// silently, because a debug connection that does nothing with no
|
|
142
|
+
// explanation is its own long afternoon.
|
|
143
|
+
if (!__DEV__ && !config.allowInRelease) {
|
|
144
|
+
logger.warn(
|
|
145
|
+
'[ScaleBun] Debug connection refused in a release build. The debug ' +
|
|
146
|
+
'transport is unencrypted (ws://) and carries captured traffic and the ' +
|
|
147
|
+
'shared secret in cleartext, so it is development-only by default. If ' +
|
|
148
|
+
'you genuinely need it in a release build, set ' +
|
|
149
|
+
'`allowInRelease: true` — and do not ship that to users.',
|
|
150
|
+
);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
118
154
|
// The desktop-debugger tooling can be excluded from a build via
|
|
119
155
|
// `withScaleBun(config, { features: { devTools: false } })`, which resolves these modules
|
|
120
156
|
// to an empty stub. Detect that explicitly: without this check the first `new
|
|
@@ -13,7 +13,7 @@ import type { OtaCheckRequest, OtaCheckResponse, OtaBundlePayload } from './OtaT
|
|
|
13
13
|
import { otaEventEmitter } from './OtaEventEmitter';
|
|
14
14
|
import { getDeviceCountry, prefetchDeviceCountry } from './geoCountry';
|
|
15
15
|
import { deviceTargetingAttributes } from './deviceAttributes';
|
|
16
|
-
import {
|
|
16
|
+
import { resolveInstallationId } from '../../core/id/installationId';
|
|
17
17
|
import { verifyBundleSignature, type SignatureConfig } from './signature';
|
|
18
18
|
import { detectOtaEnvironment, type OtaEnvironment } from './environment';
|
|
19
19
|
import { retryWithBackoff, isRetryableNetworkError, HttpStatusError } from './retry';
|
|
@@ -71,35 +71,6 @@ function subscribeNativeProgress(
|
|
|
71
71
|
/** POST at most once per this interval; first and terminal ticks always go. */
|
|
72
72
|
const PROGRESS_POST_INTERVAL_MS = 1_000;
|
|
73
73
|
|
|
74
|
-
/**
|
|
75
|
-
* P8 — the SDK's canonical per-install id, shared with the analytics lane.
|
|
76
|
-
*
|
|
77
|
-
* OTA used to REQUIRE a caller-supplied `installationId`, and the documented
|
|
78
|
-
* example passed `deviceId` — so OTA telemetry lived in its own identity
|
|
79
|
-
* namespace, disconnected from the id every other lane reports. Every join
|
|
80
|
-
* from OTA data to CDP data (blast radius, MRR-at-risk, churn bands) was a
|
|
81
|
-
* cross-namespace join that matched by luck.
|
|
82
|
-
*
|
|
83
|
-
* Reads (or mints, for OTA-only apps that never initialize analytics) the same
|
|
84
|
-
* persisted value `EventTracker` uses, via the same storage backend and the
|
|
85
|
-
* same exported key — one install, one id, every lane.
|
|
86
|
-
*/
|
|
87
|
-
function resolveInstallationId(): string {
|
|
88
|
-
try {
|
|
89
|
-
const storage = createStorageBackend();
|
|
90
|
-
const existing = storage.get(K_INSTALLATION_ID);
|
|
91
|
-
if (existing) return existing;
|
|
92
|
-
const id = `inst-${Math.random().toString(36).slice(2, 10)}${Math.random()
|
|
93
|
-
.toString(36)
|
|
94
|
-
.slice(2, 10)}`;
|
|
95
|
-
storage.set(K_INSTALLATION_ID, id);
|
|
96
|
-
return id;
|
|
97
|
-
} catch {
|
|
98
|
-
// Storage unavailable (tests, exotic hosts): a per-process id keeps sync
|
|
99
|
-
// functional; it will stabilize on the first run where storage works.
|
|
100
|
-
return `inst-ephemeral-${Math.random().toString(36).slice(2, 10)}`;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
74
|
|
|
104
75
|
/**
|
|
105
76
|
* Deliver queued OTA lifecycle events to the backend's batch endpoint.
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { logger } from '../../core/logger/internalLogger';
|
|
27
27
|
import { base64ToBytes } from '../../core/encoding/base64';
|
|
28
28
|
import { ENDPOINTS } from '../../transport/http/endpoints';
|
|
29
|
+
import { resolveInstallationId } from '../../core/id/installationId';
|
|
29
30
|
import { SDK_VERSION } from '../../core/constants/version';
|
|
30
31
|
import { bridgeAdapter } from '../replay/bridge/adapters/bridgeAdapter';
|
|
31
32
|
import { getReplaySdkNative } from '../replay/bridge/nativeModule';
|
|
@@ -262,6 +263,13 @@ export class BackendSessionAdapter implements SessionTransport {
|
|
|
262
263
|
const payload = {
|
|
263
264
|
sessionId: session.sessionId,
|
|
264
265
|
deviceId: session.deviceId,
|
|
266
|
+
// The canonical per-install id — the SAME value OTA events are
|
|
267
|
+
// reported under, and a DIFFERENT value from deviceId. The
|
|
268
|
+
// backend stores it on the Device so release health can join a
|
|
269
|
+
// bundle's installs to that device's sessions. Without it, crash
|
|
270
|
+
// impact for a release resolves to "no data" for every device
|
|
271
|
+
// that never registered for push.
|
|
272
|
+
installationId: resolveInstallationId(),
|
|
265
273
|
startedAt: session.startedAt,
|
|
266
274
|
// Link the recording row to the always-on analytics row so the
|
|
267
275
|
// dashboard can join them. track() events land on the analytics
|