@scalebun/react-native 1.13.0 → 2.0.1
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/build.gradle +8 -0
- package/android/src/androidTest/java/com/scalebun/rn/ota/ScaleBunOtaVerifierInstrumentedTest.kt +112 -0
- package/android/src/main/java/com/scalebun/rn/ota/BundleDownloader.kt +10 -0
- package/android/src/main/java/com/scalebun/rn/ota/DeviceIntegrity.kt +75 -0
- package/android/src/main/java/com/scalebun/rn/ota/OtaProtocol.kt +244 -0
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaKeyRegistry.kt +100 -0
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +132 -62
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaReleaseVerifier.kt +129 -0
- package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +80 -1
- package/android/src/main/java/com/scalebun/rn/ota/TlsPinning.kt +118 -0
- package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +6 -8
- package/dist/scalebun.full.js +964 -533
- package/dist/scalebun.slim.js +962 -531
- package/ios/Ota/OtaProtocol.swift +242 -0
- package/ios/Ota/OtaSlotManager.swift +26 -0
- package/ios/Ota/ScaleBunOtaBridge.mm +6 -3
- package/ios/Ota/ScaleBunOtaKeyRegistry.swift +147 -0
- package/ios/Ota/ScaleBunOtaModule.swift +125 -53
- package/ios/Ota/ScaleBunOtaReleaseVerifier.swift +115 -0
- package/ios/Ota/ScaleBunOtaVerifierTests.swift +92 -0
- package/ios/ReplaySdk.swift +65 -2
- package/lib/commonjs/bootstrap/SDKBootstrapper.js +30 -0
- package/lib/commonjs/core/config/schema.js +19 -12
- package/lib/commonjs/core/constants/version.js +1 -1
- package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +293 -5
- package/lib/commonjs/features/journey/ScaleBunScrollView.js +28 -0
- package/lib/commonjs/features/journey/autoInstrumentScroll.js +161 -0
- package/lib/commonjs/features/journey/interactionProtocol.js +131 -10
- package/lib/commonjs/features/journey/scrollContext.js +146 -0
- package/lib/commonjs/features/journey/targetGeometry.js +164 -0
- package/lib/commonjs/features/journey/touchTarget.js +152 -24
- package/lib/commonjs/features/journey/uiState.js +81 -1
- package/lib/commonjs/features/navigation/AutoScreenDetector.js +74 -5
- package/lib/commonjs/features/network/NetworkFeature.js +39 -5
- package/lib/commonjs/features/network/thirdParty.js +90 -0
- package/lib/commonjs/features/ota/OtaOrchestrator.js +175 -80
- package/lib/commonjs/features/session/SessionManager.js +63 -0
- package/lib/commonjs/public/ScaleBunFacade.js +23 -57
- package/lib/module/bootstrap/SDKBootstrapper.js +30 -0
- package/lib/module/core/config/schema.js +19 -12
- package/lib/module/core/constants/version.js +1 -1
- package/lib/module/features/journey/ScaleBunDebugRoot.js +295 -7
- package/lib/module/features/journey/ScaleBunScrollView.js +28 -0
- package/lib/module/features/journey/autoInstrumentScroll.js +155 -0
- package/lib/module/features/journey/interactionProtocol.js +128 -8
- package/lib/module/features/journey/scrollContext.js +135 -0
- package/lib/module/features/journey/targetGeometry.js +155 -0
- package/lib/module/features/journey/touchTarget.js +151 -24
- package/lib/module/features/journey/uiState.js +78 -1
- package/lib/module/features/navigation/AutoScreenDetector.js +74 -5
- package/lib/module/features/network/NetworkFeature.js +38 -4
- package/lib/module/features/network/thirdParty.js +83 -0
- package/lib/module/features/ota/OtaOrchestrator.js +175 -80
- package/lib/module/features/session/SessionManager.js +63 -0
- package/lib/module/public/ScaleBunFacade.js +23 -57
- package/lib/typescript/core/config/schema.d.ts +2 -3
- package/lib/typescript/core/constants/version.d.ts +1 -1
- package/lib/typescript/features/journey/autoInstrumentScroll.d.ts +46 -0
- package/lib/typescript/features/journey/interactionProtocol.d.ts +139 -1
- package/lib/typescript/features/journey/scrollContext.d.ts +85 -0
- package/lib/typescript/features/journey/targetGeometry.d.ts +114 -0
- package/lib/typescript/features/journey/touchTarget.d.ts +148 -15
- package/lib/typescript/features/journey/uiState.d.ts +39 -0
- package/lib/typescript/features/navigation/AutoScreenDetector.d.ts +48 -2
- package/lib/typescript/features/network/index.d.ts +18 -0
- package/lib/typescript/features/network/thirdParty.d.ts +65 -0
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts +12 -8
- package/lib/typescript/features/ota/OtaTypes.d.ts +22 -0
- package/lib/typescript/features/session/SessionManager.d.ts +55 -0
- package/lib/typescript/public/ScaleBunFacade.d.ts +5 -11
- package/lib/typescript/public/types.d.ts +10 -0
- package/lib/typescript/specs/NativeScaleBunOta.d.ts +32 -20
- package/package.json +3 -13
- package/scalebun-react-native.podspec +4 -0
- package/src/bootstrap/SDKBootstrapper.ts +40 -0
- package/src/core/config/schema.ts +21 -9
- package/src/core/constants/version.ts +1 -1
- package/src/features/journey/ScaleBunDebugRoot.tsx +288 -7
- package/src/features/journey/ScaleBunScrollView.tsx +29 -0
- package/src/features/journey/autoInstrumentScroll.ts +155 -0
- package/src/features/journey/interactionProtocol.ts +188 -10
- package/src/features/journey/scrollContext.ts +137 -0
- package/src/features/journey/targetGeometry.ts +176 -0
- package/src/features/journey/touchTarget.ts +237 -26
- package/src/features/journey/uiState.ts +80 -1
- package/src/features/navigation/AutoScreenDetector.ts +65 -5
- package/src/features/network/NetworkFeature.ts +41 -4
- package/src/features/network/index.ts +18 -0
- package/src/features/network/thirdParty.ts +92 -0
- package/src/features/ota/OtaOrchestrator.ts +190 -90
- package/src/features/ota/OtaTypes.ts +23 -1
- package/src/features/session/SessionManager.ts +113 -0
- package/src/public/ScaleBunFacade.ts +23 -69
- package/src/public/types.ts +10 -0
- package/src/specs/NativeScaleBunOta.ts +35 -20
- package/lib/commonjs/features/ota/crypto/builtinVerifier.js +0 -248
- package/lib/commonjs/features/ota/crypto/loadEd25519.js +0 -40
- package/lib/commonjs/features/ota/crypto/loadSha512.js +0 -40
- package/lib/commonjs/features/ota/crypto/nativeVerifier.js +0 -121
- package/lib/commonjs/features/ota/signature.js +0 -175
- package/lib/module/features/ota/crypto/builtinVerifier.js +0 -240
- package/lib/module/features/ota/crypto/loadEd25519.js +0 -34
- package/lib/module/features/ota/crypto/loadSha512.js +0 -34
- package/lib/module/features/ota/crypto/nativeVerifier.js +0 -113
- package/lib/module/features/ota/signature.js +0 -169
- package/lib/typescript/features/ota/crypto/builtinVerifier.d.ts +0 -53
- package/lib/typescript/features/ota/crypto/loadEd25519.d.ts +0 -30
- package/lib/typescript/features/ota/crypto/loadSha512.d.ts +0 -15
- package/lib/typescript/features/ota/crypto/nativeVerifier.d.ts +0 -35
- package/lib/typescript/features/ota/signature.d.ts +0 -81
- package/src/features/ota/crypto/builtinVerifier.ts +0 -257
- package/src/features/ota/crypto/loadEd25519.ts +0 -41
- package/src/features/ota/crypto/loadSha512.ts +0 -35
- package/src/features/ota/crypto/nativeVerifier.ts +0 -117
- package/src/features/ota/signature.ts +0 -206
|
@@ -235,7 +235,7 @@ class ScaleBunFacade {
|
|
|
235
235
|
const realQueue = coreContainer.get<PersistentQueue>('PersistentQueue');
|
|
236
236
|
const buffered = this._preInitBuffer.popBatch(this._preInitBuffer.length());
|
|
237
237
|
if (buffered.length > 0) {
|
|
238
|
-
|
|
238
|
+
logger.debug(`[ScaleBun] Draining ${buffered.length} pre-init events into persistent queue`);
|
|
239
239
|
for (const envelope of buffered) {
|
|
240
240
|
realQueue.add(envelope);
|
|
241
241
|
}
|
|
@@ -275,17 +275,11 @@ class ScaleBunFacade {
|
|
|
275
275
|
/**
|
|
276
276
|
* Boot the OTA orchestrator when the init config asks for it.
|
|
277
277
|
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* Verification comes from `ota.verifySignature` when supplied, else the
|
|
285
|
-
* built-in @noble-based verifier (optional peers). Pinning keys with
|
|
286
|
-
* NEITHER available is fail-CLOSED by design (signature.ts) — an update
|
|
287
|
-
* that cannot be verified is not installed. We say that out loud rather
|
|
288
|
-
* than letting the app discover it as a silent no-update condition.
|
|
278
|
+
* Signature enforcement is no longer configured from JS. Signed releases are
|
|
279
|
+
* verified and staged in NATIVE code against RSA keys embedded in the app
|
|
280
|
+
* package (SB-OTA-RSA-SHA256-V1); unsigned releases keep working on SHA-256
|
|
281
|
+
* integrity. There are no signing-key or `verifySignature` options to pass
|
|
282
|
+
* through — the host embeds its trusted keys natively.
|
|
289
283
|
*/
|
|
290
284
|
private _maybeStartOta(rawConfig: any): void {
|
|
291
285
|
const ota = rawConfig?.ota;
|
|
@@ -295,38 +289,6 @@ class ScaleBunFacade {
|
|
|
295
289
|
// module (or touch the native spec) at import time.
|
|
296
290
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
297
291
|
const { otaOrchestrator } = require('../features/ota/OtaOrchestrator');
|
|
298
|
-
const rawSingle = ota.publicSigningKey;
|
|
299
|
-
const rawList = ota.publicSigningKeys;
|
|
300
|
-
const publicKeys = Array.from(
|
|
301
|
-
new Set(
|
|
302
|
-
[
|
|
303
|
-
...(typeof rawSingle === 'string' && rawSingle ? [rawSingle] : []),
|
|
304
|
-
...(Array.isArray(rawList) ? rawList : []),
|
|
305
|
-
].filter((k: unknown): k is string => typeof k === 'string' && k.trim().length > 0),
|
|
306
|
-
),
|
|
307
|
-
);
|
|
308
|
-
// Signing was REQUESTED if a usable key exists, or the host passed a
|
|
309
|
-
// keys array at all — an empty one included. `publicSigningKeys: []`
|
|
310
|
-
// is a broken attempt to enable signing, and it must flow through to
|
|
311
|
-
// signature.ts's fail-closed handling, not silently disable signing.
|
|
312
|
-
const signingRequested = publicKeys.length > 0 || Array.isArray(rawList);
|
|
313
|
-
const verifier = ota.verifySignature;
|
|
314
|
-
if (signingRequested && typeof verifier !== 'function') {
|
|
315
|
-
// A host verifier is optional since the built-in one landed — the
|
|
316
|
-
// old unconditional warning here told every correctly-configured
|
|
317
|
-
// app that its updates would be rejected. Warn only when neither
|
|
318
|
-
// verifier can actually be resolved.
|
|
319
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
320
|
-
const { getBuiltinVerifier } = require('../features/ota/crypto/builtinVerifier');
|
|
321
|
-
if (!getBuiltinVerifier()) {
|
|
322
|
-
logger.warn(
|
|
323
|
-
'[ScaleBun] A signing key is pinned but no signature verifier is available. ' +
|
|
324
|
-
'Signature checking is fail-closed: updates will be REJECTED until one exists. ' +
|
|
325
|
-
'Install the optional peers `@noble/ed25519` + `@noble/hashes` (no further ' +
|
|
326
|
-
'code needed), or supply `ota.verifySignature`.',
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
292
|
// `healthyAfterMs` is the field BootGuardConfig actually declares.
|
|
331
293
|
// This passed `healthyTimeoutMs`, which nothing reads, so a host that
|
|
332
294
|
// tuned the boot-guard window silently got the 10s default instead.
|
|
@@ -339,16 +301,6 @@ class ScaleBunFacade {
|
|
|
339
301
|
: undefined;
|
|
340
302
|
|
|
341
303
|
otaOrchestrator.init({
|
|
342
|
-
...(signingRequested
|
|
343
|
-
? {
|
|
344
|
-
signature: {
|
|
345
|
-
// Single key stays a plain string — the shape every
|
|
346
|
-
// existing consumer and test already handles.
|
|
347
|
-
publicKey: publicKeys.length === 1 ? publicKeys[0] : publicKeys,
|
|
348
|
-
verifier,
|
|
349
|
-
},
|
|
350
|
-
}
|
|
351
|
-
: {}),
|
|
352
304
|
...(healthyAfterMs !== undefined ? { healthyAfterMs } : {}),
|
|
353
305
|
});
|
|
354
306
|
logger.info('[ScaleBun] OTA enabled from init config.');
|
|
@@ -417,7 +369,9 @@ class ScaleBunFacade {
|
|
|
417
369
|
}
|
|
418
370
|
const clientKey = this._clientKey;
|
|
419
371
|
const apiUrl = this._apiBaseUrl;
|
|
420
|
-
if (!clientKey || !apiUrl)
|
|
372
|
+
if (!clientKey || !apiUrl) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
421
375
|
|
|
422
376
|
this._otaCheckInFlight = true;
|
|
423
377
|
try {
|
|
@@ -478,7 +432,7 @@ class ScaleBunFacade {
|
|
|
478
432
|
if (!dbgConfig) return;
|
|
479
433
|
(dbgConfig as any).__skipDeprecationWarning = true;
|
|
480
434
|
this.enableDebug(dbgConfig);
|
|
481
|
-
|
|
435
|
+
logger.info('[ScaleBun] Debug auto-connected via init({ debug }).');
|
|
482
436
|
} catch (err: any) {
|
|
483
437
|
logger.warn('[ScaleBun] Auto debug connection failed:', err?.message);
|
|
484
438
|
}
|
|
@@ -495,7 +449,7 @@ class ScaleBunFacade {
|
|
|
495
449
|
const apiBaseUrl = this._apiBaseUrl;
|
|
496
450
|
if (!enabled || !appId || !clientKey || !apiBaseUrl) {
|
|
497
451
|
if (enabled && !appId) {
|
|
498
|
-
|
|
452
|
+
logger.info('[ScaleBun] Envelope event tracking disabled — no appId in init config.');
|
|
499
453
|
}
|
|
500
454
|
return;
|
|
501
455
|
}
|
|
@@ -547,7 +501,7 @@ class ScaleBunFacade {
|
|
|
547
501
|
onEvent: (eventName, revenue) => this._skanManager?.onEvent(eventName, revenue),
|
|
548
502
|
});
|
|
549
503
|
this._eventTracker.start();
|
|
550
|
-
|
|
504
|
+
logger.info('[ScaleBun] Envelope event tracking started → /v1/batch.');
|
|
551
505
|
|
|
552
506
|
// Complete the device context from the native bridge.
|
|
553
507
|
//
|
|
@@ -1222,7 +1176,7 @@ class ScaleBunFacade {
|
|
|
1222
1176
|
const cfg = await (this._getEngageTransport()?.fetchEngageConfig(context) ?? Promise.resolve(null));
|
|
1223
1177
|
const messages = cfg?.inAppMessages ?? [];
|
|
1224
1178
|
const previews = messages.filter((m) => m.preview === true || m.isTest === true);
|
|
1225
|
-
|
|
1179
|
+
logger.info(
|
|
1226
1180
|
`[Engage] debugFetchInAppMessages → ${messages.length} message(s), ${previews.length} test preview(s)`,
|
|
1227
1181
|
);
|
|
1228
1182
|
return messages;
|
|
@@ -1533,7 +1487,7 @@ class ScaleBunFacade {
|
|
|
1533
1487
|
logger.warn('SDK not initialized. Event dropped:', name);
|
|
1534
1488
|
return;
|
|
1535
1489
|
}
|
|
1536
|
-
|
|
1490
|
+
logger.debug('Tracking event:', name);
|
|
1537
1491
|
|
|
1538
1492
|
// Fan this event out to the Engage trigger engine. Same single funnel,
|
|
1539
1493
|
// extra observer — lets behavioral triggers (custom_event, failed_action,
|
|
@@ -1698,7 +1652,7 @@ class ScaleBunFacade {
|
|
|
1698
1652
|
]);
|
|
1699
1653
|
return;
|
|
1700
1654
|
}
|
|
1701
|
-
|
|
1655
|
+
logger.debug('Flushing events from persistent queue...');
|
|
1702
1656
|
const httpClient = coreContainer.get<HttpClient>('HttpClient');
|
|
1703
1657
|
await flushQueue(this._getQueue(), httpClient, getDeviceId());
|
|
1704
1658
|
});
|
|
@@ -1726,10 +1680,10 @@ class ScaleBunFacade {
|
|
|
1726
1680
|
const bootstrapPerfFeature = _getPerformanceFeature();
|
|
1727
1681
|
if (bootstrapPerfFeature) {
|
|
1728
1682
|
this._performanceFeature = bootstrapPerfFeature;
|
|
1729
|
-
|
|
1683
|
+
logger.debug('Performance feature linked from bootstrap');
|
|
1730
1684
|
}
|
|
1731
1685
|
|
|
1732
|
-
|
|
1686
|
+
logger.info('Debug enabled');
|
|
1733
1687
|
});
|
|
1734
1688
|
}
|
|
1735
1689
|
|
|
@@ -1884,14 +1838,14 @@ class ScaleBunFacade {
|
|
|
1884
1838
|
return false;
|
|
1885
1839
|
}
|
|
1886
1840
|
sm.startSession(metadata as Parameters<SessionManager['startSession']>[0]);
|
|
1887
|
-
|
|
1841
|
+
logger.info('session.start() ✅ sessionId:', sm.currentSessionId);
|
|
1888
1842
|
return true;
|
|
1889
1843
|
},
|
|
1890
1844
|
end: (reason?: 'ended' | 'timeout'): boolean => {
|
|
1891
1845
|
const sm = SessionManager.getExistingInstance();
|
|
1892
1846
|
if (!sm || !sm.isActive) return false;
|
|
1893
1847
|
sm.endSession(reason);
|
|
1894
|
-
|
|
1848
|
+
logger.info('session.end() ✅');
|
|
1895
1849
|
return true;
|
|
1896
1850
|
},
|
|
1897
1851
|
emitEvent: (type: string, data?: Record<string, unknown>) => {
|
|
@@ -1947,13 +1901,13 @@ class ScaleBunFacade {
|
|
|
1947
1901
|
/** Set SDK-wide privacy settings for report generation. */
|
|
1948
1902
|
setPrivacy: (config: PrivacyConfig): void => {
|
|
1949
1903
|
this._privacyConfig = { ...this._privacyConfig, ...config };
|
|
1950
|
-
|
|
1904
|
+
logger.debug('[report] Privacy config updated');
|
|
1951
1905
|
},
|
|
1952
1906
|
|
|
1953
1907
|
/** Set branding for exported reports. */
|
|
1954
1908
|
setBranding: (config: BrandingConfig): void => {
|
|
1955
1909
|
this._brandingConfig = { ...this._brandingConfig, ...config };
|
|
1956
|
-
|
|
1910
|
+
logger.debug('[report] Branding config updated');
|
|
1957
1911
|
},
|
|
1958
1912
|
|
|
1959
1913
|
/** Get current privacy config. */
|
|
@@ -2004,9 +1958,9 @@ class ScaleBunFacade {
|
|
|
2004
1958
|
try {
|
|
2005
1959
|
const { AutoScreenDetector } = require('../features/navigation/AutoScreenDetector');
|
|
2006
1960
|
AutoScreenDetector.getInstance().setNavigationRef(ref);
|
|
2007
|
-
|
|
1961
|
+
logger.info('[ScaleBun] Navigation ref set → auto screen detection active');
|
|
2008
1962
|
} catch (err: any) {
|
|
2009
|
-
|
|
1963
|
+
logger.debug('[ScaleBun] Navigation ref stashed (detector not ready):', err?.message);
|
|
2010
1964
|
}
|
|
2011
1965
|
});
|
|
2012
1966
|
}
|
package/src/public/types.ts
CHANGED
|
@@ -334,6 +334,16 @@ export interface SimplifiedInitConfig {
|
|
|
334
334
|
appId?: string;
|
|
335
335
|
/** SDK client key (SaaS mode). Also used to authenticate the envelope lane. */
|
|
336
336
|
clientKey?: string;
|
|
337
|
+
/**
|
|
338
|
+
* Patch React Native's ScrollView export so every scroll view reports its scroll offset,
|
|
339
|
+
* giving taps their content depth without the app swapping to ScaleBunScrollView.
|
|
340
|
+
*
|
|
341
|
+
* OFF BY DEFAULT because it changes what the host renders, not merely what the SDK records:
|
|
342
|
+
* every scroll view in the app — including ones inside libraries you did not write — routes
|
|
343
|
+
* through an SDK wrapper. Turn it on deliberately. With it off, taps on a plain ScrollView
|
|
344
|
+
* report no content depth and say so, rather than reporting a fabricated offset of 0.
|
|
345
|
+
*/
|
|
346
|
+
autoInstrumentScrollViews?: boolean;
|
|
337
347
|
/** Platform OVERRIDE — auto-detected from Platform.OS when omitted (integrations should omit it). */
|
|
338
348
|
platform?: 'ios' | 'android' | 'web' | 'react_native';
|
|
339
349
|
/**
|
|
@@ -25,6 +25,18 @@ export interface Spec extends TurboModule {
|
|
|
25
25
|
*/
|
|
26
26
|
getActiveBundle(): string | null;
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* JSON array (string) of the trusted key IDs embedded in the app package, e.g.
|
|
30
|
+
* `["production-2026-01"]`. Reported on OTA check-in for SDK trust coverage
|
|
31
|
+
* (CICDPAGE §12) so the dashboard can measure key-rotation adoption. This is
|
|
32
|
+
* telemetry ONLY — the server never trusts a key because a device claims it.
|
|
33
|
+
* Empty array when no keys are embedded.
|
|
34
|
+
*/
|
|
35
|
+
getTrustedKeyIds(): string;
|
|
36
|
+
|
|
37
|
+
/** JSON array of root/jailbreak indicator strings; [] when none found. */
|
|
38
|
+
getDeviceIntegrity(): string;
|
|
39
|
+
|
|
28
40
|
/**
|
|
29
41
|
* Returns JSON with current slot state:
|
|
30
42
|
* { current: { bundleId, version, sha256, installedAt } | null,
|
|
@@ -87,28 +99,31 @@ export interface Spec extends TurboModule {
|
|
|
87
99
|
restartApp(): void;
|
|
88
100
|
|
|
89
101
|
/**
|
|
90
|
-
* Verify a
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* provides ed25519 (iOS 13+ CryptoKit everywhere; Android API 33+).
|
|
102
|
+
* Verify a SB-OTA-RSA-SHA256-V1 release envelope in NATIVE code, with no side
|
|
103
|
+
* effects. `envelopeJson` is the JSON `{ protocol, keyId, signedPayload,
|
|
104
|
+
* signature, downloadUrl? }` from the check response. Verification uses the
|
|
105
|
+
* trusted RSA public keys EMBEDDED in the app package (never a key from JS or
|
|
106
|
+
* the server) and platform `SHA256withRSA` (present on every supported API
|
|
107
|
+
* level — no API-33 floor).
|
|
97
108
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
109
|
+
* Resolves a verdict JSON string: `{ ok: boolean, code?: string, message?,
|
|
110
|
+
* keyId?, releaseId?, bundleId?, releaseSequence?, artifactSha256?,
|
|
111
|
+
* artifactSize?, resultArtifactSha256?, fingerprint? }`. A failed check is
|
|
112
|
+
* `ok:false` with a stable `code`; a rejected promise is machinery failure and
|
|
113
|
+
* the caller must treat it as a failure, never as valid.
|
|
114
|
+
*/
|
|
115
|
+
verifyRelease(envelopeJson: string): Promise<string>;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Native-ENFORCED install: verify the RSA signature (as `verifyRelease`), then
|
|
119
|
+
* download the artifact, enforce the SIGNED size + SHA-256, and stage it into
|
|
120
|
+
* the `next` slot — all natively. JavaScript cannot stage bytes that were not
|
|
121
|
+
* proven authentic (once trusted keys are embedded, the legacy `stageBundle`
|
|
122
|
+
* path refuses). Does not activate; call `applyUpdate` to promote per the
|
|
123
|
+
* install mode. Resolves the same verdict JSON as `verifyRelease` (`ok:true`
|
|
124
|
+
* once staged).
|
|
110
125
|
*/
|
|
111
|
-
|
|
126
|
+
verifyAndStageRelease(envelopeJson: string): Promise<string>;
|
|
112
127
|
}
|
|
113
128
|
|
|
114
129
|
/**
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports._resetBuiltinVerifier = _resetBuiltinVerifier;
|
|
7
|
-
exports.decodeSignature = decodeSignature;
|
|
8
|
-
exports.getBuiltinVerifier = getBuiltinVerifier;
|
|
9
|
-
var _loadEd = require("./loadEd25519");
|
|
10
|
-
var _loadSha = require("./loadSha512");
|
|
11
|
-
var _internalLogger = require("../../../core/logger/internalLogger");
|
|
12
|
-
/**
|
|
13
|
-
* Built-in ed25519 verifier, assembled from OPTIONAL peer dependencies.
|
|
14
|
-
*
|
|
15
|
-
* WHAT THIS REPLACES. Pinning `ota.publicSigningKey` used to require the host
|
|
16
|
-
* app to write its own `verifySignature` — roughly a hundred lines of hex and
|
|
17
|
-
* base64 decoding wrapped around a crypto library. That put the single most
|
|
18
|
-
* security-critical operation in the product, the one deciding whether remote
|
|
19
|
-
* code is authentic before it executes, in code the SDK could neither test nor
|
|
20
|
-
* audit. Three failure modes all landed on the app author:
|
|
21
|
-
*
|
|
22
|
-
* - A verifier that always returns `true` silently disables the feature, and
|
|
23
|
-
* nothing can detect it.
|
|
24
|
-
* - @noble's hash-provider property moved between major versions, so a
|
|
25
|
-
* routine dependency bump made every update fail closed with no signal
|
|
26
|
-
* pointing at the cause.
|
|
27
|
-
* - Hand-written base64 decoders tend to omit base64url, so a signature
|
|
28
|
-
* containing `-` or `_` is rejected as a forgery.
|
|
29
|
-
*
|
|
30
|
-
* All three are now the SDK's problem, which is where they belong. A host that
|
|
31
|
-
* installs `@noble/ed25519` and `@noble/hashes` gets verification by pinning a
|
|
32
|
-
* key and writing no code at all.
|
|
33
|
-
*
|
|
34
|
-
* WHY OPTIONAL AND NOT A HARD DEPENDENCY. Most apps never adopt bundle
|
|
35
|
-
* signing, and they should not carry curve arithmetic they will not run. The
|
|
36
|
-
* `verifySignature` hook remains supported and still WINS over this, for teams
|
|
37
|
-
* with their own crypto policy or a native implementation to delegate to.
|
|
38
|
-
*
|
|
39
|
-
* WHAT THIS DOES NOT FIX. Verification still happens in JS, inside the very
|
|
40
|
-
* bundle it protects. An attacker who lands one malicious bundle by other means
|
|
41
|
-
* can neuter the check for every update after it. Closing that needs native
|
|
42
|
-
* verification — CryptoKit on iOS (13.4+, already the deployment target) and
|
|
43
|
-
* `Signature.getInstance("Ed25519")` on Android API 33+, with a fallback below.
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
const ED25519_SIGNATURE_BYTES = 64;
|
|
47
|
-
const ED25519_PUBLIC_KEY_BYTES = 32;
|
|
48
|
-
|
|
49
|
-
/** Hex → bytes. Returns null rather than throwing; callers treat null as "reject". */
|
|
50
|
-
function hexToBytes(hex) {
|
|
51
|
-
const clean = hex.trim().toLowerCase().replace(/^0x/, '');
|
|
52
|
-
if (clean.length === 0 || clean.length % 2 !== 0 || /[^0-9a-f]/.test(clean)) return null;
|
|
53
|
-
const out = new Uint8Array(clean.length / 2);
|
|
54
|
-
for (let i = 0; i < out.length; i++) {
|
|
55
|
-
out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
58
|
-
}
|
|
59
|
-
const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Base64 → bytes, accepting base64url as well.
|
|
63
|
-
*
|
|
64
|
-
* The `-`/`_` handling is the point. A verifier that omits it rejects a
|
|
65
|
-
* perfectly valid signature as a forgery the moment the signing side emits
|
|
66
|
-
* base64url, and the resulting symptom — updates silently stop installing —
|
|
67
|
-
* looks nothing like a decoding bug.
|
|
68
|
-
*/
|
|
69
|
-
function base64ToBytes(b64) {
|
|
70
|
-
const s = b64.trim().replace(/-/g, '+').replace(/_/g, '/').replace(/=+$/, '');
|
|
71
|
-
if (s.length === 0) return null;
|
|
72
|
-
const out = [];
|
|
73
|
-
let buffer = 0;
|
|
74
|
-
let bits = 0;
|
|
75
|
-
for (const ch of s) {
|
|
76
|
-
const idx = B64_ALPHABET.indexOf(ch);
|
|
77
|
-
if (idx === -1) return null;
|
|
78
|
-
buffer = buffer << 6 | idx;
|
|
79
|
-
bits += 6;
|
|
80
|
-
if (bits >= 8) {
|
|
81
|
-
bits -= 8;
|
|
82
|
-
out.push(buffer >> bits & 0xff);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return Uint8Array.from(out);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Decode a detached signature that may arrive hex- or base64-encoded.
|
|
90
|
-
*
|
|
91
|
-
* Tries the unambiguous case first: 128 hex characters is exactly 64 bytes and
|
|
92
|
-
* cannot be anything else. Otherwise base64, then hex as a last resort. Only a
|
|
93
|
-
* result of exactly 64 bytes is accepted, so a string that decodes under the
|
|
94
|
-
* wrong scheme is rejected rather than fed to the curve code as garbage.
|
|
95
|
-
*/
|
|
96
|
-
function decodeSignature(sig) {
|
|
97
|
-
const trimmed = sig.trim();
|
|
98
|
-
if (/^(0x)?[0-9a-fA-F]{128}$/.test(trimmed)) {
|
|
99
|
-
const hex = hexToBytes(trimmed);
|
|
100
|
-
if (hex && hex.length === ED25519_SIGNATURE_BYTES) return hex;
|
|
101
|
-
}
|
|
102
|
-
const b64 = base64ToBytes(trimmed);
|
|
103
|
-
if (b64 && b64.length === ED25519_SIGNATURE_BYTES) return b64;
|
|
104
|
-
const hex = hexToBytes(trimmed);
|
|
105
|
-
if (hex && hex.length === ED25519_SIGNATURE_BYTES) return hex;
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Give @noble its SHA-512 provider.
|
|
111
|
-
*
|
|
112
|
-
* The property it reads moved between major versions — v2 wants
|
|
113
|
-
* `etc.sha512Sync`, v3 wants `hashes.sha512` — so both are populated when
|
|
114
|
-
* present. Getting this wrong does not fail loudly: verification simply throws
|
|
115
|
-
* from inside the curve code and the outer catch converts it to "rejected",
|
|
116
|
-
* which is indistinguishable from an actual forgery. That ambiguity is exactly
|
|
117
|
-
* what made this a bad thing to ask app authors to maintain.
|
|
118
|
-
*/
|
|
119
|
-
function wireSha512(ed, sha512) {
|
|
120
|
-
// VARIADIC ON PURPOSE. @noble calls its hash provider with several byte
|
|
121
|
-
// arrays to be hashed as one concatenated message. A wrapper taking a
|
|
122
|
-
// single argument hashes only the first, silently produces the wrong
|
|
123
|
-
// digest, and every genuine signature is then reported as INVALID — no
|
|
124
|
-
// error, no exception, just an authenticity check that always says no.
|
|
125
|
-
// Verified against a real CLI-produced signature; the single-argument form
|
|
126
|
-
// failed every positive case while passing every negative one, which is
|
|
127
|
-
// the most misleading shape a bug of this kind can take.
|
|
128
|
-
const concat = parts => {
|
|
129
|
-
if (parts.length === 1) return parts[0];
|
|
130
|
-
let total = 0;
|
|
131
|
-
for (const p of parts) total += p.length;
|
|
132
|
-
const out = new Uint8Array(total);
|
|
133
|
-
let off = 0;
|
|
134
|
-
for (const p of parts) {
|
|
135
|
-
out.set(p, off);
|
|
136
|
-
off += p.length;
|
|
137
|
-
}
|
|
138
|
-
return out;
|
|
139
|
-
};
|
|
140
|
-
const sync = (...msgs) => sha512(concat(msgs));
|
|
141
|
-
const asyncFn = async (...msgs) => sha512(concat(msgs));
|
|
142
|
-
|
|
143
|
-
// EACH BRANCH GETS ITS OWN try. Wrapping both in one is a trap, and it cost
|
|
144
|
-
// an afternoon: under v3 the `hashes` assignment succeeds and the `etc`
|
|
145
|
-
// object is FROZEN, so the v2-shaped assignment throws "object is not
|
|
146
|
-
// extensible" — discarding a verifier that was, by then, already correctly
|
|
147
|
-
// wired. Only "neither shape took" means unavailable.
|
|
148
|
-
let wired = false;
|
|
149
|
-
try {
|
|
150
|
-
if (ed.hashes && typeof ed.hashes === 'object') {
|
|
151
|
-
if (typeof ed.hashes.sha512 !== 'function') ed.hashes.sha512 = sync;
|
|
152
|
-
if (typeof ed.hashes.sha512Async !== 'function') ed.hashes.sha512Async = asyncFn;
|
|
153
|
-
wired = typeof ed.hashes.sha512 === 'function';
|
|
154
|
-
}
|
|
155
|
-
} catch {
|
|
156
|
-
/* frozen or absent — try the other shape */
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
if (ed.etc && typeof ed.etc === 'object') {
|
|
160
|
-
if (typeof ed.etc.sha512Sync !== 'function') ed.etc.sha512Sync = sync;
|
|
161
|
-
if (typeof ed.etc.sha512Async !== 'function') ed.etc.sha512Async = asyncFn;
|
|
162
|
-
wired = wired || typeof ed.etc.sha512Sync === 'function';
|
|
163
|
-
}
|
|
164
|
-
} catch {
|
|
165
|
-
/* frozen or absent — `wired` already records whether the other shape took */
|
|
166
|
-
}
|
|
167
|
-
return wired;
|
|
168
|
-
}
|
|
169
|
-
let resolved;
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* The built-in verifier, or null when the optional deps are not installed.
|
|
173
|
-
* Resolution is cached, including the negative result — a missing dependency
|
|
174
|
-
* does not become present at runtime, and retrying the require on every update
|
|
175
|
-
* check would be pure overhead.
|
|
176
|
-
*/
|
|
177
|
-
function getBuiltinVerifier() {
|
|
178
|
-
if (resolved !== undefined) return resolved;
|
|
179
|
-
const ed = (0, _loadEd.loadEd25519)();
|
|
180
|
-
const sha512 = (0, _loadSha.loadSha512)();
|
|
181
|
-
if (!ed || !sha512) {
|
|
182
|
-
resolved = null;
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
if (!wireSha512(ed, sha512)) {
|
|
186
|
-
// A shape neither branch matched — a @noble major this SDK predates.
|
|
187
|
-
// Report "no verifier", which is actionable, rather than letting every
|
|
188
|
-
// update fail deep inside the curve code with an opaque error.
|
|
189
|
-
_internalLogger.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.');
|
|
190
|
-
resolved = null;
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
resolved = async ({
|
|
194
|
-
messageHex,
|
|
195
|
-
signature,
|
|
196
|
-
publicKey
|
|
197
|
-
}) => {
|
|
198
|
-
const message = hexToBytes(messageHex);
|
|
199
|
-
const pub = hexToBytes(publicKey);
|
|
200
|
-
const sig = decodeSignature(signature);
|
|
201
|
-
if (!message) {
|
|
202
|
-
_internalLogger.logger.error('[OTA] Bundle SHA-256 is not valid hex — refusing to stage.');
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
if (!pub || pub.length !== ED25519_PUBLIC_KEY_BYTES) {
|
|
206
|
-
// Worth naming precisely: a mistyped or wrongly-encoded key would
|
|
207
|
-
// otherwise present as "every update is a forgery".
|
|
208
|
-
_internalLogger.logger.error(`[OTA] ota.publicSigningKey must be ${ED25519_PUBLIC_KEY_BYTES} bytes of hex ` + `(${ED25519_PUBLIC_KEY_BYTES * 2} characters) — got ` + `${pub ? `${pub.length} bytes` : 'a value that is not hex'}.`);
|
|
209
|
-
return false;
|
|
210
|
-
}
|
|
211
|
-
if (!sig) {
|
|
212
|
-
_internalLogger.logger.error('[OTA] Bundle signature is not a 64-byte hex/base64 value.');
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// @noble THROWS rather than returning false for bytes that are not a
|
|
217
|
-
// well-formed point — which is most forgeries, since a tampered
|
|
218
|
-
// signature usually decodes to garbage rather than to a valid-but-wrong
|
|
219
|
-
// point. Swallowing that here is deliberate: from the SDK's side a
|
|
220
|
-
// malformed signature IS a rejection, and letting it escape would
|
|
221
|
-
// surface as `verifier_threw`, the outcome reserved for a HOST verifier
|
|
222
|
-
// misbehaving. Conflating "this bundle is forged" with "your verifier
|
|
223
|
-
// is broken" would point every investigation in the wrong direction.
|
|
224
|
-
try {
|
|
225
|
-
// SYNC FIRST, and this is not a style preference. @noble's
|
|
226
|
-
// `verifyAsync` reaches for `crypto.subtle` — "crypto.subtle must be
|
|
227
|
-
// defined, consider polyfill" — which React Native does not provide
|
|
228
|
-
// and neither does a plain Node test environment. The sync `verify`
|
|
229
|
-
// uses the SHA-512 provider wired above and needs no WebCrypto, so
|
|
230
|
-
// it is the path that actually works on a phone. Verification is a
|
|
231
|
-
// few hundred microseconds over a 32-byte digest, once per update
|
|
232
|
-
// check, so there is nothing to gain from the async form anyway.
|
|
233
|
-
if (typeof ed.verify === 'function') return ed.verify(sig, message, pub);
|
|
234
|
-
if (typeof ed.verifyAsync === 'function') return await ed.verifyAsync(sig, message, pub);
|
|
235
|
-
return false;
|
|
236
|
-
} catch {
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
__DEV__ && _internalLogger.logger.debug('[OTA] Using built-in ed25519 verifier (@noble/ed25519 detected).');
|
|
241
|
-
return resolved;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** Test seam — clears the cached resolution. */
|
|
245
|
-
function _resetBuiltinVerifier() {
|
|
246
|
-
resolved = undefined;
|
|
247
|
-
}
|
|
248
|
-
//# sourceMappingURL=builtinVerifier.js.map
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.loadEd25519 = loadEd25519;
|
|
7
|
-
/**
|
|
8
|
-
* Isolated lazy loader for the OPTIONAL `@noble/ed25519` dep.
|
|
9
|
-
*
|
|
10
|
-
* WHY ITS OWN FILE: Metro's dependency collector emits a dependency map one
|
|
11
|
-
* entry short for a module containing TWO OR MORE different-string inline
|
|
12
|
-
* `require()` calls, so the require indices desync and `_dependencyMap[N]`
|
|
13
|
-
* reads `undefined` ("Requiring unknown module 'undefined'"). Exactly ONE
|
|
14
|
-
* inline require per module avoids the miscount. `loadSha512` is a sibling file
|
|
15
|
-
* for the same reason — see `push/adapters/loadNotifee.ts`, which hit this first.
|
|
16
|
-
*
|
|
17
|
-
* The string must still be STATICALLY resolvable at bundle time; hosts that do
|
|
18
|
-
* not install it stub it via `withScaleBun`.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/** The subset of the @noble/ed25519 surface this SDK uses. */
|
|
22
|
-
|
|
23
|
-
function loadEd25519() {
|
|
24
|
-
try {
|
|
25
|
-
const mod = require('@noble/ed25519');
|
|
26
|
-
const ed = mod?.default ?? mod;
|
|
27
|
-
// Validate the API actually used, not mere truthiness: when the package
|
|
28
|
-
// is absent `withScaleBun` resolves it to an EMPTY STUB MODULE, which is
|
|
29
|
-
// a perfectly truthy object. A stub must read as "unavailable", because
|
|
30
|
-
// treating it as available would make every signed update fail with a
|
|
31
|
-
// crypto error instead of the actionable "no verifier" message.
|
|
32
|
-
if (typeof ed?.verifyAsync === 'function' || typeof ed?.verify === 'function') {
|
|
33
|
-
return ed;
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
//# sourceMappingURL=loadEd25519.js.map
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.loadSha512 = loadSha512;
|
|
7
|
-
/**
|
|
8
|
-
* Isolated lazy loader for the OPTIONAL `@noble/hashes` SHA-512.
|
|
9
|
-
*
|
|
10
|
-
* Separate file from `loadEd25519` because Metro miscounts a module holding two
|
|
11
|
-
* different-string inline `require()` calls — see the comment there.
|
|
12
|
-
*
|
|
13
|
-
* ed25519 is defined in terms of SHA-512, and React Native has no native
|
|
14
|
-
* SHA-512, so @noble/ed25519 cannot verify anything until a hash provider is
|
|
15
|
-
* wired into it. Which property it expects differs by major version, so the
|
|
16
|
-
* wiring lives in `builtinVerifier`, not here; this module only obtains the
|
|
17
|
-
* function.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
function loadSha512() {
|
|
21
|
-
try {
|
|
22
|
-
// The `.js` SUFFIX IS REQUIRED, not stylistic. @noble/hashes v2 declares
|
|
23
|
-
// its exports map with extensions — `"./sha2.js"`, not `"./sha2"` — so
|
|
24
|
-
// the extensionless form misses the map entirely. Node still resolves it
|
|
25
|
-
// by falling back to file-based lookup, which is why it passes here,
|
|
26
|
-
// but Metro prints
|
|
27
|
-
// "Attempted to import … not listed in the exports … Falling back to
|
|
28
|
-
// file-based resolution"
|
|
29
|
-
// on every bundle, and that fallback is exactly what a stricter resolver
|
|
30
|
-
// (or a future Metro) drops. Caught while bundling a real Expo app.
|
|
31
|
-
const mod = require('@noble/hashes/sha2.js');
|
|
32
|
-
const sha512 = mod?.sha512 ?? mod?.default?.sha512;
|
|
33
|
-
// A stubbed module yields undefined here, which correctly reads as
|
|
34
|
-
// "unavailable" rather than throwing deep inside the curve code later.
|
|
35
|
-
return typeof sha512 === 'function' ? sha512 : null;
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
//# sourceMappingURL=loadSha512.js.map
|