@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.
- package/CHANGELOG.md +12 -0
- package/CONTRIBUTING.md +92 -0
- package/LICENSE +22 -0
- package/README.md +109 -0
- package/SECURITY.md +67 -0
- package/android/build.gradle +35 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/qafka/attestation/QafkaAttestationModule.kt +92 -0
- package/android/src/main/java/com/qafka/attestation/QafkaAttestationPackage.kt +22 -0
- package/android/src/main/java/com/qafka/audio/QafkaAudioModule.kt +290 -0
- package/android/src/main/java/com/qafka/clipboard/QafkaClipboardModule.kt +28 -0
- package/android/src/main/java/com/qafka/storage/QafkaStorageModule.kt +80 -0
- package/app.plugin.js +1 -0
- package/dist/QafkaSDK.d.ts +174 -0
- package/dist/QafkaSDK.js +461 -0
- package/dist/cards/bindings/resolveFieldName.d.ts +25 -0
- package/dist/cards/bindings/resolveFieldName.js +82 -0
- package/dist/cards/cta/CardContext.d.ts +16 -0
- package/dist/cards/cta/CardContext.js +58 -0
- package/dist/cards/cta/dispatcher.d.ts +7 -0
- package/dist/cards/cta/dispatcher.js +90 -0
- package/dist/cards/cta/types.d.ts +66 -0
- package/dist/cards/cta/types.js +2 -0
- package/dist/cards/index.d.ts +20 -0
- package/dist/cards/index.js +34 -0
- package/dist/cards/primitives/QButton.d.ts +10 -0
- package/dist/cards/primitives/QButton.js +115 -0
- package/dist/cards/primitives/QDivider.d.ts +7 -0
- package/dist/cards/primitives/QDivider.js +17 -0
- package/dist/cards/primitives/QIcon.d.ts +13 -0
- package/dist/cards/primitives/QIcon.js +26 -0
- package/dist/cards/primitives/QImage.d.ts +9 -0
- package/dist/cards/primitives/QImage.js +22 -0
- package/dist/cards/primitives/QText.d.ts +9 -0
- package/dist/cards/primitives/QText.js +30 -0
- package/dist/cards/primitives/QView.d.ts +8 -0
- package/dist/cards/primitives/QView.js +19 -0
- package/dist/cards/renderer/CardRenderer.d.ts +19 -0
- package/dist/cards/renderer/CardRenderer.js +64 -0
- package/dist/cards/renderer/renderNode.d.ts +13 -0
- package/dist/cards/renderer/renderNode.js +42 -0
- package/dist/cards/types.d.ts +110 -0
- package/dist/cards/types.js +6 -0
- package/dist/components/ActionResultBadge.d.ts +12 -0
- package/dist/components/ActionResultBadge.js +58 -0
- package/dist/components/ChatPage.d.ts +44 -0
- package/dist/components/ChatPage.js +84 -0
- package/dist/components/DataChip.d.ts +8 -0
- package/dist/components/DataChip.js +80 -0
- package/dist/components/DataChipList.d.ts +13 -0
- package/dist/components/DataChipList.js +21 -0
- package/dist/components/FloatingButton.d.ts +11 -0
- package/dist/components/FloatingButton.js +162 -0
- package/dist/components/InputArea.d.ts +57 -0
- package/dist/components/InputArea.js +142 -0
- package/dist/components/MarkdownText.d.ts +15 -0
- package/dist/components/MarkdownText.js +283 -0
- package/dist/components/MessageBubble.d.ts +134 -0
- package/dist/components/MessageBubble.js +384 -0
- package/dist/components/NavigationSuggestion.d.ts +11 -0
- package/dist/components/NavigationSuggestion.js +109 -0
- package/dist/components/Qafka.d.ts +39 -0
- package/dist/components/Qafka.handlers.d.ts +21 -0
- package/dist/components/Qafka.handlers.js +54 -0
- package/dist/components/Qafka.js +493 -0
- package/dist/components/Qafka.styles.d.ts +19 -0
- package/dist/components/Qafka.styles.js +101 -0
- package/dist/components/Qafka.types.d.ts +744 -0
- package/dist/components/Qafka.types.js +2 -0
- package/dist/components/Qafka.utils.d.ts +7 -0
- package/dist/components/Qafka.utils.js +34 -0
- package/dist/components/QafkaProvider.d.ts +12 -0
- package/dist/components/QafkaProvider.js +87 -0
- package/dist/components/QuickReplies.d.ts +14 -0
- package/dist/components/QuickReplies.js +48 -0
- package/dist/components/StepProgressIndicator.d.ts +12 -0
- package/dist/components/StepProgressIndicator.js +48 -0
- package/dist/components/SuggestionButton.d.ts +42 -0
- package/dist/components/SuggestionButton.js +67 -0
- package/dist/components/ToolStatusPill.d.ts +20 -0
- package/dist/components/ToolStatusPill.js +43 -0
- package/dist/components/TypingIndicator.d.ts +28 -0
- package/dist/components/TypingIndicator.js +109 -0
- package/dist/components/VoicePage.d.ts +48 -0
- package/dist/components/VoicePage.js +683 -0
- package/dist/components/defaults/DefaultCard.d.ts +14 -0
- package/dist/components/defaults/DefaultCard.js +156 -0
- package/dist/components/defaults/DefaultDetail.d.ts +14 -0
- package/dist/components/defaults/DefaultDetail.js +138 -0
- package/dist/components/defaults/DefaultList.d.ts +12 -0
- package/dist/components/defaults/DefaultList.js +98 -0
- package/dist/components/defaults/DefaultTable.d.ts +14 -0
- package/dist/components/defaults/DefaultTable.js +204 -0
- package/dist/components/defaults/index.d.ts +14 -0
- package/dist/components/defaults/index.js +25 -0
- package/dist/components/index.d.ts +22 -0
- package/dist/components/index.js +36 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +13 -0
- package/dist/hooks/useChatMessages.d.ts +72 -0
- package/dist/hooks/useChatMessages.js +505 -0
- package/dist/hooks/useContextManager.d.ts +12 -0
- package/dist/hooks/useContextManager.js +46 -0
- package/dist/hooks/useProjectTheme.d.ts +19 -0
- package/dist/hooks/useProjectTheme.js +163 -0
- package/dist/hooks/useSDK.d.ts +31 -0
- package/dist/hooks/useSDK.js +103 -0
- package/dist/hooks/useVoiceChat.d.ts +110 -0
- package/dist/hooks/useVoiceChat.js +436 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +59 -0
- package/dist/native/QafkaAttestation.d.ts +23 -0
- package/dist/native/QafkaAttestation.js +70 -0
- package/dist/native/QafkaAudio.d.ts +14 -0
- package/dist/native/QafkaAudio.js +31 -0
- package/dist/native/QafkaClipboard.d.ts +11 -0
- package/dist/native/QafkaClipboard.js +14 -0
- package/dist/native/QafkaStorage.d.ts +15 -0
- package/dist/native/QafkaStorage.js +12 -0
- package/dist/resolve-project-config.d.ts +35 -0
- package/dist/resolve-project-config.js +41 -0
- package/dist/runtime-config-loader.d.ts +37 -0
- package/dist/runtime-config-loader.js +53 -0
- package/dist/services/AttestationManager.d.ts +38 -0
- package/dist/services/AttestationManager.js +296 -0
- package/dist/services/BackendService.d.ts +156 -0
- package/dist/services/BackendService.js +755 -0
- package/dist/services/ConversationManager.d.ts +43 -0
- package/dist/services/ConversationManager.js +96 -0
- package/dist/services/NavigationHandler.d.ts +29 -0
- package/dist/services/NavigationHandler.js +70 -0
- package/dist/services/RealtimeService.d.ts +83 -0
- package/dist/services/RealtimeService.js +203 -0
- package/dist/services/storage.d.ts +11 -0
- package/dist/services/storage.js +15 -0
- package/dist/services/storageCore.d.ts +17 -0
- package/dist/services/storageCore.js +46 -0
- package/dist/themes/dark.d.ts +5 -0
- package/dist/themes/dark.js +129 -0
- package/dist/themes/index.d.ts +12 -0
- package/dist/themes/index.js +33 -0
- package/dist/themes/light.d.ts +5 -0
- package/dist/themes/light.js +129 -0
- package/dist/themes/types.d.ts +155 -0
- package/dist/themes/types.js +5 -0
- package/dist/types/chat.d.ts +126 -0
- package/dist/types/chat.js +5 -0
- package/dist/types/components.d.ts +56 -0
- package/dist/types/components.js +16 -0
- package/dist/types/external-navigation.d.ts +19 -0
- package/dist/types/external-navigation.js +8 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.js +25 -0
- package/dist/types/navigation.d.ts +86 -0
- package/dist/types/navigation.js +5 -0
- package/dist/types/sdk.d.ts +36 -0
- package/dist/types/sdk.js +5 -0
- package/dist/utils/deepMerge.d.ts +46 -0
- package/dist/utils/deepMerge.js +70 -0
- package/dist/utils/fontUtils.d.ts +8 -0
- package/dist/utils/fontUtils.js +16 -0
- package/dist/validate-end-user.d.ts +18 -0
- package/dist/validate-end-user.js +74 -0
- package/expo-plugin/withQafkaAttestation.js +57 -0
- package/ios/QafkaAttestation.m +25 -0
- package/ios/QafkaAttestation.swift +128 -0
- package/ios/QafkaAudio.m +23 -0
- package/ios/QafkaAudio.swift +519 -0
- package/ios/QafkaClipboard.m +10 -0
- package/ios/QafkaClipboard.swift +21 -0
- package/ios/QafkaReactImports.h +2 -0
- package/ios/QafkaStorage.m +26 -0
- package/ios/QafkaStorage.swift +118 -0
- package/package.json +82 -0
- package/qafka.config.d.ts +9 -0
- package/qafka.config.js +9 -0
- package/react-native-qafka.podspec +28 -0
- 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;
|