@qafka/react-native 2.0.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 (178) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/CONTRIBUTING.md +92 -0
  3. package/LICENSE +22 -0
  4. package/README.md +109 -0
  5. package/SECURITY.md +67 -0
  6. package/android/build.gradle +35 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/java/com/qafka/attestation/QafkaAttestationModule.kt +92 -0
  9. package/android/src/main/java/com/qafka/attestation/QafkaAttestationPackage.kt +22 -0
  10. package/android/src/main/java/com/qafka/audio/QafkaAudioModule.kt +290 -0
  11. package/android/src/main/java/com/qafka/clipboard/QafkaClipboardModule.kt +28 -0
  12. package/android/src/main/java/com/qafka/storage/QafkaStorageModule.kt +80 -0
  13. package/app.plugin.js +1 -0
  14. package/dist/QafkaSDK.d.ts +174 -0
  15. package/dist/QafkaSDK.js +461 -0
  16. package/dist/cards/bindings/resolveFieldName.d.ts +25 -0
  17. package/dist/cards/bindings/resolveFieldName.js +82 -0
  18. package/dist/cards/cta/CardContext.d.ts +16 -0
  19. package/dist/cards/cta/CardContext.js +58 -0
  20. package/dist/cards/cta/dispatcher.d.ts +7 -0
  21. package/dist/cards/cta/dispatcher.js +90 -0
  22. package/dist/cards/cta/types.d.ts +66 -0
  23. package/dist/cards/cta/types.js +2 -0
  24. package/dist/cards/index.d.ts +20 -0
  25. package/dist/cards/index.js +34 -0
  26. package/dist/cards/primitives/QButton.d.ts +10 -0
  27. package/dist/cards/primitives/QButton.js +115 -0
  28. package/dist/cards/primitives/QDivider.d.ts +7 -0
  29. package/dist/cards/primitives/QDivider.js +17 -0
  30. package/dist/cards/primitives/QIcon.d.ts +13 -0
  31. package/dist/cards/primitives/QIcon.js +26 -0
  32. package/dist/cards/primitives/QImage.d.ts +9 -0
  33. package/dist/cards/primitives/QImage.js +22 -0
  34. package/dist/cards/primitives/QText.d.ts +9 -0
  35. package/dist/cards/primitives/QText.js +30 -0
  36. package/dist/cards/primitives/QView.d.ts +8 -0
  37. package/dist/cards/primitives/QView.js +19 -0
  38. package/dist/cards/renderer/CardRenderer.d.ts +19 -0
  39. package/dist/cards/renderer/CardRenderer.js +64 -0
  40. package/dist/cards/renderer/renderNode.d.ts +13 -0
  41. package/dist/cards/renderer/renderNode.js +42 -0
  42. package/dist/cards/types.d.ts +110 -0
  43. package/dist/cards/types.js +6 -0
  44. package/dist/components/ActionResultBadge.d.ts +12 -0
  45. package/dist/components/ActionResultBadge.js +58 -0
  46. package/dist/components/ChatPage.d.ts +44 -0
  47. package/dist/components/ChatPage.js +84 -0
  48. package/dist/components/DataChip.d.ts +8 -0
  49. package/dist/components/DataChip.js +80 -0
  50. package/dist/components/DataChipList.d.ts +13 -0
  51. package/dist/components/DataChipList.js +21 -0
  52. package/dist/components/FloatingButton.d.ts +11 -0
  53. package/dist/components/FloatingButton.js +162 -0
  54. package/dist/components/InputArea.d.ts +57 -0
  55. package/dist/components/InputArea.js +142 -0
  56. package/dist/components/MarkdownText.d.ts +15 -0
  57. package/dist/components/MarkdownText.js +283 -0
  58. package/dist/components/MessageBubble.d.ts +134 -0
  59. package/dist/components/MessageBubble.js +384 -0
  60. package/dist/components/NavigationSuggestion.d.ts +11 -0
  61. package/dist/components/NavigationSuggestion.js +109 -0
  62. package/dist/components/Qafka.d.ts +39 -0
  63. package/dist/components/Qafka.handlers.d.ts +21 -0
  64. package/dist/components/Qafka.handlers.js +54 -0
  65. package/dist/components/Qafka.js +493 -0
  66. package/dist/components/Qafka.styles.d.ts +19 -0
  67. package/dist/components/Qafka.styles.js +101 -0
  68. package/dist/components/Qafka.types.d.ts +744 -0
  69. package/dist/components/Qafka.types.js +2 -0
  70. package/dist/components/Qafka.utils.d.ts +7 -0
  71. package/dist/components/Qafka.utils.js +34 -0
  72. package/dist/components/QafkaProvider.d.ts +12 -0
  73. package/dist/components/QafkaProvider.js +87 -0
  74. package/dist/components/QuickReplies.d.ts +14 -0
  75. package/dist/components/QuickReplies.js +48 -0
  76. package/dist/components/StepProgressIndicator.d.ts +12 -0
  77. package/dist/components/StepProgressIndicator.js +48 -0
  78. package/dist/components/SuggestionButton.d.ts +42 -0
  79. package/dist/components/SuggestionButton.js +67 -0
  80. package/dist/components/ToolStatusPill.d.ts +20 -0
  81. package/dist/components/ToolStatusPill.js +43 -0
  82. package/dist/components/TypingIndicator.d.ts +28 -0
  83. package/dist/components/TypingIndicator.js +109 -0
  84. package/dist/components/VoicePage.d.ts +48 -0
  85. package/dist/components/VoicePage.js +683 -0
  86. package/dist/components/defaults/DefaultCard.d.ts +14 -0
  87. package/dist/components/defaults/DefaultCard.js +156 -0
  88. package/dist/components/defaults/DefaultDetail.d.ts +14 -0
  89. package/dist/components/defaults/DefaultDetail.js +138 -0
  90. package/dist/components/defaults/DefaultList.d.ts +12 -0
  91. package/dist/components/defaults/DefaultList.js +98 -0
  92. package/dist/components/defaults/DefaultTable.d.ts +14 -0
  93. package/dist/components/defaults/DefaultTable.js +204 -0
  94. package/dist/components/defaults/index.d.ts +14 -0
  95. package/dist/components/defaults/index.js +25 -0
  96. package/dist/components/index.d.ts +22 -0
  97. package/dist/components/index.js +36 -0
  98. package/dist/constants.d.ts +10 -0
  99. package/dist/constants.js +13 -0
  100. package/dist/hooks/useChatMessages.d.ts +72 -0
  101. package/dist/hooks/useChatMessages.js +505 -0
  102. package/dist/hooks/useContextManager.d.ts +12 -0
  103. package/dist/hooks/useContextManager.js +46 -0
  104. package/dist/hooks/useProjectTheme.d.ts +19 -0
  105. package/dist/hooks/useProjectTheme.js +163 -0
  106. package/dist/hooks/useSDK.d.ts +31 -0
  107. package/dist/hooks/useSDK.js +103 -0
  108. package/dist/hooks/useVoiceChat.d.ts +110 -0
  109. package/dist/hooks/useVoiceChat.js +436 -0
  110. package/dist/index.d.ts +13 -0
  111. package/dist/index.js +59 -0
  112. package/dist/native/QafkaAttestation.d.ts +23 -0
  113. package/dist/native/QafkaAttestation.js +70 -0
  114. package/dist/native/QafkaAudio.d.ts +14 -0
  115. package/dist/native/QafkaAudio.js +31 -0
  116. package/dist/native/QafkaClipboard.d.ts +11 -0
  117. package/dist/native/QafkaClipboard.js +14 -0
  118. package/dist/native/QafkaStorage.d.ts +15 -0
  119. package/dist/native/QafkaStorage.js +12 -0
  120. package/dist/resolve-project-config.d.ts +35 -0
  121. package/dist/resolve-project-config.js +41 -0
  122. package/dist/runtime-config-loader.d.ts +37 -0
  123. package/dist/runtime-config-loader.js +53 -0
  124. package/dist/services/AttestationManager.d.ts +38 -0
  125. package/dist/services/AttestationManager.js +296 -0
  126. package/dist/services/BackendService.d.ts +156 -0
  127. package/dist/services/BackendService.js +755 -0
  128. package/dist/services/ConversationManager.d.ts +43 -0
  129. package/dist/services/ConversationManager.js +96 -0
  130. package/dist/services/NavigationHandler.d.ts +29 -0
  131. package/dist/services/NavigationHandler.js +70 -0
  132. package/dist/services/RealtimeService.d.ts +83 -0
  133. package/dist/services/RealtimeService.js +203 -0
  134. package/dist/services/storage.d.ts +11 -0
  135. package/dist/services/storage.js +15 -0
  136. package/dist/services/storageCore.d.ts +17 -0
  137. package/dist/services/storageCore.js +46 -0
  138. package/dist/themes/dark.d.ts +5 -0
  139. package/dist/themes/dark.js +129 -0
  140. package/dist/themes/index.d.ts +12 -0
  141. package/dist/themes/index.js +33 -0
  142. package/dist/themes/light.d.ts +5 -0
  143. package/dist/themes/light.js +129 -0
  144. package/dist/themes/types.d.ts +155 -0
  145. package/dist/themes/types.js +5 -0
  146. package/dist/types/chat.d.ts +126 -0
  147. package/dist/types/chat.js +5 -0
  148. package/dist/types/components.d.ts +56 -0
  149. package/dist/types/components.js +16 -0
  150. package/dist/types/external-navigation.d.ts +19 -0
  151. package/dist/types/external-navigation.js +8 -0
  152. package/dist/types/index.d.ts +9 -0
  153. package/dist/types/index.js +25 -0
  154. package/dist/types/navigation.d.ts +86 -0
  155. package/dist/types/navigation.js +5 -0
  156. package/dist/types/sdk.d.ts +36 -0
  157. package/dist/types/sdk.js +5 -0
  158. package/dist/utils/deepMerge.d.ts +46 -0
  159. package/dist/utils/deepMerge.js +70 -0
  160. package/dist/utils/fontUtils.d.ts +8 -0
  161. package/dist/utils/fontUtils.js +16 -0
  162. package/dist/validate-end-user.d.ts +18 -0
  163. package/dist/validate-end-user.js +74 -0
  164. package/expo-plugin/withQafkaAttestation.js +57 -0
  165. package/ios/QafkaAttestation.m +25 -0
  166. package/ios/QafkaAttestation.swift +128 -0
  167. package/ios/QafkaAudio.m +23 -0
  168. package/ios/QafkaAudio.swift +519 -0
  169. package/ios/QafkaClipboard.m +10 -0
  170. package/ios/QafkaClipboard.swift +21 -0
  171. package/ios/QafkaReactImports.h +2 -0
  172. package/ios/QafkaStorage.m +26 -0
  173. package/ios/QafkaStorage.swift +118 -0
  174. package/package.json +82 -0
  175. package/qafka.config.d.ts +9 -0
  176. package/qafka.config.js +9 -0
  177. package/react-native-qafka.podspec +28 -0
  178. package/react-native.config.js +14 -0
