@bota.dev/react-native-app-sdk 2.0.0-beta.10 → 2.0.0-beta.11

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 (41) hide show
  1. package/README.md +12 -1
  2. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKPackage.kt +9 -0
  3. package/android/src/main/java/dev/bota/sdk/reactnative/upload/BotaUploadV2BackendModule.kt +66 -0
  4. package/android/src/main/java/dev/bota/sdk/reactnative/upload/NativeUpload.kt +400 -0
  5. package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadHttp.kt +158 -0
  6. package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadJournal.kt +112 -0
  7. package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadModels.kt +171 -0
  8. package/generated/codegen-contract.json +167 -8
  9. package/ios/BotaUploadV2Backend.mm +49 -0
  10. package/ios/UploadBackend/BotaUploadV2BackendBridge.swift +98 -0
  11. package/ios/UploadBackend/UploadCoordinator.swift +84 -0
  12. package/ios/UploadBackend/UploadHTTP.swift +82 -0
  13. package/ios/UploadBackend/UploadJournal.swift +187 -0
  14. package/ios/UploadBackend/UploadModels.swift +273 -0
  15. package/ios/UploadBackend/UploadOperation.swift +391 -0
  16. package/lib/commonjs/encryptedUploadV2Backend.js +200 -0
  17. package/lib/commonjs/encryptedUploadV2Backend.js.map +1 -0
  18. package/lib/commonjs/index.js +5 -0
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/commonjs/sdkIdentity.js +1 -1
  21. package/lib/commonjs/specs/NativeBotaUploadV2Backend.js +11 -0
  22. package/lib/commonjs/specs/NativeBotaUploadV2Backend.js.map +1 -0
  23. package/lib/module/encryptedUploadV2Backend.js +196 -0
  24. package/lib/module/encryptedUploadV2Backend.js.map +1 -0
  25. package/lib/module/index.js +3 -0
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/sdkIdentity.js +1 -1
  28. package/lib/module/specs/NativeBotaUploadV2Backend.js +9 -0
  29. package/lib/module/specs/NativeBotaUploadV2Backend.js.map +1 -0
  30. package/lib/typescript/encryptedUploadV2Backend.d.ts +41 -0
  31. package/lib/typescript/encryptedUploadV2Backend.d.ts.map +1 -0
  32. package/lib/typescript/index.d.ts +3 -0
  33. package/lib/typescript/index.d.ts.map +1 -1
  34. package/lib/typescript/sdkIdentity.d.ts +1 -1
  35. package/lib/typescript/specs/NativeBotaUploadV2Backend.d.ts +18 -0
  36. package/lib/typescript/specs/NativeBotaUploadV2Backend.d.ts.map +1 -0
  37. package/package.json +1 -1
  38. package/src/encryptedUploadV2Backend.ts +171 -0
  39. package/src/index.ts +6 -0
  40. package/src/sdkIdentity.ts +1 -1
  41. package/src/specs/NativeBotaUploadV2Backend.ts +19 -0
@@ -0,0 +1,18 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import type { EventEmitter } from 'react-native/Libraries/Types/CodegenTypes';
3
+ /** Preparation/result JSON contains only bounded identity metadata. Signed
4
+ * documents and recording bytes stay in the native backend adapter. */
5
+ export interface Spec extends TurboModule {
6
+ readonly onCredentialsRequested: EventEmitter<{
7
+ requestId: string;
8
+ operationId: string;
9
+ }>;
10
+ prepare: (inputJSON: string) => Promise<string>;
11
+ cancel: (operationId: string) => Promise<void>;
12
+ complete: (operationId: string) => Promise<void>;
13
+ resolveCredentials: (requestId: string, token: string) => Promise<void>;
14
+ rejectCredentials: (requestId: string) => Promise<void>;
15
+ }
16
+ declare const _default: Spec | null;
17
+ export default _default;
18
+ //# sourceMappingURL=NativeBotaUploadV2Backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeBotaUploadV2Backend.d.ts","sourceRoot":"","sources":["../../../src/specs/NativeBotaUploadV2Backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAE9E;uEACuE;AACvE,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,QAAQ,CAAC,sBAAsB,EAAE,YAAY,CAAC;QAC5C,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC,CAAC;IACH,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,kBAAkB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;;AAED,wBAAoE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bota.dev/react-native-app-sdk",
3
- "version": "2.0.0-beta.10",
3
+ "version": "2.0.0-beta.11",
4
4
  "description": "React Native facade for the Bota App SDK",
