@scalebun/react-native 1.10.6 → 1.10.7
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/rn/ota/ScaleBunOtaModule.kt +75 -0
- package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +12 -0
- package/dist/scalebun.full.js +432 -51
- package/dist/scalebun.slim.js +432 -51
- package/ios/Ota/ScaleBunOtaBridge.mm +6 -0
- package/ios/Ota/ScaleBunOtaModule.swift +67 -0
- package/lib/commonjs/core/config/schema.js +34 -1
- package/lib/commonjs/core/constants/version.js +1 -1
- package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +24 -0
- package/lib/commonjs/features/journey/uiState.js +87 -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/metro/serializerCompose.js +32 -0
- package/lib/commonjs/public/ScaleBunFacade.js +74 -11
- package/lib/module/core/config/schema.js +34 -1
- package/lib/module/core/constants/version.js +1 -1
- package/lib/module/features/journey/ScaleBunDebugRoot.js +24 -0
- package/lib/module/features/journey/uiState.js +79 -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/metro/serializerCompose.js +32 -0
- package/lib/module/public/ScaleBunFacade.js +74 -11
- package/lib/typescript/core/config/schema.d.ts +2 -0
- package/lib/typescript/core/constants/version.d.ts +1 -1
- 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/public/ScaleBunFacade.d.ts +35 -6
- package/lib/typescript/specs/NativeScaleBunOta.d.ts +23 -0
- package/package.json +18 -3
- package/src/core/config/schema.ts +30 -3
- package/src/core/constants/version.ts +1 -1
- package/src/features/journey/ScaleBunDebugRoot.tsx +22 -0
- package/src/features/journey/uiState.ts +84 -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/metro/serializerCompose.ts +38 -2
- package/src/public/ScaleBunFacade.ts +87 -13
- package/src/specs/NativeScaleBunOta.ts +24 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { logger } from '../../core/logger/internalLogger';
|
|
2
|
+
import { getBuiltinVerifier } from './crypto/builtinVerifier';
|
|
3
|
+
import { getNativeVerifier } from './crypto/nativeVerifier';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Bundle signature verification (OTA-03).
|
|
@@ -13,10 +15,17 @@ import { logger } from '../../core/logger/internalLogger';
|
|
|
13
15
|
* download and nothing else. On a platform whose entire purpose is remote code
|
|
14
16
|
* delivery, that is the control that matters most.
|
|
15
17
|
*
|
|
16
|
-
* WHY IT IS SHAPED LIKE THIS.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* WHY IT IS SHAPED LIKE THIS. React Native has no built-in ed25519, so the
|
|
19
|
+
* verification primitive has to come from somewhere. It is resolved in order:
|
|
20
|
+
* a host-supplied verifier, else the built-in one assembled from the OPTIONAL
|
|
21
|
+
* `@noble/ed25519` + `@noble/hashes` peers, else nothing — and the SDK's job is
|
|
22
|
+
* to decide, unambiguously, what happens in that last case.
|
|
23
|
+
*
|
|
24
|
+
* The built-in path exists because requiring every adopter to hand-write a
|
|
25
|
+
* verifier put the most security-critical operation in the product in code the
|
|
26
|
+
* SDK could neither test nor audit, and made a routine dependency bump able to
|
|
27
|
+
* silently stop all updates. See `crypto/builtinVerifier.ts`. Signing stays
|
|
28
|
+
* opt-in, and apps that never adopt it carry no curve arithmetic.
|
|
20
29
|
*
|
|
21
30
|
* THE POLICY, which is the important part:
|
|
22
31
|
*
|
|
@@ -44,15 +53,26 @@ export type SignatureVerifier = (input: {
|
|
|
44
53
|
}) => boolean | Promise<boolean>;
|
|
45
54
|
|
|
46
55
|
export interface SignatureConfig {
|
|
47
|
-
/**
|
|
48
|
-
|
|
56
|
+
/**
|
|
57
|
+
* ed25519 public key(s) shipped in the app binary. Absent = signing not
|
|
58
|
+
* adopted. An ARRAY pins several keys at once and a bundle is accepted when
|
|
59
|
+
* ANY of them verifies — this is what makes key rotation possible without an
|
|
60
|
+
* app-store release: ship a build pinning [old, new], start signing with new,
|
|
61
|
+
* drop old from the next build. With a single pinnable key, losing the
|
|
62
|
+
* private key meant no OTA capability until a new binary cleared review —
|
|
63
|
+
* the exact emergency OTA exists to solve.
|
|
64
|
+
*/
|
|
65
|
+
publicKey?: string | string[];
|
|
49
66
|
/** Host-provided ed25519 verification function. */
|
|
50
67
|
verifier?: SignatureVerifier;
|
|
51
68
|
}
|
|
52
69
|
|
|
53
70
|
export type SignatureOutcome =
|
|
54
71
|
| { ok: true; reason: 'verified' | 'not_configured' }
|
|
55
|
-
| {
|
|
72
|
+
| {
|
|
73
|
+
ok: false;
|
|
74
|
+
reason: 'no_verifier' | 'invalid_signature' | 'missing_signature' | 'verifier_threw' | 'no_keys';
|
|
75
|
+
};
|
|
56
76
|
|
|
57
77
|
let warnedNotConfigured = false;
|
|
58
78
|
|
|
@@ -69,10 +89,18 @@ export async function verifyBundleSignature(
|
|
|
69
89
|
signature: string | undefined,
|
|
70
90
|
config: SignatureConfig | undefined,
|
|
71
91
|
): Promise<SignatureOutcome> {
|
|
72
|
-
const
|
|
92
|
+
const rawKey = config?.publicKey;
|
|
93
|
+
|
|
94
|
+
// Normalize to a list. A single non-empty string is the pre-rotation config
|
|
95
|
+
// shape and behaves exactly as before; an array pins several keys at once.
|
|
96
|
+
const keys = (Array.isArray(rawKey) ? rawKey : rawKey ? [rawKey] : []).filter(
|
|
97
|
+
(k): k is string => typeof k === 'string' && k.trim().length > 0,
|
|
98
|
+
);
|
|
73
99
|
|
|
74
|
-
// Signing not adopted by this app — nothing to enforce.
|
|
75
|
-
|
|
100
|
+
// Signing not adopted by this app — nothing to enforce. Parity note: a
|
|
101
|
+
// falsy single value (undefined, '') has always meant "not configured" and
|
|
102
|
+
// still does.
|
|
103
|
+
if (rawKey === undefined || rawKey === '' || rawKey === null) {
|
|
76
104
|
if (!warnedNotConfigured) {
|
|
77
105
|
warnedNotConfigured = true;
|
|
78
106
|
logger.warn(
|
|
@@ -83,6 +111,20 @@ export async function verifyBundleSignature(
|
|
|
83
111
|
return { ok: true, reason: 'not_configured' };
|
|
84
112
|
}
|
|
85
113
|
|
|
114
|
+
// The host PROVIDED a key config that resolves to zero usable keys — an
|
|
115
|
+
// empty array, or an array of blank strings. That is a broken attempt to
|
|
116
|
+
// enable signing, not an absence of one, and the two must not collapse:
|
|
117
|
+
// treating `publicSigningKeys: []` as "not configured" would let a config
|
|
118
|
+
// mistake silently downgrade an app to unverified installs. Fail closed.
|
|
119
|
+
if (keys.length === 0) {
|
|
120
|
+
logger.error(
|
|
121
|
+
'[OTA] Signing key config resolves to zero usable keys (empty array or blank ' +
|
|
122
|
+
'strings) — refusing to stage. Remove the config to disable signing, or pin ' +
|
|
123
|
+
'at least one real key.',
|
|
124
|
+
);
|
|
125
|
+
return { ok: false, reason: 'no_keys' };
|
|
126
|
+
}
|
|
127
|
+
|
|
86
128
|
// The app opted in, so a bundle without a signature is a refusal, not a pass.
|
|
87
129
|
if (!signature) {
|
|
88
130
|
logger.error(
|
|
@@ -91,30 +133,71 @@ export async function verifyBundleSignature(
|
|
|
91
133
|
return { ok: false, reason: 'missing_signature' };
|
|
92
134
|
}
|
|
93
135
|
|
|
94
|
-
|
|
136
|
+
// Resolution order, and the order matters:
|
|
137
|
+
// 1. A host-supplied verifier ALWAYS wins. A team with its own crypto
|
|
138
|
+
// policy must be able to override whatever the SDK would otherwise pick.
|
|
139
|
+
// 2. Otherwise the NATIVE verifier (CryptoKit / Android 13+ platform
|
|
140
|
+
// ed25519). Preferred over the JS one because it runs outside the
|
|
141
|
+
// bundle it protects and needs no optional peers; on OS levels without
|
|
142
|
+
// ed25519 it delegates to the builtin internally.
|
|
143
|
+
// 3. Otherwise the built-in @noble verifier, if the optional peers are
|
|
144
|
+
// installed. This is the path that removes ~100 lines of hand-written
|
|
145
|
+
// crypto plumbing from every app that adopts signing.
|
|
146
|
+
// 4. Otherwise reject, because a configured key is a request for
|
|
147
|
+
// enforcement and quietly installing unverified code would turn a
|
|
148
|
+
// security feature into a placebo.
|
|
149
|
+
const verifier =
|
|
150
|
+
typeof config?.verifier === 'function'
|
|
151
|
+
? config.verifier
|
|
152
|
+
: (getNativeVerifier() ?? getBuiltinVerifier());
|
|
153
|
+
|
|
154
|
+
if (!verifier) {
|
|
95
155
|
logger.error(
|
|
96
156
|
'[OTA] A signing key is configured but no signature verifier is available — ' +
|
|
97
|
-
'refusing to stage.
|
|
157
|
+
'refusing to stage. Either install the optional peers ' +
|
|
158
|
+
'`@noble/ed25519` and `@noble/hashes` (the SDK then verifies with no ' +
|
|
159
|
+
'further code), or provide your own `ota.verifySignature`.',
|
|
98
160
|
);
|
|
99
161
|
return { ok: false, reason: 'no_verifier' };
|
|
100
162
|
}
|
|
101
163
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
164
|
+
// Try every pinned key; any single match accepts. The verifier contract is
|
|
165
|
+
// unchanged (one key per call) so host-supplied verifiers keep working.
|
|
166
|
+
//
|
|
167
|
+
// Outcome labelling when nothing matched, and why it matters: if at least
|
|
168
|
+
// one verifier call RAN TO COMPLETION and said no, the bundle failed
|
|
169
|
+
// verification — 'invalid_signature'. Only when EVERY call threw is the
|
|
170
|
+
// machinery itself suspect — 'verifier_threw'. Collapsing those (e.g. by
|
|
171
|
+
// letting the last key's throw win) would point an investigation at the
|
|
172
|
+
// host's verifier when the actual event was a forged bundle, or vice versa.
|
|
173
|
+
let sawThrow = false;
|
|
174
|
+
let sawCompletion = false;
|
|
175
|
+
for (const key of keys) {
|
|
176
|
+
try {
|
|
177
|
+
const valid = await verifier({
|
|
178
|
+
messageHex: bundleSha256,
|
|
179
|
+
signature,
|
|
180
|
+
publicKey: key,
|
|
181
|
+
});
|
|
182
|
+
sawCompletion = true;
|
|
183
|
+
if (valid) return { ok: true, reason: 'verified' };
|
|
184
|
+
} catch (err: any) {
|
|
185
|
+
// A throwing verifier is treated as a failed verification for this key,
|
|
186
|
+
// never as a pass — but the remaining keys still get their chance.
|
|
187
|
+
sawThrow = true;
|
|
188
|
+
logger.error(`[OTA] Signature verifier threw: ${err?.message ?? err}`);
|
|
111
189
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
logger.error(`[OTA] Signature verifier threw: ${err?.message ?? err}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (sawThrow && !sawCompletion) {
|
|
116
193
|
return { ok: false, reason: 'verifier_threw' };
|
|
117
194
|
}
|
|
195
|
+
logger.error(
|
|
196
|
+
keys.length > 1
|
|
197
|
+
? `[OTA] Bundle signature is INVALID under all ${keys.length} pinned keys — refusing to stage.`
|
|
198
|
+
: '[OTA] Bundle signature is INVALID — refusing to stage.',
|
|
199
|
+
);
|
|
200
|
+
return { ok: false, reason: 'invalid_signature' };
|
|
118
201
|
}
|
|
119
202
|
|
|
120
203
|
/** Test seam — resets the once-per-process "not configured" warning. */
|
|
@@ -106,6 +106,9 @@ export function discoverArtifacts(packageRoot: string): ArtifactMapping[] {
|
|
|
106
106
|
return out;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/** One-shot guard so an Expo build does not print the pass-through notice per bundle. */
|
|
110
|
+
let warnedUnknownShape = false;
|
|
111
|
+
|
|
109
112
|
/**
|
|
110
113
|
* Build the composing serializer. `hostSerializer` is the app's own customSerializer, if any.
|
|
111
114
|
*/
|
|
@@ -122,10 +125,43 @@ export function createComposingSerializer(
|
|
|
122
125
|
const hostResult = await hostSerializer(entryPoint, preModules, graph, options);
|
|
123
126
|
if (typeof hostResult === 'string') {
|
|
124
127
|
code = hostResult;
|
|
128
|
+
} else if (hostResult && typeof (hostResult as any).code !== 'string') {
|
|
129
|
+
/**
|
|
130
|
+
* A shape this wrapper does not understand — pass it back UNTOUCHED.
|
|
131
|
+
*
|
|
132
|
+
* Expo is the case that matters. `expo/metro-config` installs a
|
|
133
|
+
* serializer returning `{ artifacts: [...] }`, a multi-artifact
|
|
134
|
+
* shape with no top-level `code`. Reading `.code` off it yielded
|
|
135
|
+
* undefined, and returning `{ code: undefined, map }` made every
|
|
136
|
+
* `expo export:embed` die with:
|
|
137
|
+
*
|
|
138
|
+
* Serializer did not return expected format. The project copy
|
|
139
|
+
* of `expo/metro-config` may be out of date.
|
|
140
|
+
*
|
|
141
|
+
* — a message that sends you to upgrade Expo, which is not the
|
|
142
|
+
* problem. The result: `withScaleBun` broke PRODUCTION BUNDLING
|
|
143
|
+
* FOR EVERY EXPO APP, and the SDK's own marketing test app could
|
|
144
|
+
* not build. Found by bundling it.
|
|
145
|
+
*
|
|
146
|
+
* Passing it through costs ScaleBun's source-map composition on
|
|
147
|
+
* Expo (OTA stack traces stay mapped to the bundle rather than to
|
|
148
|
+
* original sources). That is a real loss and strictly better than
|
|
149
|
+
* a build that cannot run at all. Composing INTO an artifact
|
|
150
|
+
* array is the follow-up; it must not be guessed at here.
|
|
151
|
+
*/
|
|
152
|
+
if (!warnedUnknownShape) {
|
|
153
|
+
warnedUnknownShape = true;
|
|
154
|
+
console.warn(
|
|
155
|
+
'[ScaleBun/metro] The host serializer returned a shape this SDK does not ' +
|
|
156
|
+
'compose (Expo returns { artifacts }). Passing it through unchanged — ' +
|
|
157
|
+
'the build is correct, but ScaleBun source-map composition is skipped.',
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return hostResult as never;
|
|
125
161
|
} else {
|
|
126
|
-
code = hostResult.code;
|
|
162
|
+
code = (hostResult as { code: string; map: string }).code;
|
|
127
163
|
try {
|
|
128
|
-
mapObj = JSON.parse(hostResult.map);
|
|
164
|
+
mapObj = JSON.parse((hostResult as { code: string; map: string }).map);
|
|
129
165
|
} catch {
|
|
130
166
|
// Host emitted an unparsable map; fall through and rebuild from the graph so
|
|
131
167
|
// the build still succeeds with a correct map rather than failing here.
|
|
@@ -275,12 +275,17 @@ class ScaleBunFacade {
|
|
|
275
275
|
/**
|
|
276
276
|
* Boot the OTA orchestrator when the init config asks for it.
|
|
277
277
|
*
|
|
278
|
-
* `publicSigningKey`
|
|
279
|
-
* from configuration alone. The
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
278
|
+
* `publicSigningKey` / `publicSigningKeys` are honoured here so signature
|
|
279
|
+
* enforcement is reachable from configuration alone. The list form exists
|
|
280
|
+
* for key ROTATION: a build pinning [old, new] keeps verifying while the
|
|
281
|
+
* server moves to the new key, so replacing a key never needs an emergency
|
|
282
|
+
* store release. Both fields merge (deduplicated) into one pinned set.
|
|
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.
|
|
284
289
|
*/
|
|
285
290
|
private _maybeStartOta(rawConfig: any): void {
|
|
286
291
|
const ota = rawConfig?.ota;
|
|
@@ -290,16 +295,49 @@ class ScaleBunFacade {
|
|
|
290
295
|
// module (or touch the native spec) at import time.
|
|
291
296
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
292
297
|
const { otaOrchestrator } = require('../features/ota/OtaOrchestrator');
|
|
293
|
-
const
|
|
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);
|
|
294
313
|
const verifier = ota.verifySignature;
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
+
}
|
|
300
329
|
}
|
|
301
330
|
otaOrchestrator.init({
|
|
302
|
-
...(
|
|
331
|
+
...(signingRequested
|
|
332
|
+
? {
|
|
333
|
+
signature: {
|
|
334
|
+
// Single key stays a plain string — the shape every
|
|
335
|
+
// existing consumer and test already handles.
|
|
336
|
+
publicKey: publicKeys.length === 1 ? publicKeys[0] : publicKeys,
|
|
337
|
+
verifier,
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
: {}),
|
|
303
341
|
...(typeof ota.healthyTimeoutMs === 'number'
|
|
304
342
|
? { healthyTimeoutMs: ota.healthyTimeoutMs }
|
|
305
343
|
: {}),
|
|
@@ -526,6 +564,42 @@ class ScaleBunFacade {
|
|
|
526
564
|
}
|
|
527
565
|
|
|
528
566
|
/** Convenience: emit a Phase 1 purchase event (revenue + currency + transaction_id). */
|
|
567
|
+
/**
|
|
568
|
+
* Declare which UI state the user is looking at, so taps are attributed to the surface they
|
|
569
|
+
* happened on rather than averaged across every variant of the screen.
|
|
570
|
+
*
|
|
571
|
+
* ScaleBun.setUiState('filter-sheet', 'open');
|
|
572
|
+
* ScaleBun.setUiState('checkout-step', 'payment');
|
|
573
|
+
*
|
|
574
|
+
* Call it when the state CHANGES — the value is read at tap time, so it only has to be right by
|
|
575
|
+
* the time the next tap lands. On React Native this is the ONLY source of UI state: unlike the web,
|
|
576
|
+
* there is no queryable accessibility tree that says a sheet is up, and guessing would produce a
|
|
577
|
+
* state key that is right on some apps and silently wrong on others.
|
|
578
|
+
*
|
|
579
|
+
* Names and values are identifiers, not content: they become query keys in the dashboard, so a
|
|
580
|
+
* person's name or a cart total does not belong in one. `;`, `:` and `|` are stripped and both
|
|
581
|
+
* halves are capped at 32 characters.
|
|
582
|
+
*/
|
|
583
|
+
public setUiState(name: string, value: string): void {
|
|
584
|
+
try {
|
|
585
|
+
const { setUiState } = require('../features/journey/uiState');
|
|
586
|
+
setUiState(name, value);
|
|
587
|
+
} catch { /* no-throw: a state declaration must never break the host */ }
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Stop reporting a UI state dimension — or, with no argument, all of them.
|
|
592
|
+
*
|
|
593
|
+
* Clear on screen unmount. A declaration left behind follows the user onto a surface where it means
|
|
594
|
+
* nothing, and every tap there is filed under a state that was not on screen.
|
|
595
|
+
*/
|
|
596
|
+
public clearUiState(name?: string): void {
|
|
597
|
+
try {
|
|
598
|
+
const { clearUiState } = require('../features/journey/uiState');
|
|
599
|
+
clearUiState(name);
|
|
600
|
+
} catch { /* no-throw */ }
|
|
601
|
+
}
|
|
602
|
+
|
|
529
603
|
public trackPurchase(input: { revenue: number; currency: string; transactionId: string; [k: string]: any }): void {
|
|
530
604
|
noThrow(() => this._eventTracker?.trackPurchase(input));
|
|
531
605
|
}
|
|
@@ -85,6 +85,30 @@ export interface Spec extends TurboModule {
|
|
|
85
85
|
* `recreateReactContextInBackground()` (bridge). iOS: RCTReloadCommand.
|
|
86
86
|
*/
|
|
87
87
|
restartApp(): void;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Verify a detached ed25519 signature in NATIVE code.
|
|
91
|
+
*
|
|
92
|
+
* Exists because the JS verifier runs inside the very bundle it protects —
|
|
93
|
+
* an attacker who lands one malicious bundle can neuter a JS check for every
|
|
94
|
+
* update after it. Platform crypto is outside the bundle's reach, and it also
|
|
95
|
+
* removes the runtime dependency on the optional @noble peers wherever the OS
|
|
96
|
+
* provides ed25519 (iOS 13+ CryptoKit everywhere; Android API 33+).
|
|
97
|
+
*
|
|
98
|
+
* All three arguments are lowercase hex, pre-validated by the JS caller:
|
|
99
|
+
* the 32-byte bundle SHA-256 (the signature is over its RAW BYTES, matching
|
|
100
|
+
* the CLI's signing contract), the 64-byte signature, the 32-byte public key.
|
|
101
|
+
*
|
|
102
|
+
* Resolves a verdict STRING, not a boolean, because "the signature is wrong"
|
|
103
|
+
* and "this OS cannot check" must never collapse into one value:
|
|
104
|
+
* 'valid' — signature verifies under the key
|
|
105
|
+
* 'invalid' — it does not; the caller must refuse the bundle
|
|
106
|
+
* 'unavailable' — this OS level has no ed25519 (Android < 33); the caller
|
|
107
|
+
* falls back to the JS verifier
|
|
108
|
+
* A rejected promise is machinery failure and the caller treats it as
|
|
109
|
+
* 'unavailable' — never as 'valid'.
|
|
110
|
+
*/
|
|
111
|
+
verifyEd25519(messageHex: string, signatureHex: string, publicKeyHex: string): Promise<string>;
|
|
88
112
|
}
|
|
89
113
|
|
|
90
114
|
/**
|