@@ -0,0 +1,755 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BackendService = void 0;
4
+ const react_native_1 = require("react-native");
5
+ const package_json_1 = require("../../package.json");
6
+ const QafkaAttestation_1 = require("../native/QafkaAttestation");
7
+ /**
8
+ * Backend API Service using native fetch
9
+ */
10
+ class BackendService {
11
+ baseURL;
12
+ apiKey;
13
+ subProjectId = null;
14
+ // Instance state for the explicit end-user lane. Set via `setEndUser()`
15
+ // whenever the host app's `<Qafka endUserId=... />` prop changes
16
+ // (sign-in / sign-out / impersonation). Injected into every outgoing
17
+ // chat request payload.
18
+ endUserId = null;
19
+ endUserData = null;
20
+ timeout;
21
+ bundleId = null;
22
+ appVersion = null;
23
+ deviceModel = null;
24
+ sessionTokenGetter = null;
25
+ attestationManager = null;
26
+ refreshing = null;
27
+ // locale is only added to sdkContext when explicitly set by the
28
+ // developer via <Qafka locale="..." />. Drives both UI strings and the
29
+ // user-message context prefix on the server.
30
+ locale = null;
31
+ // Tool Data Channel
32
+ onToolDataRequested = null;
33
+ toolDataResolvedFor = new Map();
34
+ static RESOLVER_TIMEOUT_MS = 3000;
35
+ constructor(apiKey, apiUrl, subProjectId) {
36
+ // If custom apiUrl provided, ensure it has /api/v1 prefix
37
+ if (apiUrl) {
38
+ // Remove trailing slash if exists
39
+ const cleanUrl = apiUrl.replace(/\/$/, '');
40
+ // Add /api/v1 if not already present
41
+ this.baseURL = cleanUrl.includes('/api/v1')
42
+ ? cleanUrl
43
+ : `${cleanUrl}/api/v1`;
44
+ }
45
+ else {
46
+ this.baseURL = 'https://api.qafka.com/api/v1';
47
+ }
48
+ this.apiKey = apiKey;
49
+ this.subProjectId = subProjectId ?? null;
50
+ this.timeout = 60000; // 60 seconds for long-running AI responses
51
+ const deviceInfo = (0, QafkaAttestation_1.getDeviceInfo)();
52
+ this.bundleId = deviceInfo.bundleId || null;
53
+ this.appVersion = deviceInfo.appVersion || null;
54
+ this.deviceModel = deviceInfo.deviceModel || react_native_1.Platform.OS;
55
+ }
56
+ setSessionTokenGetter(getter) {
57
+ this.sessionTokenGetter = getter;
58
+ }
59
+ /**
60
+ * Store the validated end-user identity for subsequent chat requests.
61
+ * Called by the React component on mount and whenever the `endUserId` /
62
+ * `endUserData` props change. Validation happens upstream
63
+ * (`validate-end-user.ts`) so this method only persists the result.
64
+ */
65
+ setEndUser(endUserId, endUserData) {
66
+ this.endUserId = endUserId;
67
+ this.endUserData = endUserData ?? null;
68
+ }
69
+ setAttestationManager(am) {
70
+ this.attestationManager = am;
71
+ }
72
+ /**
73
+ * Single-flight refresh: concurrent callers share the same in-flight promise.
74
+ * Prevents multiple parallel re-attests when several requests fail simultaneously.
75
+ */
76
+ async refreshSession() {
77
+ if (!this.attestationManager)
78
+ return null;
79
+ if (!this.refreshing) {
80
+ this.refreshing = this.attestationManager.refresh()
81
+ .finally(() => { this.refreshing = null; });
82
+ }
83
+ return this.refreshing;
84
+ }
85
+ /**
86
+ * fetch() with automatic session refresh when the backend signals it —
87
+ * reactively on a 429 refresh hint, proactively when the budget header is
88
+ * low. Caller passes an init that already has Authorization/x-api-key set;
89
+ * on retry, only the Bearer header is swapped.
90
+ */
91
+ async fetchWithRefresh(url, init, retries = 1) {
92
+ const res = await fetch(url, init);
93
+ const refreshRequired = res.headers.get('X-Token-Refresh-Required') === 'true';
94
+ const remainingStr = res.headers.get('X-Token-Budget-Remaining');
95
+ const remaining = remainingStr ? parseInt(remainingStr, 10) : NaN;
96
+ // Reactive refresh — backend explicitly requests a new token
97
+ if (res.status === 429 && refreshRequired && retries > 0) {
98
+ const newToken = await this.refreshSession();
99
+ if (newToken) {
100
+ const headers = { ...init.headers };
101
+ headers['Authorization'] = `Bearer ${newToken}`;
102
+ return this.fetchWithRefresh(url, { ...init, headers }, retries - 1);
103
+ }
104
+ }
105
+ // Proactive refresh — fire-and-forget when budget is low or backend hints
106
+ if (refreshRequired || (!isNaN(remaining) && remaining < 1000)) {
107
+ void this.refreshSession();
108
+ }
109
+ return res;
110
+ }
111
+ /**
112
+ * Tool Data Channel: register the partner-provided resolver that
113
+ * supplies transient PII (`{{tooldata.X}}` substitution) bags. Called from
114
+ * QafkaSDK with the `onToolDataRequested` prop. Pass `null` to clear.
115
+ */
116
+ setOnToolDataRequested(resolver) {
117
+ this.onToolDataRequested = resolver;
118
+ }
119
+ /**
120
+ * explicit-intent locale. Only included in sdkContext when set —
121
+ * Qafka.tsx wires this from the `locale` prop. Developer can also call
122
+ * directly via QafkaSDK.setLocale().
123
+ */
124
+ setLocale(locale) {
125
+ this.locale = locale && locale.trim().length > 0 ? locale.trim() : null;
126
+ }
127
+ /**
128
+ * Build the SDK context attached to every request: universal temporal
129
+ * fields plus locale when the developer has set it.
130
+ */
131
+ buildSdkContext() {
132
+ const now = new Date();
133
+ const timezoneOffset = -now.getTimezoneOffset();
134
+ const sign = timezoneOffset >= 0 ? '+' : '-';
135
+ const hours = String(Math.floor(Math.abs(timezoneOffset) / 60)).padStart(2, '0');
136
+ const minutes = String(Math.abs(timezoneOffset) % 60).padStart(2, '0');
137
+ const tzString = `${sign}${hours}:${minutes}`;
138
+ const localIso = now.getFullYear() + '-' +
139
+ String(now.getMonth() + 1).padStart(2, '0') + '-' +
140
+ String(now.getDate()).padStart(2, '0') + 'T' +
141
+ String(now.getHours()).padStart(2, '0') + ':' +
142
+ String(now.getMinutes()).padStart(2, '0') + ':' +
143
+ String(now.getSeconds()).padStart(2, '0') + tzString;
144
+ const sdkContext = {
145
+ dateTime: localIso,
146
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
147
+ };
148
+ if (this.locale) {
149
+ sdkContext.locale = this.locale;
150
+ }
151
+ return sdkContext;
152
+ }
153
+ /**
154
+ * Get security headers for API requests
155
+ */
156
+ async getSecurityHeaders() {
157
+ const headers = {
158
+ 'x-platform': react_native_1.Platform.OS, // 'ios' or 'android'
159
+ 'x-sdk-version': package_json_1.version,
160
+ };
161
+ if (this.apiKey) {
162
+ headers['x-api-key'] = this.apiKey;
163
+ }
164
+ // Add app version
165
+ if (this.appVersion) {
166
+ headers['x-app-version'] = this.appVersion;
167
+ }
168
+ // Add device model
169
+ if (this.deviceModel) {
170
+ headers['x-device-model'] = this.deviceModel;
171
+ }
172
+ if (this.sessionTokenGetter) {
173
+ const token = await this.sessionTokenGetter();
174
+ if (token) {
175
+ headers['Authorization'] = `Bearer ${token}`;
176
+ }
177
+ }
178
+ return headers;
179
+ }
180
+ /**
181
+ * Tool Data Channel: invokes the partner resolver, POSTs the
182
+ * opaque bag to /chat/tool-data. Per-execId dedupe, soft timeout, fail-soft.
183
+ * Called by the tool_suggestions SSE handler when `needData=true`.
184
+ */
185
+ async resolveAndPostToolData(execId, tools) {
186
+ if (this.toolDataResolvedFor.has(execId))
187
+ return;
188
+ this.toolDataResolvedFor.set(execId, true);
189
+ // Best-effort cleanup so the dedupe map doesn't grow unboundedly.
190
+ // 10 min retention is plenty for one chat turn lifecycle.
191
+ setTimeout(() => this.toolDataResolvedFor.delete(execId), 10 * 60 * 1000);
192
+ const resolver = this.onToolDataRequested;
193
+ if (!resolver)
194
+ return;
195
+ const first = tools?.[0];
196
+ const toolDescriptor = first?.tool ?? first;
197
+ if (!toolDescriptor?.key || !toolDescriptor?.name)
198
+ return;
199
+ let bag = {};
200
+ try {
201
+ const resolverResult = Promise.resolve(resolver({ key: toolDescriptor.key, name: toolDescriptor.name }));
202
+ const timed = new Promise((resolve) => setTimeout(() => resolve({}), BackendService.RESOLVER_TIMEOUT_MS));
203
+ const result = await Promise.race([resolverResult, timed]);
204
+ bag = result ?? {};
205
+ }
206
+ catch (err) {
207
+ if (__DEV__) {
208
+ console.warn('[Qafka] onToolDataRequested threw - posting empty bag', err);
209
+ }
210
+ bag = {};
211
+ }
212
+ try {
213
+ await this.postToolData(execId, bag);
214
+ }
215
+ catch (err) {
216
+ if (__DEV__) {
217
+ console.warn('[Qafka] postToolData failed (fail-soft)', err);
218
+ }
219
+ }
220
+ }
221
+ /**
222
+ * POST tooldata bag to backend. Uses the standard SDK auth headers (API key
223
+ * + device attestation session token) — same chain as sendMessageStream.
224
+ * No retry, no body persistence; 64KB cap enforced server-side.
225
+ */
226
+ async postToolData(execId, data) {
227
+ const headers = await this.getSecurityHeaders();
228
+ headers['Content-Type'] = 'application/json';
229
+ await this.fetchWithRefresh(`${this.baseURL}/chat/tool-data`, {
230
+ method: 'POST',
231
+ headers,
232
+ body: JSON.stringify({ toolExecutionId: execId, data }),
233
+ });
234
+ }
235
+ /**
236
+ * Make HTTP request with timeout
237
+ */
238
+ async fetchWithTimeout(url, options) {
239
+ const controller = new AbortController();
240
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
241
+ try {
242
+ const response = await this.fetchWithRefresh(url, {
243
+ ...options,
244
+ signal: controller.signal,
245
+ headers: {
246
+ 'Content-Type': 'application/json',
247
+ ...(await this.getSecurityHeaders()),
248
+ ...options.headers,
249
+ },
250
+ });
251
+ clearTimeout(timeoutId);
252
+ return response;
253
+ }
254
+ catch (error) {
255
+ clearTimeout(timeoutId);
256
+ if (error.name === 'AbortError') {
257
+ throw new Error('Request timeout');
258
+ }
259
+ throw error;
260
+ }
261
+ }
262
+ /**
263
+ * Handle response errors with detailed messages
264
+ */
265
+ async handleResponse(response) {
266
+ if (!response.ok) {
267
+ const errorData = await response.json().catch(() => ({}));
268
+ const errorCode = errorData.error || 'UNKNOWN_ERROR';
269
+ const errorMessage = errorData.message || response.statusText;
270
+ const hint = errorData.hint || '';
271
+ // 401: Unauthorized (Invalid/Missing API Key)
272
+ if (response.status === 401) {
273
+ if (errorCode === 'MISSING_API_KEY') {
274
+ throw new Error('❌ API key is required. Please check your configuration.');
275
+ }
276
+ if (errorCode === 'INVALID_API_KEY') {
277
+ throw new Error('❌ Invalid API key. Please check your Dashboard for the correct key.');
278
+ }
279
+ throw new Error(`❌ Unauthorized: ${errorMessage}`);
280
+ }
281
+ // 403: Forbidden (Bundle ID, Platform, Permission issues)
282
+ if (response.status === 403) {
283
+ if (errorCode === 'INVALID_BUNDLE_ID') {
284
+ const bundleIdMsg = this.bundleId
285
+ ? `Your bundle ID: ${this.bundleId}`
286
+ : 'Bundle ID could not be detected';
287
+ throw new Error(`🔒 Access Denied: ${errorMessage}\n${bundleIdMsg}\n💡 ${hint || 'Add your bundle ID to API key restrictions in the Dashboard'}`);
288
+ }
289
+ if (errorCode === 'INVALID_PLATFORM') {
290
+ throw new Error(`🔒 Access Denied: ${errorMessage}\n💡 Add '${react_native_1.Platform.OS}' to allowed platforms in the Dashboard`);
291
+ }
292
+ if (errorCode === 'INACTIVE_API_KEY') {
293
+ throw new Error('🔒 API key is inactive. Please activate it in the Dashboard.');
294
+ }
295
+ if (errorCode === 'EXPIRED_API_KEY') {
296
+ throw new Error('🔒 API key has expired. Please generate a new key in the Dashboard.');
297
+ }
298
+ if (errorCode.startsWith('PERMISSION_DENIED')) {
299
+ const feature = errorCode
300
+ .replace('PERMISSION_DENIED_', '')
301
+ .toLowerCase();
302
+ throw new Error(`🔒 Permission Denied: ${feature} access is not enabled for this API key.\n💡 Enable it in the Dashboard`);
303
+ }
304
+ throw new Error(`🔒 Access Denied: ${errorMessage}\n💡 ${hint}`);
305
+ }
306
+ // 429: Rate Limit Exceeded
307
+ if (response.status === 429) {
308
+ const retryAfter = response.headers.get('Retry-After');
309
+ const rateLimitReset = response.headers.get('X-RateLimit-Reset');
310
+ const retryMsg = retryAfter
311
+ ? `Try again in ${retryAfter} seconds`
312
+ : rateLimitReset
313
+ ? `Try again at ${new Date(parseInt(rateLimitReset) * 1000).toLocaleTimeString()}`
314
+ : 'Try again later';
315
+ throw new Error(`⚠️ Rate limit exceeded: ${errorMessage}\n💡 ${retryMsg}`);
316
+ }
317
+ // Other errors
318
+ throw new Error(errorData.message ||
319
+ `❌ HTTP Error ${response.status}: ${response.statusText}`);
320
+ }
321
+ return response.json();
322
+ }
323
+ /**
324
+ * Send chat message to backend
325
+ */
326
+ async sendMessage(message, sessionId, context, contextDescription, isInitialMessage) {
327
+ try {
328
+ // Filter out undefined values from context before sending
329
+ const cleanedContext = context
330
+ ? Object.keys(context).reduce((acc, key) => {
331
+ const value = context[key];
332
+ if (value !== undefined) {
333
+ acc[key] = value;
334
+ }
335
+ return acc;
336
+ }, {})
337
+ : undefined;
338
+ const request = {
339
+ message,
340
+ sessionId,
341
+ ...(this.subProjectId && { subProjectId: this.subProjectId }),
342
+ ...(this.endUserId !== null && { endUserId: this.endUserId }),
343
+ ...(this.endUserData !== null && { endUserData: this.endUserData }),
344
+ context: cleanedContext
345
+ ? {
346
+ metadata: {
347
+ ...cleanedContext,
348
+ ...(contextDescription && { contextDescription }),
349
+ },
350
+ }
351
+ : undefined,
352
+ sdkContext: this.buildSdkContext(),
353
+ ...(isInitialMessage && { isInitialMessage: true }),
354
+ };
355
+ const response = await this.fetchWithTimeout(`${this.baseURL}/chat`, {
356
+ method: 'POST',
357
+ body: JSON.stringify(request),
358
+ });
359
+ return this.handleResponse(response);
360
+ }
361
+ catch (error) {
362
+ throw new Error(error.message || 'Failed to send message');
363
+ }
364
+ }
365
+ /**
366
+ * Get conversation history
367
+ */
368
+ async getHistory(sessionId, limit = 50) {
369
+ try {
370
+ const params = new URLSearchParams({
371
+ sessionId,
372
+ limit: limit.toString(),
373
+ });
374
+ const response = await this.fetchWithTimeout(`${this.baseURL}/chat/history?${params}`, { method: 'GET' });
375
+ return this.handleResponse(response);
376
+ }
377
+ catch (error) {
378
+ throw new Error('Failed to fetch conversation history');
379
+ }
380
+ }
381
+ /**
382
+ * Delete conversation
383
+ */
384
+ async deleteConversation(sessionId) {
385
+ try {
386
+ const params = new URLSearchParams({ sessionId });
387
+ const response = await this.fetchWithTimeout(`${this.baseURL}/chat/conversation?${params}`, { method: 'DELETE' });
388
+ if (!response.ok) {
389
+ throw new Error(`Failed to delete: ${response.statusText}`);
390
+ }
391
+ }
392
+ catch (error) {
393
+ throw new Error('Failed to delete conversation');
394
+ }
395
+ }
396
+ /**
397
+ * Get project theme
398
+ */
399
+ async getTheme() {
400
+ try {
401
+ const cleanApiUrl = this.baseURL.replace(/\/api\/v1\/?$/, '');
402
+ const queryParams = this.subProjectId
403
+ ? `?subProjectId=${encodeURIComponent(this.subProjectId)}`
404
+ : '';
405
+ const response = await this.fetchWithTimeout(`${cleanApiUrl}/api/v1/public/projects/chat-theme${queryParams}`, { method: 'GET' });
406
+ return this.handleResponse(response);
407
+ }
408
+ catch (error) {
409
+ throw new Error(error.message || 'Failed to fetch theme');
410
+ }
411
+ }
412
+ /**
413
+ * Send message with streaming response
414
+ * React Native compatible version using XMLHttpRequest
415
+ */
416
+ async sendMessageStream(message, sessionId, onChunk, onComplete, onError, context, contextDescription, onToolSuggestions, // Tool suggestions callback
417
+ isInitialMessage, onActionResult, // Action result callback
418
+ onStepCompleted, // Step completed callback
419
+ onFileUploadRequest, onExtractionResult,
420
+ // three new events for server-managed tool execution modes
421
+ onToolStatus, onToolResultPayload, onFinalChunk) {
422
+ try {
423
+ // Filter out undefined values from context before sending
424
+ const cleanedContext = context
425
+ ? Object.keys(context).reduce((acc, key) => {
426
+ const value = context[key];
427
+ if (value !== undefined) {
428
+ acc[key] = value;
429
+ }
430
+ return acc;
431
+ }, {})
432
+ : undefined;
433
+ const request = {
434
+ message,
435
+ sessionId,
436
+ ...(this.subProjectId && { subProjectId: this.subProjectId }),
437
+ ...(this.endUserId !== null && { endUserId: this.endUserId }),
438
+ ...(this.endUserData !== null && { endUserData: this.endUserData }),
439
+ context: cleanedContext
440
+ ? {
441
+ metadata: {
442
+ ...cleanedContext,
443
+ ...(contextDescription && { contextDescription }),
444
+ },
445
+ }
446
+ : undefined,
447
+ sdkContext: this.buildSdkContext(),
448
+ ...(isInitialMessage && { isInitialMessage: true }),
449
+ };
450
+ // Use XMLHttpRequest for React Native streaming support
451
+ const xhr = new XMLHttpRequest();
452
+ let fullResponse = '';
453
+ let messageId = '';
454
+ let navigationSuggestion = undefined;
455
+ let externalSuggestions = [];
456
+ let lastProcessedIndex = 0;
457
+ let firstChunkReceived = false; // ⏱️ Track first chunk
458
+ xhr.onprogress = () => {
459
+ // Get new data since last update
460
+ const currentText = xhr.responseText;
461
+ const newText = currentText.substring(lastProcessedIndex);
462
+ lastProcessedIndex = currentText.length;
463
+ if (!newText)
464
+ return;
465
+ // ⏱️ Track first chunk arrival
466
+ if (!firstChunkReceived) {
467
+ firstChunkReceived = true;
468
+ }
469
+ // Parse SSE events (format: "data: {...}\n\n")
470
+ const lines = newText.split('\n');
471
+ for (const line of lines) {
472
+ if (line.startsWith('data: ')) {
473
+ const jsonStr = line.slice(6).trim(); // Remove "data: " prefix
474
+ if (!jsonStr)
475
+ continue;
476
+ try {
477
+ const event = JSON.parse(jsonStr);
478
+ switch (event.type) {
479
+ case 'content':
480
+ // Stream content chunk
481
+ fullResponse += event.content;
482
+ onChunk(event.content);
483
+ break;
484
+ case 'navigation':
485
+ navigationSuggestion = event.navigation;
486
+ break;
487
+ case 'external_navigation':
488
+ // External suggestions (WhatsApp, phone, map, app store, …)
489
+ // emitted by backend after the assistant message.
490
+ externalSuggestions = event.externalSuggestions || event.data?.externalSuggestions || [];
491
+ break;
492
+ case 'tool_suggestions':
493
+ // Tool Registry suggestions
494
+ if (onToolSuggestions && event.tools) {
495
+ onToolSuggestions(event.tools, event.conversationId, event.messageId);
496
+ }
497
+ // Tool Data Channel. Fire-and-forget; do NOT await so
498
+ // other stream events aren't blocked while the partner resolver
499
+ // runs. resolveAndPostToolData is fail-soft and bounded by a
500
+ // 3-second timeout.
501
+ if (event.needData && event.toolExecutionId) {
502
+ void this.resolveAndPostToolData(event.toolExecutionId, event.tools || []);
503
+ }
504
+ break;
505
+ case 'tool_status':
506
+ // status pill for server-managed tools
507
+ if (onToolStatus) {
508
+ onToolStatus({
509
+ toolKey: event.toolKey,
510
+ message: event.message,
511
+ stage: event.stage,
512
+ });
513
+ }
514
+ break;
515
+ case 'tool_result_payload':
516
+ // backend-fetched tool payload to render in widget
517
+ if (onToolResultPayload) {
518
+ onToolResultPayload({
519
+ toolKey: event.toolKey,
520
+ data: event.data,
521
+ uiConfig: event.uiConfig,
522
+ });
523
+ }
524
+ break;
525
+ case 'final_chunk':
526
+ // final AI text after server-side tool execution
527
+ if (onFinalChunk && typeof event.content === 'string') {
528
+ onFinalChunk(event.content);
529
+ }
530
+ break;
531
+ case 'action_result':
532
+ // Action execution results
533
+ if (onActionResult && event.results) {
534
+ onActionResult(event.results);
535
+ }
536
+ break;
537
+ case 'step_completed':
538
+ // Step completed during multi-step tool flow
539
+ if (onStepCompleted) {
540
+ onStepCompleted({ tool: event.tool, step: event.step, data: event.data, actionResults: event.actionResults });
541
+ }
542
+ break;
543
+ case 'file_uploaded':
544
+ // Backend confirmed file upload or requests file from user
545
+ if (onFileUploadRequest && event.fileInput) {
546
+ onFileUploadRequest({
547
+ toolId: event.toolId,
548
+ fileInput: event.fileInput,
549
+ submit: async (file) => {
550
+ try {
551
+ const result = await this.uploadFile(event.toolId, file, event.conversationId);
552
+ if (result.extraction && onExtractionResult) {
553
+ onExtractionResult(result.extraction);
554
+ }
555
+ }
556
+ catch (err) {
557
+ onError(err instanceof Error ? err : new Error('File upload failed'));
558
+ }
559
+ },
560
+ cancel: () => {
561
+ // No-op — user cancelled file selection
562
+ },
563
+ });
564
+ }
565
+ break;
566
+ case 'extraction_result':
567
+ if (onExtractionResult) {
568
+ onExtractionResult({
569
+ fileId: event.fileId,
570
+ toolId: event.toolId,
571
+ data: event.data,
572
+ status: event.status,
573
+ incompleteFields: event.incompleteFields,
574
+ });
575
+ }
576
+ break;
577
+ case 'done':
578
+ // Stream complete
579
+ messageId = event.messageId;
580
+ break;
581
+ case 'error':
582
+ onError(new Error(event.error));
583
+ return;
584
+ }
585
+ }
586
+ catch {
587
+ // Skip malformed JSON silently
588
+ }
589
+ }
590
+ }
591
+ };
592
+ xhr.onload = () => {
593
+ if (xhr.status === 200) {
594
+ // Call onComplete with full response
595
+ onComplete({
596
+ id: messageId || `msg-${Date.now()}`,
597
+ text: fullResponse,
598
+ timestamp: new Date(),
599
+ navigationSuggestion,
600
+ externalSuggestions: externalSuggestions.length > 0 ? externalSuggestions : undefined,
601
+ });
602
+ }
603
+ else {
604
+ try {
605
+ const errorData = JSON.parse(xhr.responseText);
606
+ onError(new Error(errorData.message || `HTTP Error: ${xhr.status}`));
607
+ }
608
+ catch {
609
+ onError(new Error(`HTTP Error: ${xhr.status}`));
610
+ }
611
+ }
612
+ };
613
+ xhr.onerror = () => {
614
+ onError(new Error('Network error occurred'));
615
+ };
616
+ xhr.ontimeout = () => {
617
+ onError(new Error('Request timeout'));
618
+ };
619
+ xhr.open('POST', `${this.baseURL}/chat/stream`);
620
+ xhr.setRequestHeader('Content-Type', 'application/json');
621
+ const securityHeaders = await this.getSecurityHeaders();
622
+ for (const [key, value] of Object.entries(securityHeaders)) {
623
+ xhr.setRequestHeader(key, value);
624
+ }
625
+ xhr.timeout = this.timeout;
626
+ xhr.send(JSON.stringify(request));
627
+ }
628
+ catch (error) {
629
+ onError(error instanceof Error
630
+ ? error
631
+ : new Error(error.message || 'Stream failed'));
632
+ }
633
+ }
634
+ /**
635
+ * Post a tool result back to the backend so the AI can continue its turn.
636
+ * Streams back `final_chunk` and `done` events.
637
+ */
638
+ async postToolResult(payload, onFinalChunk, onDone, onError, onCard) {
639
+ const url = `${this.baseURL}/chat/tool-result`;
640
+ const headers = await this.getSecurityHeaders();
641
+ headers['Content-Type'] = 'application/json';
642
+ return new Promise((resolve) => {
643
+ const xhr = new XMLHttpRequest();
644
+ xhr.open('POST', url, true);
645
+ for (const [k, v] of Object.entries(headers)) {
646
+ xhr.setRequestHeader(k, v);
647
+ }
648
+ let processedLength = 0;
649
+ let settled = false;
650
+ const settle = () => {
651
+ if (!settled) {
652
+ settled = true;
653
+ resolve();
654
+ }
655
+ };
656
+ xhr.onprogress = () => {
657
+ const chunk = xhr.responseText.substring(processedLength);
658
+ processedLength = xhr.responseText.length;
659
+ if (!chunk)
660
+ return;
661
+ // Parse SSE events line-by-line (match sendMessageStream pattern)
662
+ const lines = chunk.split('\n');
663
+ for (const line of lines) {
664
+ if (!line.startsWith('data: '))
665
+ continue;
666
+ const jsonStr = line.slice(6).trim();
667
+ if (!jsonStr)
668
+ continue;
669
+ try {
670
+ const event = JSON.parse(jsonStr);
671
+ if (event.type === 'final_chunk' && typeof event.content === 'string') {
672
+ onFinalChunk(event.content);
673
+ }
674
+ else if (event.type === 'card' && event.card) {
675
+ onCard?.(event.card);
676
+ }
677
+ else if (event.type === 'done') {
678
+ onDone();
679
+ settle();
680
+ return;
681
+ }
682
+ else if (event.type === 'error') {
683
+ onError(new Error(event.message || 'tool-result error'));
684
+ settle();
685
+ return;
686
+ }
687
+ }
688
+ catch {
689
+ // Skip malformed SSE frames
690
+ }
691
+ }
692
+ };
693
+ xhr.onerror = () => {
694
+ onError(new Error('Network error posting tool result'));
695
+ settle();
696
+ };
697
+ xhr.ontimeout = () => {
698
+ onError(new Error('Timeout posting tool result'));
699
+ settle();
700
+ };
701
+ xhr.onload = () => {
702
+ // In case done wasn't emitted but stream ended
703
+ if (xhr.status >= 400) {
704
+ try {
705
+ const err = JSON.parse(xhr.responseText);
706
+ onError(new Error(err.message || `HTTP Error: ${xhr.status}`));
707
+ }
708
+ catch {
709
+ onError(new Error(`HTTP Error: ${xhr.status}`));
710
+ }
711
+ }
712
+ settle();
713
+ };
714
+ xhr.timeout = this.timeout;
715
+ xhr.send(JSON.stringify(payload));
716
+ });
717
+ }
718
+ async uploadFile(toolId, file, conversationId) {
719
+ const formData = new FormData();
720
+ formData.append('file', {
721
+ uri: file.uri,
722
+ name: file.name,
723
+ type: file.type,
724
+ });
725
+ if (conversationId) {
726
+ formData.append('conversationId', conversationId);
727
+ }
728
+ // Forward the device-attestation session token when available so the
729
+ // server can tie this upload to an attested device.
730
+ const headers = {
731
+ 'x-platform': react_native_1.Platform.OS,
732
+ 'x-sdk-version': package_json_1.version,
733
+ };
734
+ if (this.apiKey) {
735
+ headers['x-api-key'] = this.apiKey;
736
+ }
737
+ if (this.sessionTokenGetter) {
738
+ const sessionToken = await this.sessionTokenGetter();
739
+ if (sessionToken) {
740
+ headers['Authorization'] = `Bearer ${sessionToken}`;
741
+ }
742
+ }
743
+ const response = await this.fetchWithRefresh(`${this.baseURL}/tools-v2/${toolId}/files/upload`, {
744
+ method: 'POST',
745
+ headers,
746
+ body: formData,
747
+ });
748
+ if (!response.ok) {
749
+ const error = await response.json().catch(() => ({ message: 'Upload failed' }));
750
+ throw new Error(error.message || `Upload failed: ${response.status}`);
751
+ }
752
+ return response.json();
753
+ }
754
+ }
755
+ exports.BackendService = BackendService;