@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,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isClipboardAvailable = isClipboardAvailable;
4
+ exports.setString = setString;
5
+ const react_native_1 = require("react-native");
6
+ const native = react_native_1.NativeModules.QafkaClipboard ?? null;
7
+ function isClipboardAvailable() {
8
+ return native !== null;
9
+ }
10
+ async function setString(value) {
11
+ if (!native)
12
+ return;
13
+ await native.setString(value);
14
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Native storage module — persists key/value pairs in OS-level secure storage.
3
+ * iOS: Keychain Services with kSecAttrSynchronizable=false (no iCloud sync).
4
+ * Android: AndroidX EncryptedSharedPreferences (AES256_GCM).
5
+ *
6
+ * Uninstalling the host app deletes everything written here on both platforms.
7
+ */
8
+ export interface QafkaStorageNative {
9
+ getItem(key: string): Promise<string | null>;
10
+ setItem(key: string, value: string): Promise<void>;
11
+ removeItem(key: string): Promise<void>;
12
+ multiRemove(keys: string[]): Promise<void>;
13
+ }
14
+ export declare function isStorageAvailable(): boolean;
15
+ export declare function getNativeStorage(): QafkaStorageNative | null;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isStorageAvailable = isStorageAvailable;
4
+ exports.getNativeStorage = getNativeStorage;
5
+ const react_native_1 = require("react-native");
6
+ const native = react_native_1.NativeModules.QafkaStorage ?? null;
7
+ function isStorageAvailable() {
8
+ return native !== null;
9
+ }
10
+ function getNativeStorage() {
11
+ return native;
12
+ }
@@ -0,0 +1,35 @@
1
+ import type { RuntimeConfig } from './runtime-config-loader';
2
+ /**
3
+ * Subset of `<Qafka />` props the resolver needs. Keeping this narrow lets
4
+ * the resolver stay pure and easy to test in isolation.
5
+ */
6
+ export interface ProjectConfigProps {
7
+ apiUrl?: string;
8
+ projectId?: string;
9
+ }
10
+ export interface ResolvedProjectConfig {
11
+ apiKey: string | null;
12
+ apiUrl: string | undefined;
13
+ }
14
+ /**
15
+ * Resolve the development API key + backend URL the SDK should use.
16
+ *
17
+ * Only called in `__DEV__` builds; the call site is stripped from production
18
+ * bundles, so `qafka.config.js` never ships to production. Production builds
19
+ * authenticate without this config.
20
+ *
21
+ * Resolution order for `apiKey`:
22
+ * 1. `projectId` prop → `runtimeConfig.projects[projectId].developmentKey`.
23
+ * 2. `runtimeConfig.defaultProjectId` → that project's `developmentKey`.
24
+ * 3. `null` — caller may still proceed; backend falls back gracefully.
25
+ *
26
+ * Resolution order for `apiUrl`:
27
+ * 1. Explicit `apiUrl` prop.
28
+ * 2. `runtimeConfig.apiUrl`.
29
+ * 3. `undefined` — the downstream `BackendService` applies the SDK default.
30
+ *
31
+ * Throws a `RangeError` when an explicit `projectId` does not exist in the
32
+ * runtime config — that is always a developer mistake and silently falling
33
+ * back to the default would mask the typo.
34
+ */
35
+ export declare function resolveProjectConfig(props: ProjectConfigProps, runtimeConfig: RuntimeConfig | null): ResolvedProjectConfig;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveProjectConfig = resolveProjectConfig;
4
+ /**
5
+ * Resolve the development API key + backend URL the SDK should use.
6
+ *
7
+ * Only called in `__DEV__` builds; the call site is stripped from production
8
+ * bundles, so `qafka.config.js` never ships to production. Production builds
9
+ * authenticate without this config.
10
+ *
11
+ * Resolution order for `apiKey`:
12
+ * 1. `projectId` prop → `runtimeConfig.projects[projectId].developmentKey`.
13
+ * 2. `runtimeConfig.defaultProjectId` → that project's `developmentKey`.
14
+ * 3. `null` — caller may still proceed; backend falls back gracefully.
15
+ *
16
+ * Resolution order for `apiUrl`:
17
+ * 1. Explicit `apiUrl` prop.
18
+ * 2. `runtimeConfig.apiUrl`.
19
+ * 3. `undefined` — the downstream `BackendService` applies the SDK default.
20
+ *
21
+ * Throws a `RangeError` when an explicit `projectId` does not exist in the
22
+ * runtime config — that is always a developer mistake and silently falling
23
+ * back to the default would mask the typo.
24
+ */
25
+ function resolveProjectConfig(props, runtimeConfig) {
26
+ const apiUrl = props.apiUrl ?? runtimeConfig?.apiUrl ?? undefined;
27
+ if (props.projectId) {
28
+ const entry = runtimeConfig?.projects[props.projectId];
29
+ if (!entry) {
30
+ throw new RangeError(`Qafka: projectId "${props.projectId}" is not registered in qafka.config.js. ` +
31
+ 'Run `qafka project` to add it.');
32
+ }
33
+ return { apiKey: entry.developmentKey ?? null, apiUrl };
34
+ }
35
+ const defaultId = runtimeConfig?.defaultProjectId;
36
+ if (defaultId) {
37
+ const entry = runtimeConfig?.projects[defaultId];
38
+ return { apiKey: entry?.developmentKey ?? null, apiUrl };
39
+ }
40
+ return { apiKey: null, apiUrl };
41
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Per-project entry in `qafka.config.js`. The SDK only consumes
3
+ * `developmentKey`; the CLI may add more fields later.
4
+ */
5
+ export interface RuntimeProjectEntry {
6
+ /** The project's TEST API key, used by the SDK in dev builds. */
7
+ developmentKey?: string;
8
+ }
9
+ /**
10
+ * Contract shipped by the CLI in `node_modules/@qafka/react-native/qafka.config.js`.
11
+ * The package ships a placeholder version of this file so Metro can always
12
+ * resolve the import; the CLI overwrites it with real values on the
13
+ * consumer's machine.
14
+ */
15
+ export interface RuntimeConfig {
16
+ /** Project the SDK uses when mounted without an explicit `projectId`. */
17
+ defaultProjectId?: string;
18
+ /** Backend base URL override (without `/api/v1` suffix). */
19
+ apiUrl?: string;
20
+ /** Every registered project, keyed by project id. */
21
+ projects: Record<string, RuntimeProjectEntry>;
22
+ }
23
+ /**
24
+ * Narrow a value returned by `require('../qafka.config')` into a
25
+ * `RuntimeConfig`. Exported separately so it can be unit-tested without
26
+ * filesystem coupling.
27
+ */
28
+ export declare function normalizeRuntimeConfig(raw: unknown): RuntimeConfig | null;
29
+ /**
30
+ * Load the runtime config bundled with the SDK package. The path resolves
31
+ * at compile time to `node_modules/@qafka/react-native/qafka.config.js`
32
+ * on the consumer's machine — a placeholder ships with the package so this
33
+ * `require` always succeeds, and the CLI overwrites it with real values on
34
+ * `qafka init`. Returns `null` only if the require throws (e.g. the
35
+ * placeholder was somehow removed).
36
+ */
37
+ export declare function loadRuntimeConfig(): RuntimeConfig | null;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeRuntimeConfig = normalizeRuntimeConfig;
4
+ exports.loadRuntimeConfig = loadRuntimeConfig;
5
+ /**
6
+ * Narrow a value returned by `require('../qafka.config')` into a
7
+ * `RuntimeConfig`. Exported separately so it can be unit-tested without
8
+ * filesystem coupling.
9
+ */
10
+ function normalizeRuntimeConfig(raw) {
11
+ if (!raw || typeof raw !== 'object')
12
+ return null;
13
+ const obj = raw;
14
+ const projectsRaw = obj.projects;
15
+ const projects = {};
16
+ if (projectsRaw && typeof projectsRaw === 'object') {
17
+ for (const [id, value] of Object.entries(projectsRaw)) {
18
+ if (value && typeof value === 'object') {
19
+ const entry = value;
20
+ const developmentKey = typeof entry.developmentKey === 'string' ? entry.developmentKey : undefined;
21
+ projects[id] = developmentKey !== undefined ? { developmentKey } : {};
22
+ }
23
+ }
24
+ }
25
+ const out = { projects };
26
+ if (typeof obj.defaultProjectId === 'string') {
27
+ out.defaultProjectId = obj.defaultProjectId;
28
+ }
29
+ if (typeof obj.apiUrl === 'string') {
30
+ out.apiUrl = obj.apiUrl;
31
+ }
32
+ return out;
33
+ }
34
+ /**
35
+ * Load the runtime config bundled with the SDK package. The path resolves
36
+ * at compile time to `node_modules/@qafka/react-native/qafka.config.js`
37
+ * on the consumer's machine — a placeholder ships with the package so this
38
+ * `require` always succeeds, and the CLI overwrites it with real values on
39
+ * `qafka init`. Returns `null` only if the require throws (e.g. the
40
+ * placeholder was somehow removed).
41
+ */
42
+ function loadRuntimeConfig() {
43
+ try {
44
+ // The compiled output of this file lives at `dist/runtime-config-loader.js`,
45
+ // so `../qafka.config` resolves to the package-root file.
46
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
47
+ const raw = require('../qafka.config');
48
+ return normalizeRuntimeConfig(raw);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
@@ -0,0 +1,38 @@
1
+ interface AttestationConfig {
2
+ apiUrl: string;
3
+ apiKey: string | null;
4
+ debug?: boolean;
5
+ }
6
+ export declare class AttestationManager {
7
+ private config;
8
+ private sessionToken;
9
+ private keyId;
10
+ constructor(config: AttestationConfig);
11
+ private log;
12
+ initialize(): Promise<void>;
13
+ private initializeIos;
14
+ private performIosAttestation;
15
+ private performIosAssertion;
16
+ private initializeAndroid;
17
+ private performAndroidAttestation;
18
+ getSessionToken(): Promise<string | null>;
19
+ /**
20
+ * Force a session token refresh. iOS tries the lightweight assertion path
21
+ * first (single signature, no cert chain) and falls back to a full
22
+ * re-attestation if the assertion is rejected — in some environments a
23
+ * stored attestation public key occasionally fails server-side signature
24
+ * verification. Full re-attestation always recovers because the cert chain
25
+ * is re-validated from scratch.
26
+ *
27
+ * Called by BackendService when the backend signals budget exhaustion or
28
+ * imminent token expiry via response headers.
29
+ */
30
+ refresh(): Promise<string | null>;
31
+ private refreshSession;
32
+ isDevBypass(): boolean;
33
+ private getHeaders;
34
+ private requestChallenge;
35
+ private submitAttestation;
36
+ reset(): Promise<void>;
37
+ }
38
+ export {};
@@ -0,0 +1,296 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AttestationManager = void 0;
37
+ const react_native_1 = require("react-native");
38
+ const storage_1 = require("./storage");
39
+ const NativeAttestation = __importStar(require("../native/QafkaAttestation"));
40
+ const QafkaAttestation_1 = require("../native/QafkaAttestation");
41
+ const STORAGE_KEY_ID = '@qafka/attestation_key_id';
42
+ class AttestationManager {
43
+ config;
44
+ sessionToken = null;
45
+ keyId = null;
46
+ constructor(config) {
47
+ this.config = config;
48
+ }
49
+ // Production: log basic state only. Verbose details (URLs, endpoint paths)
50
+ // are gated behind `__DEV__` or `config.debug` to keep release-build logs
51
+ // minimal.
52
+ log(message, verboseMessage) {
53
+ const verbose = this.config.debug || __DEV__;
54
+ console.warn(verbose && verboseMessage ? verboseMessage : message);
55
+ }
56
+ async initialize() {
57
+ const moduleLinked = (0, QafkaAttestation_1.isAttestationAvailable)();
58
+ // Diagnostic logs are intentionally NOT __DEV__-gated. Init is a once-per-
59
+ // session event; surfacing the path taken in Xcode device console / logcat
60
+ // is essential for diagnosing release-build failures where the JS-side
61
+ // error overlay only shows a generic "App configuration error".
62
+ this.log(`[Qafka:init] platform=${react_native_1.Platform.OS} moduleLinked=${moduleLinked}`, `[Qafka:init] platform=${react_native_1.Platform.OS} url=${this.config.apiUrl} moduleLinked=${moduleLinked}`);
63
+ const supported = await NativeAttestation.isSupported();
64
+ this.log(`[Qafka:init] isSupported=${supported}`);
65
+ if (!supported) {
66
+ const reason = !moduleLinked
67
+ ? `the QafkaAttestation native module is not registered (rebuild your app after installing the SDK)`
68
+ : `this device does not support attestation (Android 8.0+ / iOS 14+ required)`;
69
+ if (__DEV__) {
70
+ console.warn(`[Qafka] Attestation unavailable in this dev environment (${reason}). ` +
71
+ `Continuing without it; see the Qafka docs for local development setup.`);
72
+ this.sessionToken = { token: 'dev-bypass', expiresAt: Date.now() + 30 * 60 * 1000 };
73
+ return;
74
+ }
75
+ throw new Error(`Device attestation is not supported: ${reason}.`);
76
+ }
77
+ try {
78
+ if (react_native_1.Platform.OS === 'ios') {
79
+ await this.initializeIos();
80
+ }
81
+ else if (react_native_1.Platform.OS === 'android') {
82
+ await this.initializeAndroid();
83
+ }
84
+ this.log('[Qafka:init] attestation OK, session token acquired');
85
+ }
86
+ catch (err) {
87
+ const msg = err instanceof Error ? err.message : String(err);
88
+ this.log(`[Qafka:init] attestation FAILED: ${msg}`);
89
+ throw err;
90
+ }
91
+ }
92
+ async initializeIos() {
93
+ this.keyId = await storage_1.storage.getItem(STORAGE_KEY_ID);
94
+ if (!this.keyId) {
95
+ this.keyId = await NativeAttestation.generateKey();
96
+ await storage_1.storage.setItem(STORAGE_KEY_ID, this.keyId);
97
+ }
98
+ // Always perform a fresh attestation at session start. Lighter
99
+ // assertions (`performIosAssertion`) are used later to refresh expired
100
+ // tokens within the same session.
101
+ try {
102
+ await this.performIosAttestation();
103
+ }
104
+ catch (err) {
105
+ // Self-heal ONLY when the failure looks like a stale local key (the
106
+ // stored keyId references a native key the OS no longer recognises —
107
+ // reinstall, OS reset, pruned key, etc.).
108
+ //
109
+ // Recover from native key/attestation errors only, NEVER from
110
+ // server-side rejections: a server error means the local key is fine,
111
+ // so regenerating it would not help. DeviceCheck surfaces stale-key
112
+ // conditions via `com.apple.devicecheck.error` and Android Keystore via
113
+ // an opaque KeystoreException, so we match those framework prefixes in
114
+ // addition to the English heuristic.
115
+ const msg = err instanceof Error ? err.message : String(err);
116
+ const looksLikeStaleNativeKey = !/HTTP|\b401\b|\b403\b|\b404\b|\b5\d\d\b|app not registered|app_not_registered|fetch|network/i.test(msg) &&
117
+ (/invalid|not found|unknown|key|attestation/i.test(msg) ||
118
+ /com\.apple\.devicecheck\.error/i.test(msg) ||
119
+ /android\.security\.keystore/i.test(msg));
120
+ if (!looksLikeStaleNativeKey) {
121
+ this.log(`[Qafka:init] attestation failed (no key regeneration — not a stale-key signature): ${msg}`);
122
+ throw err;
123
+ }
124
+ this.log(`[Qafka:init] initial attestation failed (${msg}), regenerating key and retrying once`);
125
+ await storage_1.storage.removeItem(STORAGE_KEY_ID);
126
+ this.keyId = await NativeAttestation.generateKey();
127
+ await storage_1.storage.setItem(STORAGE_KEY_ID, this.keyId);
128
+ await this.performIosAttestation();
129
+ }
130
+ }
131
+ async performIosAttestation() {
132
+ const challenge = await this.requestChallenge('ios');
133
+ const attestationData = await NativeAttestation.attestKey(this.keyId, challenge);
134
+ const response = await this.submitAttestation({
135
+ platform: 'ios',
136
+ attestationData,
137
+ keyId: this.keyId,
138
+ challenge,
139
+ });
140
+ this.sessionToken = {
141
+ token: response.sessionToken,
142
+ expiresAt: Date.now() + response.expiresIn * 1000,
143
+ };
144
+ }
145
+ async performIosAssertion() {
146
+ const challenge = await this.requestChallenge('ios');
147
+ const assertionData = await NativeAttestation.generateAssertion(this.keyId, challenge);
148
+ const response = await this.submitAttestation({
149
+ platform: 'ios',
150
+ attestationData: assertionData,
151
+ keyId: this.keyId,
152
+ challenge,
153
+ isAssertion: true,
154
+ });
155
+ this.sessionToken = {
156
+ token: response.sessionToken,
157
+ expiresAt: Date.now() + response.expiresIn * 1000,
158
+ };
159
+ }
160
+ async initializeAndroid() {
161
+ await this.performAndroidAttestation();
162
+ }
163
+ async performAndroidAttestation() {
164
+ const challenge = await this.requestChallenge('android');
165
+ await NativeAttestation.generateKeyPair('qafka_attest', challenge);
166
+ const certChain = await NativeAttestation.getAttestationCertChain('qafka_attest');
167
+ const response = await this.submitAttestation({
168
+ platform: 'android',
169
+ certChain,
170
+ challenge,
171
+ });
172
+ this.sessionToken = {
173
+ token: response.sessionToken,
174
+ expiresAt: Date.now() + response.expiresIn * 1000,
175
+ };
176
+ }
177
+ async getSessionToken() {
178
+ if (this.sessionToken?.token === 'dev-bypass')
179
+ return null;
180
+ if (this.sessionToken && Date.now() < this.sessionToken.expiresAt - 60_000) {
181
+ return this.sessionToken.token;
182
+ }
183
+ try {
184
+ await this.refreshSession();
185
+ return this.sessionToken?.token || null;
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ }
191
+ /**
192
+ * Force a session token refresh. iOS tries the lightweight assertion path
193
+ * first (single signature, no cert chain) and falls back to a full
194
+ * re-attestation if the assertion is rejected — in some environments a
195
+ * stored attestation public key occasionally fails server-side signature
196
+ * verification. Full re-attestation always recovers because the cert chain
197
+ * is re-validated from scratch.
198
+ *
199
+ * Called by BackendService when the backend signals budget exhaustion or
200
+ * imminent token expiry via response headers.
201
+ */
202
+ async refresh() {
203
+ if (this.isDevBypass())
204
+ return null;
205
+ try {
206
+ await this.refreshSession();
207
+ return this.sessionToken?.token ?? null;
208
+ }
209
+ catch (err) {
210
+ this.log(`[Qafka:init] session refresh failed: ${err instanceof Error ? err.message : String(err)}`);
211
+ return null;
212
+ }
213
+ }
214
+ async refreshSession() {
215
+ if (react_native_1.Platform.OS === 'android') {
216
+ await this.performAndroidAttestation();
217
+ return;
218
+ }
219
+ try {
220
+ await this.performIosAssertion();
221
+ }
222
+ catch (assertionErr) {
223
+ // Lightweight assertion failed — fall back to a full attestation so a
224
+ // single bad assertion does not break the chat surface. The heavy path
225
+ // re-validates the cert chain and stores a fresh attestation row, which
226
+ // also clears any drift between client and server interpretation of
227
+ // the existing key.
228
+ this.log(`[Qafka:init] iOS assertion failed (${assertionErr instanceof Error ? assertionErr.message : String(assertionErr)}), falling back to full attestation`);
229
+ await this.initializeIos();
230
+ }
231
+ }
232
+ isDevBypass() {
233
+ return this.sessionToken?.token === 'dev-bypass';
234
+ }
235
+ getHeaders() {
236
+ const headers = {
237
+ 'Content-Type': 'application/json',
238
+ };
239
+ if (this.config.apiKey) {
240
+ headers['x-api-key'] = this.config.apiKey;
241
+ }
242
+ return headers;
243
+ }
244
+ async requestChallenge(platform) {
245
+ const url = `${this.config.apiUrl}/attest/challenge`;
246
+ this.log('[Qafka:init] challenge request', `[Qafka:init] POST ${url}`);
247
+ let response;
248
+ try {
249
+ response = await fetch(url, {
250
+ method: 'POST',
251
+ headers: this.getHeaders(),
252
+ body: JSON.stringify({ platform }),
253
+ });
254
+ }
255
+ catch (err) {
256
+ const msg = err instanceof Error ? err.message : String(err);
257
+ this.log(`[Qafka:init] challenge fetch threw: ${msg}`);
258
+ throw err;
259
+ }
260
+ this.log(`[Qafka:init] challenge status=${response.status}`);
261
+ if (!response.ok) {
262
+ throw new Error(`Challenge request failed: ${response.status}`);
263
+ }
264
+ const data = await response.json();
265
+ return data.challenge;
266
+ }
267
+ async submitAttestation(body) {
268
+ const url = `${this.config.apiUrl}/attest`;
269
+ this.log('[Qafka:init] attest submit', `[Qafka:init] POST ${url} isAssertion=${body.isAssertion ?? false}`);
270
+ let response;
271
+ try {
272
+ response = await fetch(url, {
273
+ method: 'POST',
274
+ headers: this.getHeaders(),
275
+ body: JSON.stringify(body),
276
+ });
277
+ }
278
+ catch (err) {
279
+ const msg = err instanceof Error ? err.message : String(err);
280
+ this.log(`[Qafka:init] attest fetch threw: ${msg}`);
281
+ throw err;
282
+ }
283
+ this.log(`[Qafka:init] attest status=${response.status}`);
284
+ if (!response.ok) {
285
+ const errorData = await response.json().catch(() => ({}));
286
+ throw new Error(`Attestation failed: ${response.status} — ${errorData.message || errorData.error || 'Unknown error'}`);
287
+ }
288
+ return response.json();
289
+ }
290
+ async reset() {
291
+ this.sessionToken = null;
292
+ this.keyId = null;
293
+ await storage_1.storage.removeItem(STORAGE_KEY_ID);
294
+ }
295
+ }
296
+ exports.AttestationManager = AttestationManager;