@scalebun/react-native 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +70 -4
  2. package/lib/commonjs/bootstrap/SDKBootstrapper.js +12 -9
  3. package/lib/commonjs/bootstrap/SDKBootstrapper.js.map +1 -1
  4. package/lib/commonjs/compat/codepush.js +78 -15
  5. package/lib/commonjs/compat/codepush.js.map +1 -1
  6. package/lib/commonjs/core/config/schema.js +18 -5
  7. package/lib/commonjs/core/config/schema.js.map +1 -1
  8. package/lib/commonjs/core/constants/endpoints.js +9 -6
  9. package/lib/commonjs/core/constants/endpoints.js.map +1 -1
  10. package/lib/commonjs/features/ota/OtaOrchestrator.js +9 -0
  11. package/lib/commonjs/features/ota/OtaOrchestrator.js.map +1 -1
  12. package/lib/commonjs/public/ScaleBunFacade.js +49 -0
  13. package/lib/commonjs/public/ScaleBunFacade.js.map +1 -1
  14. package/lib/module/bootstrap/SDKBootstrapper.js +12 -9
  15. package/lib/module/bootstrap/SDKBootstrapper.js.map +1 -1
  16. package/lib/module/compat/codepush.js +77 -14
  17. package/lib/module/compat/codepush.js.map +1 -1
  18. package/lib/module/core/config/schema.js +18 -5
  19. package/lib/module/core/config/schema.js.map +1 -1
  20. package/lib/module/core/constants/endpoints.js +9 -6
  21. package/lib/module/core/constants/endpoints.js.map +1 -1
  22. package/lib/module/features/ota/OtaOrchestrator.js +9 -0
  23. package/lib/module/features/ota/OtaOrchestrator.js.map +1 -1
  24. package/lib/module/index.js +1 -1
  25. package/lib/module/public/ScaleBunFacade.js +49 -0
  26. package/lib/module/public/ScaleBunFacade.js.map +1 -1
  27. package/lib/typescript/bootstrap/SDKBootstrapper.d.ts.map +1 -1
  28. package/lib/typescript/compat/codepush.d.ts +27 -4
  29. package/lib/typescript/compat/codepush.d.ts.map +1 -1
  30. package/lib/typescript/core/config/schema.d.ts +3 -0
  31. package/lib/typescript/core/config/schema.d.ts.map +1 -1
  32. package/lib/typescript/core/constants/endpoints.d.ts +9 -6
  33. package/lib/typescript/core/constants/endpoints.d.ts.map +1 -1
  34. package/lib/typescript/features/engage/EngageTransport.d.ts +1 -1
  35. package/lib/typescript/features/engage/EngageTransport.d.ts.map +1 -1
  36. package/lib/typescript/features/ota/OtaOrchestrator.d.ts +6 -0
  37. package/lib/typescript/features/ota/OtaOrchestrator.d.ts.map +1 -1
  38. package/lib/typescript/public/ScaleBunFacade.d.ts +11 -0
  39. package/lib/typescript/public/ScaleBunFacade.d.ts.map +1 -1
  40. package/lib/typescript/public/types.d.ts +34 -8
  41. package/lib/typescript/public/types.d.ts.map +1 -1
  42. package/package.json +1 -1
  43. package/src/bootstrap/SDKBootstrapper.ts +12 -9
  44. package/src/compat/codepush.ts +87 -16
  45. package/src/core/config/schema.ts +18 -5
  46. package/src/core/constants/endpoints.ts +9 -6
  47. package/src/features/engage/EngageTransport.ts +1 -1
  48. package/src/features/ota/OtaOrchestrator.ts +9 -0
  49. package/src/index.ts +1 -1
  50. package/src/public/ScaleBunFacade.ts +45 -0
  51. package/src/public/types.ts +35 -8
@@ -1,4 +1,5 @@
1
1
  import { otaOrchestrator } from '../features/ota/OtaOrchestrator';
2
+ import { logger } from '../core/logger/internalLogger';
2
3
 