5
5
  "packageManager": "npm@12.0.2",
6
6
  "main": "lib/commonjs/index.js",
@@ -0,0 +1,171 @@
1
+ import type { BotaDeviceSDKRecordingClient, BotaEncryptedUploadV2Recording,
2
+ BotaEncryptedUploadV2ProfileDecision, BotaEncryptedUploadV2Progress } from './client';
3
+ import type { ConnectedDevice } from './models/Device';
4
+ import type { Spec } from './specs/NativeBotaUploadV2Backend';
5
+
6
+ export interface EncryptedUploadV2BackendOptions {
7
+ /** Customer-authenticated HTTPS proxy exposing the v2 routes, e.g. https://api.example.com/v1. */
8
+ baseUrl: string;
9
+ /** Stable, non-secret account/project/environment identity. */
10
+ scopeKey: string;
11
+ accountId: string;
12
+ projectId: string;
13
+ endUserId: string;
14
+ environment: string;
15
+ /** Only needed by an organization-scoped backend bridge. */
16
+ organizationId?: string;
17
+ /** Abort permanently on logout or any account/project/binding change. */
18
+ signal: AbortSignal;
19
+ /** Return an application access token, never a Bota secret API key. */
20
+ getAccessToken(signal: AbortSignal): Promise<string>;
21
+ }
22
+
23
+ export interface EncryptedUploadV2SyncTarget {
24
+ deviceId: string;
25
+ bindingGeneration: number;
26
+ signal?: AbortSignal;
27
+ onProgress?: (progress: BotaEncryptedUploadV2Progress) => void;
28
+ /** Optional import of an existing integration's exact cloud identity. */
29
+ priorUpload?: { recordingId: string; sessionId?: string; ownerRevision?: number };
30
+ }
31
+
32
+ export interface EncryptedUploadV2Backend {
33
+ /** Reinvoking after restart/reconnect resumes the native journal for this exact recording. */
34
+ sync(device: ConnectedDevice, recording: BotaEncryptedUploadV2Recording,
35
+ target: EncryptedUploadV2SyncTarget): Promise<{ recordingId: string }>;
36
+ dispose(): Promise<void>;
37
+ }
38
+
39
+ /** @internal Dependency injection keeps the public facade and host tests on the same implementation. */
40
+ export function createManagedEncryptedUploadV2Backend(
41
+ native: Spec | null, recordings: BotaDeviceSDKRecordingClient,
42
+ options: EncryptedUploadV2BackendOptions,
43
+ ): EncryptedUploadV2Backend {
44
+ // Capture identity once; mutating the caller's object cannot repoint an active request.
45
+ const config = { ...options };
46
+ const url = new URL(config.baseUrl);
47
+ const path = url.pathname.replace(/\/$/, '');
48
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash ||
49
+ !/^(\/[A-Za-z0-9_-]+)+$/.test(path) || !config.scopeKey || !config.signal) {
50
+ throw new Error('Invalid encrypted upload backend configuration');
51
+ }
52
+ const apiOrigin = url.origin;
53
+ const attempts = new Set<{ cancel(): Promise<void>; settled: Promise<void> }>();
54
+ let disposed = false;
55
+ const backend: EncryptedUploadV2Backend = {
56
+ async sync(device, recording, target) {
57
+ device = { ...device };
58
+ recording = { ...recording };
59
+ target = { ...target, priorUpload: target.priorUpload && { ...target.priorUpload } };
60
+ if (!native) throw new Error('Encrypted upload backend requires the matching native SDK binary');
61
+ if (disposed || config.signal.aborted || target.signal?.aborted) throw new Error('Encrypted upload cancelled');
62
+ const signal = new AbortController();
63
+ let operationId: string | undefined;
64
+ let selected: (BotaEncryptedUploadV2ProfileDecision & { recordingId: string }) | undefined;
65
+ let cancelTask: Promise<void> | undefined;
66
+ let completed = false;
67
+ const check = () => {
68
+ if (disposed || signal.signal.aborted || config.signal.aborted || target.signal?.aborted) {
69
+ throw new Error('Encrypted upload cancelled');
70
+ }
71
+ };
72
+ const cancel = () => {
73
+ signal.abort();
74
+ if (operationId) cancelTask ??= Promise.resolve().then(() => native.cancel(operationId!));
75
+ return cancelTask ?? Promise.resolve();
76
+ };
77
+ // Event callbacks must not create unhandled rejections; disposal awaits the same task.
78
+ const abort = () => { void cancel().catch(() => {}); };
79
+ let settle!: () => void;
80
+ const settled = new Promise<void>(resolve => { settle = resolve; });
81
+ const attempt = { cancel, settled };
82
+ attempts.add(attempt);
83
+ let subscription: { remove(): void } | undefined;
84
+ try {
85
+ config.signal.addEventListener('abort', abort, { once: true });
86
+ target.signal?.addEventListener('abort', abort, { once: true });
87
+ subscription = native.onCredentialsRequested(event => {
88
+ if (!operationId || event.operationId !== operationId) return;
89
+ void (async () => {
90
+ try {
91
+ check();
92
+ const token = await config.getAccessToken(signal.signal);
93
+ check();
94
+ await native.resolveCredentials(event.requestId, token);
95
+ } catch { await native.rejectCredentials(event.requestId).catch(() => {}); }
96
+ })();
97
+ });
98
+ check();
99
+ await recordings.syncEncryptedRecordingV2(device, recording, async context => {
100
+ check();
101
+ if (!context.operationId || (operationId && operationId !== context.operationId)) {
102
+ throw new Error('Encrypted upload operation changed');
103
+ }
104
+ operationId = context.operationId;
105
+ const value: unknown = JSON.parse(await native.prepare(JSON.stringify({
106
+ operationId,
107
+ scope: { apiOrigin, apiBasePath: path, scopeKey: config.scopeKey,
108
+ accountId: config.accountId, projectId: config.projectId, endUserId: config.endUserId,
109
+ organizationId: config.organizationId ?? '', environment: config.environment,
110
+ deviceId: target.deviceId, bindingGeneration: target.bindingGeneration,
111
+ serialNumber: device.serialNumber, nativeDeviceId: device.id },
112
+ recording: { uuid: context.recording.uuid, generation: context.recording.generation,
113
+ ciphertextLength: context.recording.ciphertextLength, ciphertextSha256: context.recording.ciphertextSha256,
114
+ startedAtMs: context.recording.startedAtMs, durationMs: context.recording.durationMs,
115
+ plaintextLength: context.recording.plaintextLength, storageFormat: context.recording.storageFormat },
116
+ capability: { rawValueHex: context.capability.rawValueHex, flags: context.capability.flags },
117
+ ...(context.checkpoint === undefined ? {} : { checkpoint: {
118
+ version: context.checkpoint.version, uploadSessionId: context.checkpoint.uploadSessionId,
119
+ ownerRevision: context.checkpoint.ownerRevision, revision: context.checkpoint.revision,
120
+ nextCiphertextOffset: context.checkpoint.nextCiphertextOffset, prefixSha256: context.checkpoint.prefixSha256,
121
+ highestContiguousSequence: context.checkpoint.highestContiguousSequence,
122
+ transportSessionId: context.checkpoint.transportSessionId, sinkRegistrationId: context.checkpoint.sinkRegistrationId,
123
+ windowPackets: context.checkpoint.windowPackets, dataPayloadBytes: context.checkpoint.dataPayloadBytes,
124
+ } }),
125
+ ...(target.priorUpload === undefined ? {} : { priorJournalEntry: {
126
+ recordingId: target.priorUpload.recordingId, sessionId: target.priorUpload.sessionId,
127
+ ownerRevision: target.priorUpload.ownerRevision } }),
128
+ })));
129
+ check();
130
+ const result = value as NonNullable<typeof selected>;
131
+ if (!result || result.profile !== 'encrypted_upload_v2' ||
132
+ !/^rec_[A-Za-z0-9_-]+$/.test(result.recordingId) ||
133
+ !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(result.uploadSessionId) ||
134
+ !Number.isSafeInteger(result.ownerRevision) || result.ownerRevision < 1 ||
135
+ !result.materialRegistrationId || !['legacy_allowed', 'v2_preferred', 'v2_required'].includes(result.securityPolicy)) {
136
+ throw new Error('Invalid encrypted upload backend response');
137
+ }
138
+ selected = result;
139
+ return { profile: result.profile, uploadSessionId: result.uploadSessionId,
140
+ ownerRevision: result.ownerRevision, securityPolicy: result.securityPolicy,
141
+ materialRegistrationId: result.materialRegistrationId };
142
+ }, progress => { if (!signal.signal.aborted) target.onProgress?.(progress); }, { signal: signal.signal });
143
+ check();
144
+ if (!operationId || !selected) throw new Error('Encrypted upload confirmation is missing');
145
+ // The native sync only resolves after signed receipt/CONFIRM. Never infer this from HTTP 2xx.
146
+ await native.complete(operationId);
147
+ completed = true;
148
+ return { recordingId: selected.recordingId };
149
+ } finally {
150
+ // Native complete already releases the operation. A later bridge cancellation
151
+ // failure must not turn confirmed device cleanup into a retryable sync failure.
152
+ try { if (completed) signal.abort(); else await cancel(); }
153
+ finally {
154
+ attempts.delete(attempt);
155
+ config.signal.removeEventListener('abort', abort);
156
+ target.signal?.removeEventListener('abort', abort);
157
+ subscription?.remove();
158
+ settle();
159
+ }
160
+ }
161
+ },
162
+ async dispose() {
163
+ disposed = true;
164
+ const results = await Promise.allSettled([...attempts].map(async attempt => {
165
+ try { await attempt.cancel(); } finally { await attempt.settled; }
166
+ }));
167
+ if (results.some(result => result.status === 'rejected')) throw new Error('Encrypted upload cancellation failed');
168
+ },
169
+ };
170
+ return backend;
171
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import NativeBotaDeviceSDK from './specs/NativeBotaDeviceSDK';
2
2
  import { createBotaDeviceSDK } from './client';