3
4
  export enum CodePushInstallMode {
4
5
  IMMEDIATE = 0,
@@ -38,12 +39,57 @@ export interface CodePushPackage {
38
39
  }
39
40
 
40
41
  /**
41
- * Microsoft CodePush Drop-in Compatibility Shim — Sprint 10 (S10-CLI-1).
42
+ * Microsoft CodePush compatibility shim.
42
43
  *
43
- * Allows developers to migrate from CodePush to Scalebun OTA by simply changing:
44
- * - import codePush from 'react-native-code-push'
45
- * + import codePush from '@scalebun/react-native/compat/codepush'
44
+ * Maps the CodePush surface (`sync`, `checkForUpdate`, the HOC) onto the
45
+ * ScaleBun orchestrator so a migrating app keeps compiling while its call sites
46
+ * are ported.
47
+ *
48
+ * CONFIGURE IT FIRST. This shim used to carry hardcoded defaults —
49
+ * `http://localhost:3001/api/v1` with a dev client key — so a migrated
50
+ * production app silently checked localhost forever and reported UP_TO_DATE on
51
+ * every sync. There is no correct default for someone else's backend, so there
52
+ * is now none: call `configureCodePushShim()` (or `ScaleBun.init({ ota })`,
53
+ * which the facade wires for you) before the first sync, and an unconfigured
54
+ * shim says so loudly instead of pretending to be up to date.
55
+ *
56
+ * import codePush, { configureCodePushShim } from '@scalebun/react-native/compat/codepush';
57
+ * configureCodePushShim({ apiUrl, clientKey, appVersion, installationId });
58
+ */
59
+
60
+ export interface CodePushShimConfig {
61
+ /** Backend base URL, e.g. https://api.yourbackend.com/api/v1 */
62
+ apiUrl: string;
63
+ /** SDK client key for this app/environment. */
64
+ clientKey: string;
65
+ /** Native app version this binary reports (e.g. '1.4.0'). */
66
+ appVersion: string;
67
+ /** Stable per-install id. Defaults to the SDK's canonical installation id. */
68
+ installationId?: string;
69
+ }
70
+
71
+ let shimConfig: CodePushShimConfig | null = null;
72
+
73
+ /** Point the shim at a real backend. Required before sync/checkForUpdate. */
74
+ export function configureCodePushShim(config: CodePushShimConfig): void {
75
+ shimConfig = config;
76
+ }
77
+
78
+ /**
79
+ * Resolve the sync params, or null when unconfigured. Returning null (rather
80
+ * than substituting a localhost default) is what turns a silent no-update into
81
+ * a visible, fixable error.
46
82
  */
83
+ function shimParams(): CodePushShimConfig | null {
84
+ if (!shimConfig) {
85
+ logger.warn(
86
+ '[ScaleBun] CodePush shim is not configured — no update check was performed. ' +
87
+ 'Call configureCodePushShim({ apiUrl, clientKey, appVersion }) during startup.',
88
+ );
89
+ return null;
90
+ }
91
+ return shimConfig;
92
+ }
47
93
  const codePushShim = function <P>(rootComponent: React.ComponentType<P>) {
48
94
  return rootComponent;
49
95
  };
@@ -53,14 +99,21 @@ codePushShim.sync = async (
53
99
  syncStatusCallback?: (status: CodePushSyncStatus) => void,
54
100
  downloadProgressCallback?: (progress: { receivedBytes: number; totalBytes: number }) => void,
55
101
  ): Promise<CodePushSyncStatus> => {
102
+ const params = shimParams();
103
+ if (!params) {
104
+ // Unconfigured is an ERROR, not "up to date": reporting UP_TO_DATE here is
105
+ // exactly how a broken migration stays invisible for months.
106
+ syncStatusCallback?.(CodePushSyncStatus.UNKNOWN_ERROR);
107
+ return CodePushSyncStatus.UNKNOWN_ERROR;
108
+ }
56
109
  try {
57
110
  syncStatusCallback?.(CodePushSyncStatus.CHECKING_FOR_UPDATE);
58
111
  const result = await otaOrchestrator.sync({
59
- apiUrl: 'http://localhost:3001/api/v1',
60
- clientKey: 'scalebun_ck_dev_default_0000000000000000000000',
61
- appVersion: '1.0.0',
62
- installationId: 'codepush_shim',
63
- });
112
+ ...params,
113
+ // Omitted → the orchestrator falls back to the SDK's canonical
114
+ // installation id, which is the right identity anyway.
115
+ ...(params.installationId ? { installationId: params.installationId } : {}),
116
+ } as any);
64
117
 
65
118
  if (result.status === 'UP_TO_DATE') {
66
119
  syncStatusCallback?.(CodePushSyncStatus.UP_TO_DATE);
@@ -81,16 +134,16 @@ codePushShim.sync = async (
81
134
  };
82
135
 
83
136
  codePushShim.checkForUpdate = async (): Promise<CodePushPackage | null> => {
137
+ const params = shimParams();
138
+ if (!params) return null;
84
139
  const check = await otaOrchestrator.checkForUpdate({
85
- apiUrl: 'http://localhost:3001/api/v1',
86
- clientKey: 'scalebun_ck_dev_default_0000000000000000000000',
87
- appVersion: '1.0.0',
88
- installationId: 'codepush_shim',
89
- });
140
+ ...params,
141
+ ...(params.installationId ? { installationId: params.installationId } : {}),
142
+ } as any);
90
143
  if (check.action === 'NONE' || !check.bundle) return null;
91
144
 
92
145
  return {
93
- appVersion: '1.0.0',
146
+ appVersion: params.appVersion,
94
147
  deploymentKey: 'scalebun_ota',
95
148
  description: check.bundle.releaseNote ?? '',
96
149
  failedInstall: false,
@@ -111,8 +164,26 @@ codePushShim.allowRestart = () => {
111
164
  // Scalebun handles restart safety automatically via boot guard
112
165
  };
113
166
 
167
+ /**
168
+ * Metadata for the bundle currently RUNNING. CodePush apps use this to show a
169
+ * version or decide whether a pending update landed; returning a flat null
170
+ * (as this did) makes every such check read as "no OTA bundle installed".
171
+ */
114
172
  codePushShim.getUpdateMetadata = async (): Promise<CodePushPackage | null> => {
115
- return null;
173
+ const current = otaOrchestrator.getCurrentBundle();
174
+ if (!current) return null;
175
+ return {
176
+ appVersion: shimConfig?.appVersion ?? '',
177
+ deploymentKey: 'scalebun_ota',
178
+ description: current.releaseNote ?? '',
179
+ failedInstall: false,
180
+ isFirstRun: false,
181
+ isMandatory: current.isMandatory ?? false,
182
+ isPending: false,
183
+ label: `v${current.version}`,
184
+ packageHash: current.sha256,
185
+ packageSize: current.size,
186
+ };
116
187
  };
117
188
 
118
189
  export default codePushShim;
@@ -13,11 +13,10 @@ export const ConfigSchema = z.object({
13
13
  * immediately after parse. To point the SDK at a different backend, change the
14
14
  * constant at build time.
15
15
  */
16
- // Thunk, not a bare string: `default()` is typed `Partial<T> | (() => T)`, and for
17
- // `z.any()` T is `any`, so `Partial<any>` widens to an index signature that a string
18
- // does not satisfy. The function overload does, and DefaultSchema calls it only when
19
- // the input is undefined identical at runtime, and it type-checks, which `bob build`
20
- // requires to emit definitions at all.
16
+ // NB: `.default()` on the vendored `z` types its arg as `Partial<T> | (() => T)`, so a bare
17
+ // string arg fails `tsc` (.d.ts generation via bob) with TS2345. The factory form satisfies the
18
+ // `(() => any)` overload. Behaviour is identical omitted apiBaseUrl resolves to the default,
19
+ // which the bootstrapper overwrites anyway (the endpoint is locked).
21
20
  apiBaseUrl: z.any().default(() => DEFAULT_API_BASE_URL),
22
21
  debug: z.boolean().default(false),
23
22
  /**
@@ -169,6 +168,11 @@ export const ConfigSchema = z.object({
169
168
  * Plumbed into SessionManager via SDKBootstrapper.
170
169
  */
171
170
  replay: z.object({
171
+ // Auto-start frame recording in SaaS mode (clientKey set) at cold launch, so replay
172
+ // sessions carry frames out of the box. Default: true (undefined ⇒ record). Set false for
173
+ // events-only sessions — the host can still record on demand via ScaleBun.startSession().
174
+ // Decoupled from finalizeOnBackground (which only governs per-foreground session boundaries).
175
+ record: z.boolean().optional(),
172
176
  captureMode: z.enum(['navigation', 'interactions', 'frequency']).optional(),
173
177
  intervalMs: z.number().min(500).optional(),
174
178
  quality: z.enum(['high', 'normal', 'low', 'grayscale']).optional(),
@@ -214,6 +218,15 @@ export const ConfigSchema = z.object({
214
218
  channelOverride: z.string().optional(),
215
219
  /** Ed25519 public key for bundle signature verification (base64). */
216
220
  publicSigningKey: z.string().optional(),
221
+ /**
222
+ * Ed25519 verifier supplied by the host — the SDK ships no crypto
223
+ * implementation. Required whenever `publicSigningKey` is set: pinning a
224
+ * key without a verifier is fail-closed, so updates stop installing.
225
+ * `(message, signature, publicKey) => boolean | Promise<boolean>`.
226
+ */
227
+ verifySignature: z.any().optional(),
228
+ /** Override how long a new bundle has to prove healthy (ms). */
229
+ healthyTimeoutMs: z.number().optional(),
217
230
  /** Block UI interaction while a mandatory update is pending. Default: false. */
218
231
  mandatoryBlocksUi: z.boolean().default(false),
219
232
  }).optional(),
@@ -1,9 +1,12 @@
1
1
  /**
2
- * ScaleBun cloud API base URL.
2
+ * ScaleBun cloud API base URL — the single, LOCKED endpoint for the SDK.
3
3
  *
4
- * Used as the default for `apiBaseUrl` when an integrator does not supply one,
5
- * so the common "just give me a project key" integration works without knowing
6
- * the server address. Override `apiBaseUrl` in init config to point at a
7
- * staging environment or a self-hosted backend.
4
+ * `SDKBootstrapper` ignores any `apiBaseUrl` passed in init config and always
5
+ * uses this value, so an integrator cannot repoint the SDK at another backend.
6
+ * Includes the `/api/v1` path segment because every SDK route is built relative
7
+ * to this base (e.g. `${apiBaseUrl}/batch`, `${apiBaseUrl}/ingestion/sessions`,
8
+ * `${apiBaseUrl}/config`). Without `/api/v1` the ingestion routes return 404 and
9
+ * events are silently lost. To target a staging or self-hosted backend, change
10
+ * this value at build time.
8
11
  */
9
- export const DEFAULT_API_BASE_URL = 'https://api.scalebun.com';
12
+ export const DEFAULT_API_BASE_URL = 'https://api.scalebun.com/api/v1';
@@ -56,7 +56,7 @@ function uuidV4(): string {
56
56
  }
57
57
 
58
58
  export interface EngageTransportConfig {
59
- /** Backend API base URL (e.g. https://api.scalebun.com). */
59
+ /** Backend API base URL (e.g. https://api.scalebun.com/api/v1). */
60
60
  endpoint: string;
61
61
  /** SaaS multi-tenant client key — gates upload lanes + x-scalebun-client-key header. */
62
62
  clientKey?: string;
@@ -199,6 +199,15 @@ const DEFAULT_MAX_REVERT_ATTEMPTS = 2;
199
199
  export class OtaOrchestrator {
200
200
  private enabled = false;
201
201
  private currentBundle: OtaBundlePayload | null = null;
202
+
203
+ /**
204
+ * The bundle this process is running, once hydrated from the native slots.
205
+ * Read-only view for integrations (e.g. the CodePush shim's
206
+ * getUpdateMetadata) that need to report the running version.
207
+ */
208
+ public getCurrentBundle(): OtaBundlePayload | null {
209
+ return this.currentBundle;
210
+ }
202
211
  private isRestartRequiredState = false;
203
212
  private healthyTimer: ReturnType<typeof setTimeout> | null = null;
204
213
  private bootGuardConfig: BootGuardConfig = {};
package/src/index.ts CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  // ─── Default Export: SDK Facade Singleton ───────────────────────────────────
14
14
  // Usage: import ScaleBun from '@scalebun/react-native';
15
- // await ScaleBun.init({ projectId: '...', publishableKey: '...', apiBaseUrl: '...' });
15
+ // await ScaleBun.init({ projectId: '...', publishableKey: '...' });
16
16
 
17
17
  export { default } from './public/ScaleBunFacade';
18
18
  export { ScaleBunProvider } from './public/ScaleBunProvider';
@@ -251,6 +251,13 @@ class ScaleBunFacade {
251
251
  // Continue — debug connection is independent of backend API
252
252
  }
253
253
 
254
+ // OTA from init config. Without this the whole `ota` config block was
255
+ // inert: `otaOrchestrator.init()` was reachable only by importing the
256
+ // orchestrator directly, so an app that followed the documented
257
+ // `ota: { enabled: true }` path silently got no updates AND no
258
+ // signature enforcement (sync() gates on the flag init() sets).
259
+ this._maybeStartOta(rawConfig);
260
+
254
261
  // Auto debug connection from init config — runs regardless of backend status
255
262
  const debugConfig = (rawConfig?.desktopDebug ?? rawConfig?.debug) as DebugConnectionConfig | undefined;
256
263
  if (debugConfig) {
@@ -264,6 +271,44 @@ class ScaleBunFacade {
264
271
  }
265
272
  }
266
273
 
274
+ /**
275
+ * Boot the OTA orchestrator when the init config asks for it.
276
+ *
277
+ * `publicSigningKey` is honoured here so signature enforcement is reachable
278
+ * from configuration alone. The SDK ships no ed25519 implementation, so a
279
+ * host that pins a key must also supply `ota.verifySignature`; pinning a key
280
+ * without a verifier is fail-CLOSED by design (signature.ts) — an update
281
+ * that cannot be verified is not installed. We say that out loud rather than
282
+ * letting the app discover it as a silent no-update condition.
283
+ */
284
+ private _maybeStartOta(rawConfig: any): void {
285
+ const ota = rawConfig?.ota;
286
+ if (!ota?.enabled) return;
287
+ try {
288
+ // Required lazily: hosts that never enable OTA should not pay for the
289
+ // module (or touch the native spec) at import time.
290
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
291
+ const { otaOrchestrator } = require('../features/ota/OtaOrchestrator');
292
+ const publicKey = ota.publicSigningKey;
293
+ const verifier = ota.verifySignature;
294
+ if (publicKey && typeof verifier !== 'function') {
295
+ logger.warn(
296
+ '[ScaleBun] ota.publicSigningKey is set but ota.verifySignature is not a function. ' +
297
+ 'Signature checking is fail-closed: updates will be REJECTED until a verifier is supplied.',
298
+ );
299
+ }
300
+ otaOrchestrator.init({
301
+ ...(publicKey ? { signature: { publicKey, verifier } } : {}),
302
+ ...(typeof ota.healthyTimeoutMs === 'number'
303
+ ? { healthyTimeoutMs: ota.healthyTimeoutMs }
304
+ : {}),
305
+ });
306
+ logger.info('[ScaleBun] OTA enabled from init config.');
307
+ } catch (err: any) {
308
+ logger.warn(`[ScaleBun] OTA init failed: ${err?.message ?? err}`);
309
+ }
310
+ }
311
+
267
312
  private _autoEnableDebug(debugConfig: DebugConnectionConfig): void {
268
313
  try {
269
314
  const dbgConfig = buildDebugConfigFromInitConfig(debugConfig);
@@ -78,14 +78,28 @@ export interface ScaleBunInitConfig {
78
78
  /** Publishable API key from dashboard */
79
79
  publishableKey: string;
80
80
  /**
81
- * Backend API base URL. Optional — defaults to the ScaleBun cloud.
82
- * Override only for staging or a self-hosted backend.
81
+ * @deprecated The endpoint is LOCKED to the ScaleBun cloud. Any value passed here is accepted
82
+ * (for backward compatibility) but ignored the bootstrapper always uses the fixed
83
+ * `DEFAULT_API_BASE_URL`. To target a different backend, change that constant at build time.
83
84
  */
84
85
  apiBaseUrl?: string;
85
86
  /** @deprecated Use desktopDebug instead */
86
87
  debug?: boolean;
87
- /** Enable session replay capture. Default: true */
88
+ /** Enable session replay capture (feature on/off). Default: true */
88
89
  sessionReplay?: boolean;
90
+ /**
91
+ * Session-replay capture options (SaaS mode).
92
+ * - `record` (default **true**): auto-start frame recording at cold launch so replays appear out
93
+ * of the box. Set `false` for events-only sessions — you can still record on demand with
94
+ * `ScaleBun.startSession()`.
95
+ * - `captureMode` / `intervalMs` / `quality`: tune capture cadence.
96
+ */
97
+ replay?: {
98
+ record?: boolean;
99
+ captureMode?: 'navigation' | 'interactions' | 'frequency';
100
+ intervalMs?: number;
101
+ quality?: 'high' | 'normal' | 'low' | 'grayscale';
102
+ };
89
103
  /** Event flush interval in ms. Default: 5000 */
90
104
  flushIntervalMs?: number;
91
105
  /** Max events per flush batch. Default: 1000 */
@@ -234,7 +248,6 @@ export interface ExportResult {
234
248
  * await ScaleBun.init({
235
249
  * projectId: 'proj_xxx',
236
250
  * publishableKey: 'pk_xxx',
237
- * apiBaseUrl: 'https://api.scalebun.com',
238
251
  * });
239
252
  * ```
240
253
  *
@@ -243,7 +256,6 @@ export interface ExportResult {
243
256
  * await ScaleBun.init({
244
257
  * projectId: 'proj_xxx',
245
258
  * publishableKey: 'pk_xxx',
246
- * apiBaseUrl: 'https://api.scalebun.com',
247
259
  * debug: true,
248
260
  * });
249
261
  * ```
@@ -254,8 +266,9 @@ export interface SimplifiedInitConfig {
254
266
  /** Publishable API key from dashboard */
255
267
  publishableKey: string;
256
268
  /**
257
- * Backend API base URL. Optional — defaults to the ScaleBun cloud.
258
- * Override only for staging or a self-hosted backend.
269
+ * @deprecated The endpoint is LOCKED to the ScaleBun cloud. Any value passed here is accepted
270
+ * (for backward compatibility) but ignored the bootstrapper always uses the fixed
271
+ * `DEFAULT_API_BASE_URL`. To target a different backend, change that constant at build time.
259
272
  */
260
273
  apiBaseUrl?: string;
261
274
 
@@ -271,9 +284,23 @@ export interface SimplifiedInitConfig {
271
284
  /** @deprecated Use desktopDebug instead. */
272
285
  debug?: DebugConnectionConfig;
273
286
 
274
- /** Enable session replay capture. Default: true */
287
+ /** Enable session replay capture (feature on/off). Default: true */
275
288
  sessionReplay?: boolean;
276
289
 
290
+ /**
291
+ * Session-replay capture options (SaaS mode).
292
+ * - `record` (default **true**): auto-start frame recording at cold launch so replays appear out
293
+ * of the box. Set `false` for events-only sessions — you can still record on demand with
294
+ * `ScaleBun.startSession()`.
295
+ * - `captureMode` / `intervalMs` / `quality`: tune capture cadence.
296
+ */
297
+ replay?: {
298
+ record?: boolean;
299
+ captureMode?: 'navigation' | 'interactions' | 'frequency';
300
+ intervalMs?: number;
301
+ quality?: 'high' | 'normal' | 'low' | 'grayscale';
302
+ };
303
+
277
304
  /** SDK-wide privacy settings */
278
305
  privacy?: PrivacyConfig;
279
306