3
+ import NativeBotaUploadV2Backend from './specs/NativeBotaUploadV2Backend';
4
+ import { createManagedEncryptedUploadV2Backend } from './encryptedUploadV2Backend';
5
+ import type { EncryptedUploadV2BackendOptions } from './encryptedUploadV2Backend';
3
6
  import { setDefaultCompatibilityClient } from './compatibility/runtime';
4
7
 
5
8
  export {
@@ -52,6 +55,9 @@ export type {
52
55
  } from './client';
53
56
 
54
57
  export const BotaDeviceSDK = createBotaDeviceSDK(NativeBotaDeviceSDK);
58
+ export const createEncryptedUploadV2Backend = (options: EncryptedUploadV2BackendOptions) =>
59
+ createManagedEncryptedUploadV2Backend(NativeBotaUploadV2Backend, BotaDeviceSDK.recordings, options);
60
+ export type { EncryptedUploadV2BackendOptions, EncryptedUploadV2Backend, EncryptedUploadV2SyncTarget } from './encryptedUploadV2Backend';
55
61
  setDefaultCompatibilityClient(BotaDeviceSDK);
56
62
 
57
63
  export { BotaClient } from './BotaClient';
@@ -1,3 +1,3 @@
1
1
  // Generated by cargo xtask client-identity generate. Do not edit.
2
2
  export const SDK_PACKAGE = '@bota.dev/react-native-app-sdk'
3
- export const SDK_VERSION = '2.0.0-beta.10'
3
+ export const SDK_VERSION = '2.0.0-beta.11'
@@ -0,0 +1,19 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+ import type { EventEmitter } from 'react-native/Libraries/Types/CodegenTypes';
4
+
5
+ /** Preparation/result JSON contains only bounded identity metadata. Signed
6
+ * documents and recording bytes stay in the native backend adapter. */
7
+ export interface Spec extends TurboModule {
8
+ readonly onCredentialsRequested: EventEmitter<{
9
+ requestId: string;
10
+ operationId: string;
11
+ }>;
12
+ prepare: (inputJSON: string) => Promise<string>;
13
+ cancel: (operationId: string) => Promise<void>;
14
+ complete: (operationId: string) => Promise<void>;
15
+ resolveCredentials: (requestId: string, token: string) => Promise<void>;
16
+ rejectCredentials: (requestId: string) => Promise<void>;
17
+ }
18
+
19
+ export default TurboModuleRegistry.get<Spec>('BotaUploadV2Backend');