@antzsoft/chat-core 1.1.7 → 1.1.9
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/README.md +13 -0
- package/dist/{chunk-XSGY3VBK.js → chunk-P7VAN6NA.js} +56 -17
- package/dist/chunk-P7VAN6NA.js.map +1 -0
- package/dist/index.cjs +101 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +47 -20
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.js +1 -1
- package/docs/integration-guide.html +138 -5
- package/package.json +4 -1
- package/dist/chunk-XSGY3VBK.js.map +0 -1
package/README.md
CHANGED
|
@@ -2451,6 +2451,19 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
|
|
|
2451
2451
|
|
|
2452
2452
|
## Changelog
|
|
2453
2453
|
|
|
2454
|
+
### v1.1.9
|
|
2455
|
+
- **Fix: Transit 403 on `GET /users/me` and `POST /users/me/devices` in external / authToken mode** — In external and authToken auth modes, `AntzChatProvider` calls `GET /users/me` immediately on mount to refresh the user profile, and push token registration (`POST /users/me/devices`) fires on auth state change — both before the socket connects and the ECDH transit session is established. With transit enforcement enabled these requests were receiving `403 Transit encryption required`. Both endpoints are now marked `@PreTransit()` on the server so they are reachable during the bootstrap window. **No integration changes required.**
|
|
2456
|
+
- **Fix: React Native / Hermes support for transit encryption** — The core SDK (`@antzsoft/chat-core`) now works on React Native and Hermes out of the box. When `globalThis.crypto.subtle` is unavailable (Hermes does not expose it), the SDK automatically falls back to `@noble/curves` (X25519 ECDH) and `@noble/ciphers` (AES-256-GCM) for both the handshake and all payload encryption/decryption. Previously the SDK crashed with `TypeError: Cannot read property 'subtle' of undefined` on RN. **No integration changes required — no extra packages to install.**
|
|
2457
|
+
|
|
2458
|
+
### v1.1.8
|
|
2459
|
+
- **Fix: Socket connect_error "User shadow record not found" in external / authToken auth modes** — The socket gateway was looking up the shadow user record but rejecting the connection if it didn't exist yet, requiring a prior HTTP call to create it. The gateway now creates the shadow record on first connect (via gRPC, same as the HTTP JWT guard) when it isn't found — no prior HTTP call needed. The connection bootstraps cleanly on first connect in all auth modes. **No integration changes required.**
|
|
2460
|
+
- **Fix: Transit encryption enforced consistently across HTTP and WebSocket** — When `TRANSIT_ENCRYPTION_ENABLED=true`, the server now enforces transit on both HTTP and WebSocket uniformly. A `@PreTransit()` decorator marks the four genuinely pre-session endpoints (`/crypto/pubkey`, `/auth/login`, `/auth/register`, `/auth/refresh`) — these are exempt because no session key can exist before the ECDH handshake. All other endpoints require a transit session. **No integration changes required.**
|
|
2461
|
+
- **Fix: SDK transit wait no longer blocks unauthenticated requests** — `waitForTransitReady()` in the axios interceptor now only applies to authenticated requests (requests with a token). Unauthenticated requests — login, register, pubkey — fire immediately without waiting for the ECDH handshake. This eliminates the deadlock in builtin auth mode where login was blocking forever waiting for a session that required login to exist first. **No integration changes required.**
|
|
2462
|
+
|
|
2463
|
+
### v1.1.7
|
|
2464
|
+
- **Fix: Transit encryption — `POST /auth/login` and socket connect blocked forever when `transitEncryption: true`** — The axios request interceptor called `waitForTransitReady()` on every outgoing HTTP request. This promise only resolves after the ECDH handshake completes, which itself requires a socket connection, which requires login first — a deadlock. Auth endpoints (`/auth/login`, `/auth/register`, `/auth/refresh`) and the ECDH bootstrap endpoint (`/crypto/pubkey`) are now exempt from the transit wait and encryption, so login always fires immediately regardless of transit config. **No integration changes required.**
|
|
2465
|
+
- **Fix: Misleading `connect_error` when transit handshake fails** — When `transitEncryption: true` and `fetchServerKeys()` or `performHandshake()` threw any error (e.g. wrong `apiUrl`, CORS, network failure), the SDK silently swallowed the error and attempted to connect the socket without the required `transitEphemeralPub`/`transitAlgo` handshake fields. The server then rejected the socket with a confusing "missing transitEphemeralPub" error that obscured the real cause. The SDK now re-throws immediately so the actual error (CORS, wrong API URL, network failure) is surfaced to the caller. **No integration changes required.**
|
|
2466
|
+
|
|
2454
2467
|
### v1.1.6
|
|
2455
2468
|
- **Fix: Messages arrive out of order when typing and sending rapidly** — When a user sent 10–15+ messages in quick succession, the SDK fired all `send_message` socket emits concurrently. Each emit raced independently against the 5-second ACK timeout, and the server received them in an unpredictable order, causing messages to appear jumbled in the recipient's view. The SDK now serialises sends through a per-conversation FIFO queue — the next message is only emitted after the previous ACK is received, ensuring the server always processes them in the order the user sent them. Queue limits: max 100 pending entries (overflow rejects immediately) and a 30-second per-entry TTL (stale entries are dropped before reaching the socket). **No integration changes required.**
|
|
2456
2469
|
- **New: POST mirrors for all PUT and DELETE endpoints** — Every `PUT` and `DELETE` route on the server now has an equivalent `POST` endpoint running alongside it. The SDK calls the POST mirrors exclusively. This allows the SDK to work through black-box proxies and infrastructure that blocks non-POST/GET methods (a common requirement in enterprise and carrier-grade deployments). Original PUT/DELETE routes remain fully operational for direct REST clients and legacy integrations — nothing is removed. The complete mirror map is documented in `POST-MIRRORS.md` in the server repo. **No integration changes required.**
|
|
@@ -122,24 +122,40 @@ function getSessionId() {
|
|
|
122
122
|
import axios from "axios";
|
|
123
123
|
|
|
124
124
|
// src/crypto/transit.ts
|
|
125
|
+
function hasWebCrypto() {
|
|
126
|
+
return typeof globalThis.crypto?.subtle !== "undefined";
|
|
127
|
+
}
|
|
125
128
|
async function encryptPayload(data, sessionKey) {
|
|
126
|
-
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
127
129
|
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
if (!hasWebCrypto() || sessionKey instanceof Uint8Array) {
|
|
131
|
+
return encryptNoble(plaintext, sessionKey);
|
|
132
|
+
}
|
|
133
|
+
return encryptWebCrypto(plaintext, sessionKey);
|
|
134
|
+
}
|
|
135
|
+
async function encryptWebCrypto(plaintext, sessionKey) {
|
|
136
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
137
|
+
const encrypted = await globalThis.crypto.subtle.encrypt({ name: "AES-GCM", iv }, sessionKey, plaintext);
|
|
133
138
|
const ct = encrypted.slice(0, encrypted.byteLength - 16);
|
|
134
139
|
const tag = encrypted.slice(encrypted.byteLength - 16);
|
|
135
|
-
return {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
return { v: 1, iv: bufToB64(iv), tag: bufToB64(tag), ct: bufToB64(ct) };
|
|
141
|
+
}
|
|
142
|
+
async function encryptNoble(plaintext, sessionKey) {
|
|
143
|
+
const { gcm } = await import("@noble/ciphers/aes");
|
|
144
|
+
const { randomBytes } = await import("@noble/hashes/utils");
|
|
145
|
+
const iv = randomBytes(12);
|
|
146
|
+
const cipher = gcm(sessionKey, iv);
|
|
147
|
+
const encrypted = cipher.encrypt(plaintext);
|
|
148
|
+
const ct = encrypted.slice(0, encrypted.length - 16);
|
|
149
|
+
const tag = encrypted.slice(encrypted.length - 16);
|
|
150
|
+
return { v: 1, iv: uint8ToB64(iv), tag: uint8ToB64(tag), ct: uint8ToB64(ct) };
|
|
141
151
|
}
|
|
142
152
|
async function decryptPayload(envelope, sessionKey) {
|
|
153
|
+
if (!hasWebCrypto() || sessionKey instanceof Uint8Array) {
|
|
154
|
+
return decryptNoble(envelope, sessionKey);
|
|
155
|
+
}
|
|
156
|
+
return decryptWebCrypto(envelope, sessionKey);
|
|
157
|
+
}
|
|
158
|
+
async function decryptWebCrypto(envelope, sessionKey) {
|
|
143
159
|
const iv = b64ToBuf(envelope.iv);
|
|
144
160
|
const tag = b64ToBuf(envelope.tag);
|
|
145
161
|
const ct = b64ToBuf(envelope.ct);
|
|
@@ -153,6 +169,18 @@ async function decryptPayload(envelope, sessionKey) {
|
|
|
153
169
|
);
|
|
154
170
|
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
155
171
|
}
|
|
172
|
+
async function decryptNoble(envelope, sessionKey) {
|
|
173
|
+
const { gcm } = await import("@noble/ciphers/aes");
|
|
174
|
+
const iv = base64ToUint8(envelope.iv);
|
|
175
|
+
const tag = base64ToUint8(envelope.tag);
|
|
176
|
+
const ct = base64ToUint8(envelope.ct);
|
|
177
|
+
const combined = new Uint8Array(ct.length + tag.length);
|
|
178
|
+
combined.set(ct, 0);
|
|
179
|
+
combined.set(tag, ct.length);
|
|
180
|
+
const cipher = gcm(sessionKey, iv);
|
|
181
|
+
const decrypted = cipher.decrypt(combined);
|
|
182
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
183
|
+
}
|
|
156
184
|
function isTransitEnvelope(v) {
|
|
157
185
|
return typeof v === "object" && v !== null && v.v === 1 && typeof v.iv === "string" && typeof v.tag === "string" && typeof v.ct === "string";
|
|
158
186
|
}
|
|
@@ -164,12 +192,25 @@ function bufToB64(buf) {
|
|
|
164
192
|
});
|
|
165
193
|
return btoa(str);
|
|
166
194
|
}
|
|
195
|
+
function uint8ToB64(bytes) {
|
|
196
|
+
let str = "";
|
|
197
|
+
bytes.forEach((b) => {
|
|
198
|
+
str += String.fromCharCode(b);
|
|
199
|
+
});
|
|
200
|
+
return btoa(str);
|
|
201
|
+
}
|
|
167
202
|
function b64ToBuf(b64) {
|
|
168
203
|
const bin = atob(b64);
|
|
169
204
|
const buf = new Uint8Array(bin.length);
|
|
170
205
|
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
171
206
|
return buf.buffer;
|
|
172
207
|
}
|
|
208
|
+
function base64ToUint8(b64) {
|
|
209
|
+
const bin = atob(b64);
|
|
210
|
+
const buf = new Uint8Array(bin.length);
|
|
211
|
+
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
212
|
+
return buf;
|
|
213
|
+
}
|
|
173
214
|
|
|
174
215
|
// src/api/client.ts
|
|
175
216
|
var _tokenStore = null;
|
|
@@ -194,10 +235,8 @@ function initApiClient(config, tokenStore) {
|
|
|
194
235
|
else if (_config.avatar.url) req.headers["x-avatar-url"] = _config.avatar.url;
|
|
195
236
|
_avatarSent = true;
|
|
196
237
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (!isExempt) await waitForTransitReady();
|
|
200
|
-
if (!isExempt && isTransitEnabled()) {
|
|
238
|
+
if (token) await waitForTransitReady();
|
|
239
|
+
if (isTransitEnabled()) {
|
|
201
240
|
const sessionId = getSessionId();
|
|
202
241
|
const key = getSessionKey();
|
|
203
242
|
if (sessionId && key) {
|
|
@@ -428,4 +467,4 @@ export {
|
|
|
428
467
|
uploadBatch,
|
|
429
468
|
uploadBatchWithSlots
|
|
430
469
|
};
|
|
431
|
-
//# sourceMappingURL=chunk-
|
|
470
|
+
//# sourceMappingURL=chunk-P7VAN6NA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/compression/compress.ts","../src/crypto/session.ts","../src/api/client.ts","../src/crypto/transit.ts","../src/crypto/uuid.ts","../src/api/storage.ts"],"sourcesContent":["import type { UploadableFile, CompressedFile, CompressionAlgorithm } from '../types/index.js';\nimport type { PlatformCompressFn, ResolvedCompressionConfig } from '../config/types.js';\n\n// MIME types that benefit from gzip (text-based, not already compressed)\nconst GZIP_MIME_TYPES = new Set([\n 'text/plain', 'text/csv', 'text/markdown', 'text/x-markdown',\n 'text/xml', 'application/xml', 'text/yaml', 'text/x-yaml',\n 'application/x-yaml', 'application/rtf', 'text/rtf',\n 'application/json', 'image/svg+xml',\n]);\n\nconst IMAGE_MIME_TYPES = new Set([\n 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'image/tiff',\n]);\n\n// Already-compressed formats — no gain from recompressing\nconst SKIP_MIME_TYPES = new Set([\n 'video/mp4', 'video/webm', 'video/quicktime',\n 'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm', 'audio/mp4',\n 'application/zip', 'application/pdf',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n]);\n\nexport type CompressionStrategy = 'image' | 'gzip' | 'skip';\n\nexport function getCompressionStrategy(\n mimeType: string,\n config: ResolvedCompressionConfig,\n): CompressionStrategy {\n if (SKIP_MIME_TYPES.has(mimeType)) return 'skip';\n if (IMAGE_MIME_TYPES.has(mimeType)) return 'image';\n if (config.compressDocuments && GZIP_MIME_TYPES.has(mimeType)) return 'gzip';\n return 'skip';\n}\n\n/**\n * Attempt to compress a file using the platform-provided compressor.\n * Returns the original file unchanged (as a CompressedFile with compressed=false)\n * if compression is disabled, no compressor is provided, or the strategy is 'skip'.\n */\nexport async function compressFile(\n file: UploadableFile,\n platformCompressFn: PlatformCompressFn | undefined,\n config: ResolvedCompressionConfig,\n): Promise<CompressedFile> {\n const noop: CompressedFile = {\n ...file,\n originalSize: file.size,\n compressed: false,\n compressionAlgorithm: 'none' as CompressionAlgorithm,\n };\n\n if (!config.enabled || !platformCompressFn) return noop;\n\n const strategy = getCompressionStrategy(file.type, config);\n if (strategy === 'skip') return noop;\n\n try {\n return await platformCompressFn(file, config);\n } catch {\n // Compression failure is non-fatal — fall back to original\n return noop;\n }\n}\n","import type { TransitAlgo } from './detect.js';\n\ninterface TransitSession {\n sessionKey: CryptoKey | Uint8Array; // CryptoKey on Web Crypto, Uint8Array on noble/RN\n algo: TransitAlgo;\n sessionId: string;\n enabled: boolean;\n}\n\n// All state lives on globalThis so Turbopack <locals> module splits and any\n// other bundler that creates multiple instances of this module still share a\n// single source of truth. The symbol key prevents accidental collisions.\nconst _KEY = Symbol.for('__antz_chat_transit__');\n\ninterface TransitState {\n session: TransitSession | null;\n sessionEverEstablished: boolean;\n readyResolve: (() => void) | null;\n readyPromise: Promise<void> | null;\n transitConfigured: boolean | null;\n}\n\nfunction getState(): TransitState {\n const g = globalThis as any;\n if (!g[_KEY]) {\n g[_KEY] = {\n session: null,\n sessionEverEstablished: false,\n readyResolve: null,\n readyPromise: null,\n transitConfigured: null,\n } satisfies TransitState;\n }\n return g[_KEY] as TransitState;\n}\n\nexport function configureTransit(enabled: boolean): void {\n const s = getState();\n s.transitConfigured = enabled;\n if (!enabled) {\n // Transit disabled — resolve immediately so HTTP requests don't block\n s.readyResolve?.();\n s.readyResolve = null;\n }\n}\n\nexport function waitForTransitReady(): Promise<void> {\n const s = getState();\n // Not configured yet or disabled — resolve immediately\n if (!s.transitConfigured) return Promise.resolve();\n // Already have an active session — resolve immediately\n if (s.session) return Promise.resolve();\n // Session was previously established but is now cleared (socket dropped/reconnecting).\n // The encryption block in the interceptor is guarded by isTransitEnabled() and will\n // skip itself with no session, so there is no point blocking here.\n if (s.sessionEverEstablished) return Promise.resolve();\n // First startup — block until the initial ECDH handshake completes.\n if (!s.readyPromise) {\n s.readyPromise = new Promise<void>((resolve) => {\n s.readyResolve = resolve;\n });\n }\n return s.readyPromise;\n}\n\nexport function setTransitSession(session: TransitSession): void {\n const s = getState();\n s.session = session;\n s.sessionEverEstablished = true;\n // Resolve any pending HTTP requests waiting for the session key\n s.readyResolve?.();\n s.readyResolve = null;\n}\n\nexport function getTransitSession(): TransitSession | null {\n return getState().session;\n}\n\nexport function clearTransitSession(): void {\n const s = getState();\n s.session = null;\n // Reset the ready promise so it can be recreated on next waitForTransitReady call.\n // sessionEverEstablished intentionally left true — HTTP requests fired while\n // reconnecting should not block (isTransitEnabled() guards encryption anyway).\n s.readyPromise = null;\n s.readyResolve = null;\n}\n\nexport function isTransitEnabled(): boolean {\n return getState().session?.enabled === true;\n}\n\nexport function getSessionKey(): CryptoKey | Uint8Array | null {\n return getState().session?.sessionKey ?? null;\n}\n\n// Returns sessionId for the x-transit-session header sent with HTTP requests.\nexport function getSessionId(): string | null {\n return getState().session?.sessionId ?? null;\n}\n","import axios, {\n AxiosInstance,\n InternalAxiosRequestConfig,\n} from 'axios';\nimport type { ResolvedConfig } from '../config/types.js';\nimport type { AuthTokens } from '../types/index.js';\nimport { encryptPayload, decryptPayload, isTransitEnvelope } from '../crypto/transit.js';\nimport { getSessionKey, getSessionId, isTransitEnabled, waitForTransitReady, configureTransit } from '../crypto/session.js';\n\nexport type TokenStore = {\n getAccessToken: () => string | null | undefined;\n getRefreshToken: () => string | null | undefined;\n setTokens: (tokens: AuthTokens) => void;\n clearTokens: () => void;\n};\n\nlet _tokenStore: TokenStore | null = null;\nlet _config: ResolvedConfig | null = null;\n// Avatar headers are sent only on the first authenticated request after init.\n// The server hashes on receive and deduplicates — subsequent requests don't need them.\nlet _avatarSent = false;\n\nexport function initApiClient(config: ResolvedConfig, tokenStore: TokenStore): AxiosInstance {\n _config = config;\n _tokenStore = tokenStore;\n _avatarSent = false; // reset on re-init (new session / authToken change)\n\n const client = axios.create({\n baseURL: config.apiUrl,\n headers: { 'Content-Type': 'application/json' },\n });\n\n // Configure transit as early as possible — before any requests fire —\n // so waitForTransitReady() in the interceptor knows whether to block or not.\n configureTransit(config.transitEncryption);\n\n // ── Request interceptor ──────────────────────────────────────────────────\n client.interceptors.request.use(async (req: InternalAxiosRequestConfig) => {\n const token = _tokenStore?.getAccessToken();\n if (token) req.headers['Authorization'] = `Bearer ${token}`;\n if (_config?.userId) req.headers['x-user-id'] = _config.userId;\n if (_config?.tenantId) req.headers['X-Tenant-ID'] = _config.tenantId;\n // Send avatar on the first request only — server hashes and deduplicates\n if (token && !_avatarSent && _config?.avatar) {\n if (_config.avatar.base64) req.headers['x-avatar-base64'] = _config.avatar.base64;\n else if (_config.avatar.url) req.headers['x-avatar-url'] = _config.avatar.url;\n _avatarSent = true;\n }\n\n // Wait for the transit session key before sending authenticated requests.\n // Unauthenticated requests (no token) fire before login/socket connect —\n // they can never have a transit session so never block.\n if (token) await waitForTransitReady();\n\n if (isTransitEnabled()) {\n const sessionId = getSessionId();\n const key = getSessionKey();\n if (sessionId && key) {\n req.headers['x-transit-session'] = sessionId;\n if (req.data !== undefined && req.data !== null) {\n const envelope = await encryptPayload(req.data, key);\n req.data = envelope;\n req.headers['x-transit-encrypted'] = '1';\n }\n }\n }\n\n return req;\n });\n\n let isRefreshing = false;\n let refreshQueue: Array<(token: string) => void> = [];\n\n // ── Response interceptor ─────────────────────────────────────────────────\n client.interceptors.response.use(\n async (response) => {\n // Transit decryption — server wraps encrypted payload inside { success, data: <envelope> }.\n // Decrypt the inner envelope, then let the standard unwrap below handle { success, data }.\n if (isTransitEnabled()) {\n const key = getSessionKey();\n if (key) {\n // Case 1: entire response is the envelope (unlikely but handle it)\n if (isTransitEnvelope(response.data)) {\n response.data = await decryptPayload(response.data, key);\n }\n // Case 2: envelope is nested inside { success, data: <envelope> }\n else if (response.data?.data && isTransitEnvelope(response.data.data)) {\n response.data.data = await decryptPayload(response.data.data, key);\n }\n }\n }\n\n // Standard { success, data } unwrap\n if (\n response.data &&\n typeof response.data === 'object' &&\n 'success' in response.data &&\n 'data' in response.data\n ) {\n response.data = response.data.data;\n }\n return response;\n },\n async (error) => {\n // Decrypt error response body — error filter encrypts it too\n if (isTransitEnabled() && error.response?.data) {\n const key = getSessionKey();\n if (key) {\n try {\n if (isTransitEnvelope(error.response.data)) {\n error.response.data = await decryptPayload(error.response.data, key);\n } else if (error.response.data?.data && isTransitEnvelope(error.response.data.data)) {\n error.response.data.data = await decryptPayload(error.response.data.data, key);\n }\n } catch { /* decryption failed — leave as-is */ }\n }\n }\n\n const original = error.config as InternalAxiosRequestConfig & { _retry?: boolean };\n\n if (error.response?.status === 401 && !original._retry) {\n const refreshToken = _tokenStore?.getRefreshToken();\n if (!refreshToken) {\n _tokenStore?.clearTokens();\n return Promise.reject(error);\n }\n\n if (isRefreshing) {\n return new Promise((resolve) => {\n refreshQueue.push((newToken) => {\n original.headers['Authorization'] = `Bearer ${newToken}`;\n resolve(client(original));\n });\n });\n }\n\n original._retry = true;\n isRefreshing = true;\n\n try {\n const { data } = await axios.post<{ data: AuthTokens }>(\n `${_config!.apiUrl}/auth/refresh`,\n { refreshToken },\n );\n const tokens: AuthTokens = (data as any).data ?? data;\n _tokenStore?.setTokens(tokens);\n refreshQueue.forEach((cb) => cb(tokens.accessToken));\n refreshQueue = [];\n original.headers['Authorization'] = `Bearer ${tokens.accessToken}`;\n return client(original);\n } catch {\n _tokenStore?.clearTokens();\n return Promise.reject(error);\n } finally {\n isRefreshing = false;\n }\n }\n\n return Promise.reject(error);\n },\n );\n\n return client;\n}\n\nlet _instance: AxiosInstance | null = null;\n\nexport function setApiClientInstance(instance: AxiosInstance) {\n _instance = instance;\n}\n\nexport function getApiClient(): AxiosInstance {\n if (!_instance) throw new Error('[AntzChat] API client not initialized. Call initApiClient first.');\n return _instance;\n}\n","export interface TransitEnvelope {\n v: 1;\n iv: string; // base64, 12 bytes\n tag: string; // base64, 16 bytes\n ct: string; // base64, ciphertext\n}\n\n// sessionKey is CryptoKey on Web Crypto path, Uint8Array on noble/RN path.\ntype AnySessionKey = CryptoKey | Uint8Array;\n\nfunction hasWebCrypto(): boolean {\n return typeof globalThis.crypto?.subtle !== 'undefined';\n}\n\n// ─── Encrypt ─────────────────────────────────────────────────────────────────\n\nexport async function encryptPayload(\n data: unknown,\n sessionKey: AnySessionKey,\n): Promise<TransitEnvelope> {\n const plaintext = new TextEncoder().encode(JSON.stringify(data));\n\n if (!hasWebCrypto() || sessionKey instanceof Uint8Array) {\n return encryptNoble(plaintext, sessionKey as Uint8Array);\n }\n return encryptWebCrypto(plaintext, sessionKey as CryptoKey);\n}\n\nasync function encryptWebCrypto(plaintext: Uint8Array, sessionKey: CryptoKey): Promise<TransitEnvelope> {\n const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));\n const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv as Uint8Array<ArrayBuffer> }, sessionKey, plaintext as Uint8Array<ArrayBuffer>);\n const ct = encrypted.slice(0, encrypted.byteLength - 16);\n const tag = encrypted.slice(encrypted.byteLength - 16);\n return { v: 1, iv: bufToB64(iv), tag: bufToB64(tag), ct: bufToB64(ct) };\n}\n\nasync function encryptNoble(plaintext: Uint8Array, sessionKey: Uint8Array): Promise<TransitEnvelope> {\n const { gcm } = await import('@noble/ciphers/aes');\n const { randomBytes } = await import('@noble/hashes/utils');\n const iv = randomBytes(12);\n const cipher = gcm(sessionKey, iv);\n const encrypted = cipher.encrypt(plaintext); // ct + 16-byte tag appended\n const ct = encrypted.slice(0, encrypted.length - 16);\n const tag = encrypted.slice(encrypted.length - 16);\n return { v: 1, iv: uint8ToB64(iv), tag: uint8ToB64(tag), ct: uint8ToB64(ct) };\n}\n\n// ─── Decrypt ─────────────────────────────────────────────────────────────────\n\nexport async function decryptPayload(\n envelope: TransitEnvelope,\n sessionKey: AnySessionKey,\n): Promise<unknown> {\n if (!hasWebCrypto() || sessionKey instanceof Uint8Array) {\n return decryptNoble(envelope, sessionKey as Uint8Array);\n }\n return decryptWebCrypto(envelope, sessionKey as CryptoKey);\n}\n\nasync function decryptWebCrypto(envelope: TransitEnvelope, sessionKey: CryptoKey): Promise<unknown> {\n const iv = b64ToBuf(envelope.iv);\n const tag = b64ToBuf(envelope.tag);\n const ct = b64ToBuf(envelope.ct);\n const combined = new Uint8Array(ct.byteLength + tag.byteLength);\n combined.set(new Uint8Array(ct), 0);\n combined.set(new Uint8Array(tag), ct.byteLength);\n const decrypted = await globalThis.crypto.subtle.decrypt(\n { name: 'AES-GCM', iv: new Uint8Array(iv) },\n sessionKey,\n combined,\n );\n return JSON.parse(new TextDecoder().decode(decrypted));\n}\n\nasync function decryptNoble(envelope: TransitEnvelope, sessionKey: Uint8Array): Promise<unknown> {\n const { gcm } = await import('@noble/ciphers/aes');\n const iv = base64ToUint8(envelope.iv);\n const tag = base64ToUint8(envelope.tag);\n const ct = base64ToUint8(envelope.ct);\n const combined = new Uint8Array(ct.length + tag.length);\n combined.set(ct, 0);\n combined.set(tag, ct.length);\n const cipher = gcm(sessionKey, iv);\n const decrypted = cipher.decrypt(combined);\n return JSON.parse(new TextDecoder().decode(decrypted));\n}\n\nexport function isTransitEnvelope(v: unknown): v is TransitEnvelope {\n return (\n typeof v === 'object' &&\n v !== null &&\n (v as any).v === 1 &&\n typeof (v as any).iv === 'string' &&\n typeof (v as any).tag === 'string' &&\n typeof (v as any).ct === 'string'\n );\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nfunction bufToB64(buf: ArrayBuffer | Uint8Array): string {\n const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);\n let str = '';\n bytes.forEach(b => { str += String.fromCharCode(b); });\n return btoa(str);\n}\n\nfunction uint8ToB64(bytes: Uint8Array): string {\n let str = '';\n bytes.forEach(b => { str += String.fromCharCode(b); });\n return btoa(str);\n}\n\nfunction b64ToBuf(b64: string): ArrayBuffer {\n const bin = atob(b64);\n const buf = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);\n return buf.buffer;\n}\n\nfunction base64ToUint8(b64: string): Uint8Array {\n const bin = atob(b64);\n const buf = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);\n return buf;\n}\n","// crypto.randomUUID() doesn't exist in React Native's Hermes engine.\n// Fall back to a RFC 4122 v4 UUID built from Math.random() when unavailable.\nexport function generateUUID(): string {\n if (typeof globalThis.crypto?.randomUUID === 'function') {\n return globalThis.crypto.randomUUID();\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n","import type {\n BatchUploadResult,\n FileResponse,\n PaginatedResponse,\n PresignedUrlRequest,\n PresignedUrlResponse,\n FileType,\n UploadableFile,\n} from '../types/index.js';\nimport type { PlatformUploadFn, PlatformCompressFn, ResolvedCompressionConfig } from '../config/types.js';\nimport { compressFile } from '../compression/compress.js';\nimport { getApiClient } from './client.js';\nimport { generateUUID } from '../crypto/uuid.js';\n\nexport const storageApi = {\n async requestPresignedUrl(payload: PresignedUrlRequest): Promise<PresignedUrlResponse> {\n const { data } = await getApiClient().post<PresignedUrlResponse>('/storage/presigned-url', payload);\n return data;\n },\n\n async requestPresignedUrlBatch(files: PresignedUrlRequest[]): Promise<{\n urls: PresignedUrlResponse[];\n errors: Array<{ filename: string; error: string; clientIndex?: number }>;\n }> {\n const { data } = await getApiClient().post('/storage/presigned-url/batch', { files });\n return data;\n },\n\n async confirmUpload(fileId: string): Promise<FileResponse> {\n const { data } = await getApiClient().post<FileResponse>(`/storage/confirm/${fileId}`);\n return data;\n },\n\n async getFile(fileId: string): Promise<FileResponse> {\n const { data } = await getApiClient().get<FileResponse>(`/storage/files/${fileId}`);\n return data;\n },\n\n async getFileUrl(fileId: string, expiresIn?: number): Promise<{ url: string; expiresAt: string }> {\n const { data } = await getApiClient().get(`/storage/files/${fileId}/url`, {\n params: expiresIn ? { expiresIn } : {},\n });\n return data;\n },\n\n async deleteFile(fileId: string): Promise<void> {\n await getApiClient().post(`/storage/files/${fileId}/delete`);\n },\n\n async getConversationFiles(\n conversationId: string,\n params: { page?: number; limit?: number; type?: FileType } = {},\n ): Promise<PaginatedResponse<FileResponse>> {\n const { data } = await getApiClient().get(\n `/storage/conversations/${conversationId}/files`,\n { params },\n );\n return data;\n },\n\n async getMyFiles(params: { page?: number; limit?: number } = {}): Promise<PaginatedResponse<FileResponse>> {\n const { data } = await getApiClient().get('/storage/my-files', { params });\n return data;\n },\n};\n\n/**\n * High-level batch upload.\n * The actual binary transfer is delegated to platformUploadFn so this\n * function is platform-agnostic (works on web and React Native).\n * If platformCompressFn + compressionConfig are provided, each file is\n * compressed before the presigned URL is requested (so the server receives\n * the correct compressed size and MIME type).\n */\n/**\n * Core upload implementation. Returns the public BatchUploadResult plus a\n * slotId → FileResponse map that useChat hooks use internally to match\n * confirmed uploads back to optimistic UI slots by position rather than\n * filename. The slotToFile map is never part of the public API.\n */\nasync function runUploadBatch(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n slotIds: string[],\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n): Promise<{ result: BatchUploadResult; slotToFile: Map<string, FileResponse> }> {\n // Compress all files first (no-ops for unsupported types or when disabled)\n const compressedFiles = await Promise.all(\n files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true })),\n );\n\n // Pair each compressed file with its slot ID and a clientIndex.\n // clientIndex is sent to the server and echoed back in both urls and errors,\n // giving us a reliable position mapping regardless of which files fail.\n const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i], clientIndex: i }));\n\n const requests: PresignedUrlRequest[] = slotted.map(({ file: f, clientIndex }) => ({\n filename: f.name,\n mimeType: f.type,\n size: f.size,\n conversationId,\n clientIndex,\n ...(f.compressed && {\n metadata: {\n compressed: f.compressed,\n originalSize: f.originalSize,\n compressionAlgorithm: f.compressionAlgorithm,\n },\n }),\n }));\n\n const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);\n\n // Use the echoed clientIndex to identify which original slots failed.\n // This is reliable even for same-named files and any failure pattern.\n const failedSlotIds = new Set<string>();\n const failed: Array<{ filename: string; error: string }> = requestErrors.map((e) => {\n const idx = e.clientIndex ?? slotted.findIndex((s) => s.file.name === e.filename);\n const slotId = slotted[idx]?.slotId;\n if (slotId) failedSlotIds.add(slotId);\n return { filename: e.filename, error: e.error };\n });\n\n // Map each presigned URL back to its original slot via clientIndex.\n const progressMap: Record<number, number> = {};\n const reportProgress = () => {\n if (!onProgress) return;\n const vals = Object.values(progressMap);\n const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);\n onProgress(Math.round(avg));\n };\n\n const successful: FileResponse[] = [];\n const slotToFile = new Map<string, FileResponse>();\n\n await Promise.all(\n urls.map(async (presigned, idx) => {\n // Resolve the original slot via echoed clientIndex; fall back to position\n // in urls[] only if the server didn't echo it (older server version).\n const originalIdx = presigned.clientIndex ?? idx;\n const { file, slotId } = slotted[originalIdx];\n progressMap[originalIdx] = 0;\n try {\n await platformUploadFn(presigned, file, (pct) => {\n progressMap[originalIdx] = Math.round(pct * 0.9);\n reportProgress();\n });\n\n const fileResponse = await storageApi.confirmUpload(presigned.fileId);\n progressMap[originalIdx] = 100;\n reportProgress();\n successful.push(fileResponse);\n slotToFile.set(slotId, fileResponse);\n } catch (err) {\n failed.push({ filename: file.name, error: (err as Error).message });\n }\n }),\n );\n\n return { result: { successful, failed }, slotToFile };\n}\n\n/** Public API — returns standard BatchUploadResult, slot tracking is internal. */\nexport async function uploadBatch(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n): Promise<BatchUploadResult> {\n const slotIds = files.map(() => generateUUID());\n const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig);\n return result;\n}\n\n/**\n * Used only by useChat hooks (web + RN) to get the slotId → FileResponse map\n * for matching confirmed uploads back to optimistic UI slots.\n * Not exported from the package index — internal SDK use only.\n */\nexport async function uploadBatchWithSlots(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n slotIds: string[],\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n): Promise<{ result: BatchUploadResult; slotToFile: Map<string, FileResponse> }> {\n return runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig);\n}\n"],"mappings":";AAIA,IAAM,kBAAkB,oBAAI,IAAI;AAAA,EAC9B;AAAA,EAAc;AAAA,EAAY;AAAA,EAAiB;AAAA,EAC3C;AAAA,EAAY;AAAA,EAAmB;AAAA,EAAa;AAAA,EAC5C;AAAA,EAAsB;AAAA,EAAmB;AAAA,EACzC;AAAA,EAAoB;AACtB,CAAC;AAED,IAAM,mBAAmB,oBAAI,IAAI;AAAA,EAC/B;AAAA,EAAc;AAAA,EAAa;AAAA,EAAa;AAAA,EAAc;AAAA,EAAa;AACrE,CAAC;AAGD,IAAM,kBAAkB,oBAAI,IAAI;AAAA,EAC9B;AAAA,EAAa;AAAA,EAAc;AAAA,EAC3B;AAAA,EAAc;AAAA,EAAa;AAAA,EAAa;AAAA,EAAc;AAAA,EACtD;AAAA,EAAmB;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAIM,SAAS,uBACd,UACA,QACqB;AACrB,MAAI,gBAAgB,IAAI,QAAQ,EAAG,QAAO;AAC1C,MAAI,iBAAiB,IAAI,QAAQ,EAAG,QAAO;AAC3C,MAAI,OAAO,qBAAqB,gBAAgB,IAAI,QAAQ,EAAG,QAAO;AACtE,SAAO;AACT;AAOA,eAAsB,aACpB,MACA,oBACA,QACyB;AACzB,QAAM,OAAuB;AAAA,IAC3B,GAAG;AAAA,IACH,cAAc,KAAK;AAAA,IACnB,YAAY;AAAA,IACZ,sBAAsB;AAAA,EACxB;AAEA,MAAI,CAAC,OAAO,WAAW,CAAC,mBAAoB,QAAO;AAEnD,QAAM,WAAW,uBAAuB,KAAK,MAAM,MAAM;AACzD,MAAI,aAAa,OAAQ,QAAO;AAEhC,MAAI;AACF,WAAO,MAAM,mBAAmB,MAAM,MAAM;AAAA,EAC9C,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;;;ACrDA,IAAM,OAAO,uBAAO,IAAI,uBAAuB;AAU/C,SAAS,WAAyB;AAChC,QAAM,IAAI;AACV,MAAI,CAAC,EAAE,IAAI,GAAG;AACZ,MAAE,IAAI,IAAI;AAAA,MACR,SAAS;AAAA,MACT,wBAAwB;AAAA,MACxB,cAAc;AAAA,MACd,cAAc;AAAA,MACd,mBAAmB;AAAA,IACrB;AAAA,EACF;AACA,SAAO,EAAE,IAAI;AACf;AAEO,SAAS,iBAAiB,SAAwB;AACvD,QAAM,IAAI,SAAS;AACnB,IAAE,oBAAoB;AACtB,MAAI,CAAC,SAAS;AAEZ,MAAE,eAAe;AACjB,MAAE,eAAe;AAAA,EACnB;AACF;AAEO,SAAS,sBAAqC;AACnD,QAAM,IAAI,SAAS;AAEnB,MAAI,CAAC,EAAE,kBAAmB,QAAO,QAAQ,QAAQ;AAEjD,MAAI,EAAE,QAAS,QAAO,QAAQ,QAAQ;AAItC,MAAI,EAAE,uBAAwB,QAAO,QAAQ,QAAQ;AAErD,MAAI,CAAC,EAAE,cAAc;AACnB,MAAE,eAAe,IAAI,QAAc,CAAC,YAAY;AAC9C,QAAE,eAAe;AAAA,IACnB,CAAC;AAAA,EACH;AACA,SAAO,EAAE;AACX;AAEO,SAAS,kBAAkB,SAA+B;AAC/D,QAAM,IAAI,SAAS;AACnB,IAAE,UAAU;AACZ,IAAE,yBAAyB;AAE3B,IAAE,eAAe;AACjB,IAAE,eAAe;AACnB;AAMO,SAAS,sBAA4B;AAC1C,QAAM,IAAI,SAAS;AACnB,IAAE,UAAU;AAIZ,IAAE,eAAe;AACjB,IAAE,eAAe;AACnB;AAEO,SAAS,mBAA4B;AAC1C,SAAO,SAAS,EAAE,SAAS,YAAY;AACzC;AAEO,SAAS,gBAA+C;AAC7D,SAAO,SAAS,EAAE,SAAS,cAAc;AAC3C;AAGO,SAAS,eAA8B;AAC5C,SAAO,SAAS,EAAE,SAAS,aAAa;AAC1C;;;ACnGA,OAAO,WAGA;;;ACOP,SAAS,eAAwB;AAC/B,SAAO,OAAO,WAAW,QAAQ,WAAW;AAC9C;AAIA,eAAsB,eACpB,MACA,YAC0B;AAC1B,QAAM,YAAY,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,IAAI,CAAC;AAE/D,MAAI,CAAC,aAAa,KAAK,sBAAsB,YAAY;AACvD,WAAO,aAAa,WAAW,UAAwB;AAAA,EACzD;AACA,SAAO,iBAAiB,WAAW,UAAuB;AAC5D;AAEA,eAAe,iBAAiB,WAAuB,YAAiD;AACtG,QAAM,KAAK,WAAW,OAAO,gBAAgB,IAAI,WAAW,EAAE,CAAC;AAC/D,QAAM,YAAY,MAAM,WAAW,OAAO,OAAO,QAAQ,EAAE,MAAM,WAAW,GAAkC,GAAG,YAAY,SAAoC;AACjK,QAAM,KAAM,UAAU,MAAM,GAAG,UAAU,aAAa,EAAE;AACxD,QAAM,MAAM,UAAU,MAAM,UAAU,aAAa,EAAE;AACrD,SAAO,EAAE,GAAG,GAAG,IAAI,SAAS,EAAE,GAAG,KAAK,SAAS,GAAG,GAAG,IAAI,SAAS,EAAE,EAAE;AACxE;AAEA,eAAe,aAAa,WAAuB,YAAkD;AACnG,QAAM,EAAE,IAAI,IAAI,MAAM,OAAO,oBAAoB;AACjD,QAAM,EAAE,YAAY,IAAI,MAAM,OAAO,qBAAqB;AAC1D,QAAM,KAAY,YAAY,EAAE;AAChC,QAAM,SAAY,IAAI,YAAY,EAAE;AACpC,QAAM,YAAY,OAAO,QAAQ,SAAS;AAC1C,QAAM,KAAM,UAAU,MAAM,GAAG,UAAU,SAAS,EAAE;AACpD,QAAM,MAAM,UAAU,MAAM,UAAU,SAAS,EAAE;AACjD,SAAO,EAAE,GAAG,GAAG,IAAI,WAAW,EAAE,GAAG,KAAK,WAAW,GAAG,GAAG,IAAI,WAAW,EAAE,EAAE;AAC9E;AAIA,eAAsB,eACpB,UACA,YACkB;AAClB,MAAI,CAAC,aAAa,KAAK,sBAAsB,YAAY;AACvD,WAAO,aAAa,UAAU,UAAwB;AAAA,EACxD;AACA,SAAO,iBAAiB,UAAU,UAAuB;AAC3D;AAEA,eAAe,iBAAiB,UAA2B,YAAyC;AAClG,QAAM,KAAM,SAAS,SAAS,EAAE;AAChC,QAAM,MAAM,SAAS,SAAS,GAAG;AACjC,QAAM,KAAM,SAAS,SAAS,EAAE;AAChC,QAAM,WAAW,IAAI,WAAW,GAAG,aAAa,IAAI,UAAU;AAC9D,WAAS,IAAI,IAAI,WAAW,EAAE,GAAG,CAAC;AAClC,WAAS,IAAI,IAAI,WAAW,GAAG,GAAG,GAAG,UAAU;AAC/C,QAAM,YAAY,MAAM,WAAW,OAAO,OAAO;AAAA,IAC/C,EAAE,MAAM,WAAW,IAAI,IAAI,WAAW,EAAE,EAAE;AAAA,IAC1C;AAAA,IACA;AAAA,EACF;AACA,SAAO,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,SAAS,CAAC;AACvD;AAEA,eAAe,aAAa,UAA2B,YAA0C;AAC/F,QAAM,EAAE,IAAI,IAAI,MAAM,OAAO,oBAAoB;AACjD,QAAM,KAAM,cAAc,SAAS,EAAE;AACrC,QAAM,MAAM,cAAc,SAAS,GAAG;AACtC,QAAM,KAAM,cAAc,SAAS,EAAE;AACrC,QAAM,WAAW,IAAI,WAAW,GAAG,SAAS,IAAI,MAAM;AACtD,WAAS,IAAI,IAAI,CAAC;AAClB,WAAS,IAAI,KAAK,GAAG,MAAM;AAC3B,QAAM,SAAY,IAAI,YAAY,EAAE;AACpC,QAAM,YAAY,OAAO,QAAQ,QAAQ;AACzC,SAAO,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,SAAS,CAAC;AACvD;AAEO,SAAS,kBAAkB,GAAkC;AAClE,SACE,OAAO,MAAM,YACb,MAAM,QACL,EAAU,MAAM,KACjB,OAAQ,EAAU,OAAO,YACzB,OAAQ,EAAU,QAAQ,YAC1B,OAAQ,EAAU,OAAO;AAE7B;AAIA,SAAS,SAAS,KAAuC;AACvD,QAAM,QAAQ,eAAe,aAAa,MAAM,IAAI,WAAW,GAAG;AAClE,MAAI,MAAM;AACV,QAAM,QAAQ,OAAK;AAAE,WAAO,OAAO,aAAa,CAAC;AAAA,EAAG,CAAC;AACrD,SAAO,KAAK,GAAG;AACjB;AAEA,SAAS,WAAW,OAA2B;AAC7C,MAAI,MAAM;AACV,QAAM,QAAQ,OAAK;AAAE,WAAO,OAAO,aAAa,CAAC;AAAA,EAAG,CAAC;AACrD,SAAO,KAAK,GAAG;AACjB;AAEA,SAAS,SAAS,KAA0B;AAC1C,QAAM,MAAM,KAAK,GAAG;AACpB,QAAM,MAAM,IAAI,WAAW,IAAI,MAAM;AACrC,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,IAAK,KAAI,CAAC,IAAI,IAAI,WAAW,CAAC;AAC9D,SAAO,IAAI;AACb;AAEA,SAAS,cAAc,KAAyB;AAC9C,QAAM,MAAM,KAAK,GAAG;AACpB,QAAM,MAAM,IAAI,WAAW,IAAI,MAAM;AACrC,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,IAAK,KAAI,CAAC,IAAI,IAAI,WAAW,CAAC;AAC9D,SAAO;AACT;;;AD7GA,IAAI,cAAiC;AACrC,IAAI,UAAiC;AAGrC,IAAI,cAAc;AAEX,SAAS,cAAc,QAAwB,YAAuC;AAC3F,YAAU;AACV,gBAAc;AACd,gBAAc;AAEd,QAAM,SAAS,MAAM,OAAO;AAAA,IAC1B,SAAS,OAAO;AAAA,IAChB,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,EAChD,CAAC;AAID,mBAAiB,OAAO,iBAAiB;AAGzC,SAAO,aAAa,QAAQ,IAAI,OAAO,QAAoC;AACzE,UAAM,QAAQ,aAAa,eAAe;AAC1C,QAAI,MAAO,KAAI,QAAQ,eAAe,IAAI,UAAU,KAAK;AACzD,QAAI,SAAS,OAAU,KAAI,QAAQ,WAAW,IAAM,QAAQ;AAC5D,QAAI,SAAS,SAAU,KAAI,QAAQ,aAAa,IAAI,QAAQ;AAE5D,QAAI,SAAS,CAAC,eAAe,SAAS,QAAQ;AAC5C,UAAI,QAAQ,OAAO,OAAQ,KAAI,QAAQ,iBAAiB,IAAI,QAAQ,OAAO;AAAA,eAClE,QAAQ,OAAO,IAAK,KAAI,QAAQ,cAAc,IAAI,QAAQ,OAAO;AAC1E,oBAAc;AAAA,IAChB;AAKA,QAAI,MAAO,OAAM,oBAAoB;AAErC,QAAI,iBAAiB,GAAG;AACtB,YAAM,YAAY,aAAa;AAC/B,YAAM,MAAM,cAAc;AAC1B,UAAI,aAAa,KAAK;AACpB,YAAI,QAAQ,mBAAmB,IAAI;AACnC,YAAI,IAAI,SAAS,UAAa,IAAI,SAAS,MAAM;AAC/C,gBAAM,WAAW,MAAM,eAAe,IAAI,MAAM,GAAG;AACnD,cAAI,OAAO;AACX,cAAI,QAAQ,qBAAqB,IAAI;AAAA,QACvC;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT,CAAC;AAED,MAAI,eAAe;AACnB,MAAI,eAA+C,CAAC;AAGpD,SAAO,aAAa,SAAS;AAAA,IAC3B,OAAO,aAAa;AAGlB,UAAI,iBAAiB,GAAG;AACtB,cAAM,MAAM,cAAc;AAC1B,YAAI,KAAK;AAEP,cAAI,kBAAkB,SAAS,IAAI,GAAG;AACpC,qBAAS,OAAO,MAAM,eAAe,SAAS,MAAM,GAAG;AAAA,UACzD,WAES,SAAS,MAAM,QAAQ,kBAAkB,SAAS,KAAK,IAAI,GAAG;AACrE,qBAAS,KAAK,OAAO,MAAM,eAAe,SAAS,KAAK,MAAM,GAAG;AAAA,UACnE;AAAA,QACF;AAAA,MACF;AAGA,UACE,SAAS,QACT,OAAO,SAAS,SAAS,YACzB,aAAa,SAAS,QACtB,UAAU,SAAS,MACnB;AACA,iBAAS,OAAO,SAAS,KAAK;AAAA,MAChC;AACA,aAAO;AAAA,IACT;AAAA,IACA,OAAO,UAAU;AAEf,UAAI,iBAAiB,KAAK,MAAM,UAAU,MAAM;AAC9C,cAAM,MAAM,cAAc;AAC1B,YAAI,KAAK;AACP,cAAI;AACF,gBAAI,kBAAkB,MAAM,SAAS,IAAI,GAAG;AAC1C,oBAAM,SAAS,OAAO,MAAM,eAAe,MAAM,SAAS,MAAM,GAAG;AAAA,YACrE,WAAW,MAAM,SAAS,MAAM,QAAQ,kBAAkB,MAAM,SAAS,KAAK,IAAI,GAAG;AACnF,oBAAM,SAAS,KAAK,OAAO,MAAM,eAAe,MAAM,SAAS,KAAK,MAAM,GAAG;AAAA,YAC/E;AAAA,UACF,QAAQ;AAAA,UAAwC;AAAA,QAClD;AAAA,MACF;AAEA,YAAM,WAAW,MAAM;AAEvB,UAAI,MAAM,UAAU,WAAW,OAAO,CAAC,SAAS,QAAQ;AACtD,cAAM,eAAe,aAAa,gBAAgB;AAClD,YAAI,CAAC,cAAc;AACjB,uBAAa,YAAY;AACzB,iBAAO,QAAQ,OAAO,KAAK;AAAA,QAC7B;AAEA,YAAI,cAAc;AAChB,iBAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,yBAAa,KAAK,CAAC,aAAa;AAC9B,uBAAS,QAAQ,eAAe,IAAI,UAAU,QAAQ;AACtD,sBAAQ,OAAO,QAAQ,CAAC;AAAA,YAC1B,CAAC;AAAA,UACH,CAAC;AAAA,QACH;AAEA,iBAAS,SAAS;AAClB,uBAAe;AAEf,YAAI;AACF,gBAAM,EAAE,KAAK,IAAI,MAAM,MAAM;AAAA,YAC3B,GAAG,QAAS,MAAM;AAAA,YAClB,EAAE,aAAa;AAAA,UACjB;AACA,gBAAM,SAAsB,KAAa,QAAQ;AACjD,uBAAa,UAAU,MAAM;AAC7B,uBAAa,QAAQ,CAAC,OAAO,GAAG,OAAO,WAAW,CAAC;AACnD,yBAAe,CAAC;AAChB,mBAAS,QAAQ,eAAe,IAAI,UAAU,OAAO,WAAW;AAChE,iBAAO,OAAO,QAAQ;AAAA,QACxB,QAAQ;AACN,uBAAa,YAAY;AACzB,iBAAO,QAAQ,OAAO,KAAK;AAAA,QAC7B,UAAE;AACA,yBAAe;AAAA,QACjB;AAAA,MACF;AAEA,aAAO,QAAQ,OAAO,KAAK;AAAA,IAC7B;AAAA,EACF;AAEA,SAAO;AACT;AAEA,IAAI,YAAkC;AAE/B,SAAS,qBAAqB,UAAyB;AAC5D,cAAY;AACd;AAEO,SAAS,eAA8B;AAC5C,MAAI,CAAC,UAAW,OAAM,IAAI,MAAM,kEAAkE;AAClG,SAAO;AACT;;;AE5KO,SAAS,eAAuB;AACrC,MAAI,OAAO,WAAW,QAAQ,eAAe,YAAY;AACvD,WAAO,WAAW,OAAO,WAAW;AAAA,EACtC;AACA,SAAO,uCAAuC,QAAQ,SAAS,CAAC,MAAM;AACpE,UAAM,IAAK,KAAK,OAAO,IAAI,KAAM;AACjC,YAAQ,MAAM,MAAM,IAAK,IAAI,IAAO,GAAK,SAAS,EAAE;AAAA,EACtD,CAAC;AACH;;;ACIO,IAAM,aAAa;AAAA,EACxB,MAAM,oBAAoB,SAA6D;AACrF,UAAM,EAAE,KAAK,IAAI,MAAM,aAAa,EAAE,KAA2B,0BAA0B,OAAO;AAClG,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,yBAAyB,OAG5B;AACD,UAAM,EAAE,KAAK,IAAI,MAAM,aAAa,EAAE,KAAK,gCAAgC,EAAE,MAAM,CAAC;AACpF,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,cAAc,QAAuC;AACzD,UAAM,EAAE,KAAK,IAAI,MAAM,aAAa,EAAE,KAAmB,oBAAoB,MAAM,EAAE;AACrF,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,QAAQ,QAAuC;AACnD,UAAM,EAAE,KAAK,IAAI,MAAM,aAAa,EAAE,IAAkB,kBAAkB,MAAM,EAAE;AAClF,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,WAAW,QAAgB,WAAiE;AAChG,UAAM,EAAE,KAAK,IAAI,MAAM,aAAa,EAAE,IAAI,kBAAkB,MAAM,QAAQ;AAAA,MACxE,QAAQ,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACvC,CAAC;AACD,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,WAAW,QAA+B;AAC9C,UAAM,aAAa,EAAE,KAAK,kBAAkB,MAAM,SAAS;AAAA,EAC7D;AAAA,EAEA,MAAM,qBACJ,gBACA,SAA6D,CAAC,GACpB;AAC1C,UAAM,EAAE,KAAK,IAAI,MAAM,aAAa,EAAE;AAAA,MACpC,0BAA0B,cAAc;AAAA,MACxC,EAAE,OAAO;AAAA,IACX;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,WAAW,SAA4C,CAAC,GAA6C;AACzG,UAAM,EAAE,KAAK,IAAI,MAAM,aAAa,EAAE,IAAI,qBAAqB,EAAE,OAAO,CAAC;AACzE,WAAO;AAAA,EACT;AACF;AAgBA,eAAe,eACb,OACA,kBACA,SACA,gBACA,YACA,oBACA,mBAC+E;AAE/E,QAAM,kBAAkB,MAAM,QAAQ;AAAA,IACpC,MAAM,IAAI,CAAC,MAAM,aAAa,GAAG,oBAAoB,qBAAqB,EAAE,SAAS,OAAO,cAAc,MAAM,mBAAmB,MAAM,mBAAmB,KAAK,CAAC,CAAC;AAAA,EACrK;AAKA,QAAM,UAAU,gBAAgB,IAAI,CAAC,GAAG,OAAO,EAAE,MAAM,GAAG,QAAQ,QAAQ,CAAC,GAAG,aAAa,EAAE,EAAE;AAE/F,QAAM,WAAkC,QAAQ,IAAI,CAAC,EAAE,MAAM,GAAG,YAAY,OAAO;AAAA,IACjF,UAAU,EAAE;AAAA,IACZ,UAAU,EAAE;AAAA,IACZ,MAAM,EAAE;AAAA,IACR;AAAA,IACA;AAAA,IACA,GAAI,EAAE,cAAc;AAAA,MAClB,UAAU;AAAA,QACR,YAAY,EAAE;AAAA,QACd,cAAc,EAAE;AAAA,QAChB,sBAAsB,EAAE;AAAA,MAC1B;AAAA,IACF;AAAA,EACF,EAAE;AAEF,QAAM,EAAE,MAAM,QAAQ,cAAc,IAAI,MAAM,WAAW,yBAAyB,QAAQ;AAI1F,QAAM,gBAAgB,oBAAI,IAAY;AACtC,QAAM,SAAqD,cAAc,IAAI,CAAC,MAAM;AAClF,UAAM,MAAM,EAAE,eAAe,QAAQ,UAAU,CAAC,MAAM,EAAE,KAAK,SAAS,EAAE,QAAQ;AAChF,UAAM,SAAS,QAAQ,GAAG,GAAG;AAC7B,QAAI,OAAQ,eAAc,IAAI,MAAM;AACpC,WAAO,EAAE,UAAU,EAAE,UAAU,OAAO,EAAE,MAAM;AAAA,EAChD,CAAC;AAGD,QAAM,cAAsC,CAAC;AAC7C,QAAM,iBAAiB,MAAM;AAC3B,QAAI,CAAC,WAAY;AACjB,UAAM,OAAO,OAAO,OAAO,WAAW;AACtC,UAAM,MAAM,KAAK,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,KAAK,IAAI,KAAK,QAAQ,CAAC;AACrE,eAAW,KAAK,MAAM,GAAG,CAAC;AAAA,EAC5B;AAEA,QAAM,aAA6B,CAAC;AACpC,QAAM,aAAa,oBAAI,IAA0B;AAEjD,QAAM,QAAQ;AAAA,IACZ,KAAK,IAAI,OAAO,WAAW,QAAQ;AAGjC,YAAM,cAAc,UAAU,eAAe;AAC7C,YAAM,EAAE,MAAM,OAAO,IAAI,QAAQ,WAAW;AAC5C,kBAAY,WAAW,IAAI;AAC3B,UAAI;AACF,cAAM,iBAAiB,WAAW,MAAM,CAAC,QAAQ;AAC/C,sBAAY,WAAW,IAAI,KAAK,MAAM,MAAM,GAAG;AAC/C,yBAAe;AAAA,QACjB,CAAC;AAED,cAAM,eAAe,MAAM,WAAW,cAAc,UAAU,MAAM;AACpE,oBAAY,WAAW,IAAI;AAC3B,uBAAe;AACf,mBAAW,KAAK,YAAY;AAC5B,mBAAW,IAAI,QAAQ,YAAY;AAAA,MACrC,SAAS,KAAK;AACZ,eAAO,KAAK,EAAE,UAAU,KAAK,MAAM,OAAQ,IAAc,QAAQ,CAAC;AAAA,MACpE;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO,EAAE,QAAQ,EAAE,YAAY,OAAO,GAAG,WAAW;AACtD;AAGA,eAAsB,YACpB,OACA,kBACA,gBACA,YACA,oBACA,mBAC4B;AAC5B,QAAM,UAAU,MAAM,IAAI,MAAM,aAAa,CAAC;AAC9C,QAAM,EAAE,OAAO,IAAI,MAAM,eAAe,OAAO,kBAAkB,SAAS,gBAAgB,YAAY,oBAAoB,iBAAiB;AAC3I,SAAO;AACT;AAOA,eAAsB,qBACpB,OACA,kBACA,SACA,gBACA,YACA,oBACA,mBAC+E;AAC/E,SAAO,eAAe,OAAO,kBAAkB,SAAS,gBAAgB,YAAY,oBAAoB,iBAAiB;AAC3H;","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -251,24 +251,40 @@ async function compressFile(file, platformCompressFn, config) {
|
|
|
251
251
|
var import_axios = __toESM(require("axios"), 1);
|
|
252
252
|
|
|
253
253
|
// src/crypto/transit.ts
|
|
254
|
+
function hasWebCrypto() {
|
|
255
|
+
return typeof globalThis.crypto?.subtle !== "undefined";
|
|
256
|
+
}
|
|
254
257
|
async function encryptPayload(data, sessionKey) {
|
|
255
|
-
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
256
258
|
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
if (!hasWebCrypto() || sessionKey instanceof Uint8Array) {
|
|
260
|
+
return encryptNoble(plaintext, sessionKey);
|
|
261
|
+
}
|
|
262
|
+
return encryptWebCrypto(plaintext, sessionKey);
|
|
263
|
+
}
|
|
264
|
+
async function encryptWebCrypto(plaintext, sessionKey) {
|
|
265
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
266
|
+
const encrypted = await globalThis.crypto.subtle.encrypt({ name: "AES-GCM", iv }, sessionKey, plaintext);
|
|
262
267
|
const ct = encrypted.slice(0, encrypted.byteLength - 16);
|
|
263
268
|
const tag = encrypted.slice(encrypted.byteLength - 16);
|
|
264
|
-
return {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
269
|
+
return { v: 1, iv: bufToB64(iv), tag: bufToB64(tag), ct: bufToB64(ct) };
|
|
270
|
+
}
|
|
271
|
+
async function encryptNoble(plaintext, sessionKey) {
|
|
272
|
+
const { gcm } = await import("@noble/ciphers/aes");
|
|
273
|
+
const { randomBytes } = await import("@noble/hashes/utils");
|
|
274
|
+
const iv = randomBytes(12);
|
|
275
|
+
const cipher = gcm(sessionKey, iv);
|
|
276
|
+
const encrypted = cipher.encrypt(plaintext);
|
|
277
|
+
const ct = encrypted.slice(0, encrypted.length - 16);
|
|
278
|
+
const tag = encrypted.slice(encrypted.length - 16);
|
|
279
|
+
return { v: 1, iv: uint8ToB64(iv), tag: uint8ToB64(tag), ct: uint8ToB64(ct) };
|
|
270
280
|
}
|
|
271
281
|
async function decryptPayload(envelope, sessionKey) {
|
|
282
|
+
if (!hasWebCrypto() || sessionKey instanceof Uint8Array) {
|
|
283
|
+
return decryptNoble(envelope, sessionKey);
|
|
284
|
+
}
|
|
285
|
+
return decryptWebCrypto(envelope, sessionKey);
|
|
286
|
+
}
|
|
287
|
+
async function decryptWebCrypto(envelope, sessionKey) {
|
|
272
288
|
const iv = b64ToBuf(envelope.iv);
|
|
273
289
|
const tag = b64ToBuf(envelope.tag);
|
|
274
290
|
const ct = b64ToBuf(envelope.ct);
|
|
@@ -282,6 +298,18 @@ async function decryptPayload(envelope, sessionKey) {
|
|
|
282
298
|
);
|
|
283
299
|
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
284
300
|
}
|
|
301
|
+
async function decryptNoble(envelope, sessionKey) {
|
|
302
|
+
const { gcm } = await import("@noble/ciphers/aes");
|
|
303
|
+
const iv = base64ToUint8(envelope.iv);
|
|
304
|
+
const tag = base64ToUint8(envelope.tag);
|
|
305
|
+
const ct = base64ToUint8(envelope.ct);
|
|
306
|
+
const combined = new Uint8Array(ct.length + tag.length);
|
|
307
|
+
combined.set(ct, 0);
|
|
308
|
+
combined.set(tag, ct.length);
|
|
309
|
+
const cipher = gcm(sessionKey, iv);
|
|
310
|
+
const decrypted = cipher.decrypt(combined);
|
|
311
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
312
|
+
}
|
|
285
313
|
function isTransitEnvelope(v) {
|
|
286
314
|
return typeof v === "object" && v !== null && v.v === 1 && typeof v.iv === "string" && typeof v.tag === "string" && typeof v.ct === "string";
|
|
287
315
|
}
|
|
@@ -293,12 +321,25 @@ function bufToB64(buf) {
|
|
|
293
321
|
});
|
|
294
322
|
return btoa(str);
|
|
295
323
|
}
|
|
324
|
+
function uint8ToB64(bytes) {
|
|
325
|
+
let str = "";
|
|
326
|
+
bytes.forEach((b) => {
|
|
327
|
+
str += String.fromCharCode(b);
|
|
328
|
+
});
|
|
329
|
+
return btoa(str);
|
|
330
|
+
}
|
|
296
331
|
function b64ToBuf(b64) {
|
|
297
332
|
const bin = atob(b64);
|
|
298
333
|
const buf = new Uint8Array(bin.length);
|
|
299
334
|
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
300
335
|
return buf.buffer;
|
|
301
336
|
}
|
|
337
|
+
function base64ToUint8(b64) {
|
|
338
|
+
const bin = atob(b64);
|
|
339
|
+
const buf = new Uint8Array(bin.length);
|
|
340
|
+
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
341
|
+
return buf;
|
|
342
|
+
}
|
|
302
343
|
|
|
303
344
|
// src/crypto/session.ts
|
|
304
345
|
var _KEY = /* @__PURE__ */ Symbol.for("__antz_chat_transit__");
|
|
@@ -381,10 +422,8 @@ function initApiClient(config, tokenStore) {
|
|
|
381
422
|
else if (_config.avatar.url) req.headers["x-avatar-url"] = _config.avatar.url;
|
|
382
423
|
_avatarSent = true;
|
|
383
424
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (!isExempt) await waitForTransitReady();
|
|
387
|
-
if (!isExempt && isTransitEnabled()) {
|
|
425
|
+
if (token) await waitForTransitReady();
|
|
426
|
+
if (isTransitEnabled()) {
|
|
388
427
|
const sessionId = getSessionId();
|
|
389
428
|
const key = getSessionKey();
|
|
390
429
|
if (sessionId && key) {
|
|
@@ -978,6 +1017,9 @@ function resetAlgoCache() {
|
|
|
978
1017
|
}
|
|
979
1018
|
|
|
980
1019
|
// src/crypto/handshake.ts
|
|
1020
|
+
function hasWebCrypto2() {
|
|
1021
|
+
return typeof globalThis.crypto?.subtle !== "undefined";
|
|
1022
|
+
}
|
|
981
1023
|
async function fetchServerKeys(apiUrl) {
|
|
982
1024
|
const res = await fetch(`${apiUrl}/crypto/pubkey`);
|
|
983
1025
|
if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
|
|
@@ -985,6 +1027,12 @@ async function fetchServerKeys(apiUrl) {
|
|
|
985
1027
|
return body?.data ?? body;
|
|
986
1028
|
}
|
|
987
1029
|
async function performHandshake(algo, serverKeys, socketHandshakeAuth) {
|
|
1030
|
+
if (hasWebCrypto2()) {
|
|
1031
|
+
return performWebCryptoHandshake(algo, serverKeys, socketHandshakeAuth);
|
|
1032
|
+
}
|
|
1033
|
+
return performNobleHandshake(serverKeys, socketHandshakeAuth);
|
|
1034
|
+
}
|
|
1035
|
+
async function performWebCryptoHandshake(algo, serverKeys, socketHandshakeAuth) {
|
|
988
1036
|
const ephemeral = await globalThis.crypto.subtle.generateKey(
|
|
989
1037
|
algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" },
|
|
990
1038
|
false,
|
|
@@ -994,31 +1042,18 @@ async function performHandshake(algo, serverKeys, socketHandshakeAuth) {
|
|
|
994
1042
|
socketHandshakeAuth["transitEphemeralPub"] = bufToB642(pubRaw);
|
|
995
1043
|
socketHandshakeAuth["transitAlgo"] = algo;
|
|
996
1044
|
const ephemeralPriv = ephemeral.privateKey;
|
|
997
|
-
return (sessionId) =>
|
|
1045
|
+
return (sessionId) => deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId);
|
|
998
1046
|
}
|
|
999
|
-
async function
|
|
1000
|
-
const
|
|
1001
|
-
const serverPubRaw = b64ToBuf2(serverPubB64);
|
|
1047
|
+
async function deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
|
|
1048
|
+
const serverPubRaw = b64ToBuf2(algo === "x25519" ? serverKeys.x25519 : serverKeys.p256);
|
|
1002
1049
|
const keyAlgoParams = algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" };
|
|
1003
|
-
const serverPubKey = await globalThis.crypto.subtle.importKey(
|
|
1004
|
-
"raw",
|
|
1005
|
-
serverPubRaw,
|
|
1006
|
-
keyAlgoParams,
|
|
1007
|
-
false,
|
|
1008
|
-
[]
|
|
1009
|
-
);
|
|
1050
|
+
const serverPubKey = await globalThis.crypto.subtle.importKey("raw", serverPubRaw, keyAlgoParams, false, []);
|
|
1010
1051
|
const sharedBits = await globalThis.crypto.subtle.deriveBits(
|
|
1011
1052
|
{ name: algo === "x25519" ? "X25519" : "ECDH", public: serverPubKey },
|
|
1012
1053
|
ephemeralPriv,
|
|
1013
1054
|
256
|
|
1014
1055
|
);
|
|
1015
|
-
const hkdfKey = await globalThis.crypto.subtle.importKey(
|
|
1016
|
-
"raw",
|
|
1017
|
-
sharedBits,
|
|
1018
|
-
"HKDF",
|
|
1019
|
-
false,
|
|
1020
|
-
["deriveKey"]
|
|
1021
|
-
);
|
|
1056
|
+
const hkdfKey = await globalThis.crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
|
|
1022
1057
|
const salt = new TextEncoder().encode(sessionId);
|
|
1023
1058
|
const info = new TextEncoder().encode("antz-transit-v1");
|
|
1024
1059
|
return globalThis.crypto.subtle.deriveKey(
|
|
@@ -1029,6 +1064,24 @@ async function deriveSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
|
|
|
1029
1064
|
["encrypt", "decrypt"]
|
|
1030
1065
|
);
|
|
1031
1066
|
}
|
|
1067
|
+
async function performNobleHandshake(serverKeys, socketHandshakeAuth) {
|
|
1068
|
+
const { x25519 } = await import("@noble/curves/ed25519");
|
|
1069
|
+
const { hkdf } = await import("@noble/hashes/hkdf");
|
|
1070
|
+
const { sha256 } = await import("@noble/hashes/sha256");
|
|
1071
|
+
const { randomBytes } = await import("@noble/hashes/utils");
|
|
1072
|
+
const ephemeralPriv = randomBytes(32);
|
|
1073
|
+
const ephemeralPub = x25519.getPublicKey(ephemeralPriv);
|
|
1074
|
+
const serverPubBytes = base64ToUint82(serverKeys.x25519);
|
|
1075
|
+
socketHandshakeAuth["transitEphemeralPub"] = uint8ToBase64(ephemeralPub);
|
|
1076
|
+
socketHandshakeAuth["transitAlgo"] = "x25519";
|
|
1077
|
+
return (sessionId) => {
|
|
1078
|
+
const sharedSecret = x25519.getSharedSecret(ephemeralPriv, serverPubBytes);
|
|
1079
|
+
const salt = new TextEncoder().encode(sessionId);
|
|
1080
|
+
const info = new TextEncoder().encode("antz-transit-v1");
|
|
1081
|
+
const sessionKey = hkdf(sha256, sharedSecret, salt, info, 32);
|
|
1082
|
+
return Promise.resolve(sessionKey);
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1032
1085
|
function bufToB642(buf) {
|
|
1033
1086
|
const bytes = new Uint8Array(buf);
|
|
1034
1087
|
let str = "";
|
|
@@ -1043,6 +1096,19 @@ function b64ToBuf2(b64) {
|
|
|
1043
1096
|
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
1044
1097
|
return buf.buffer;
|
|
1045
1098
|
}
|
|
1099
|
+
function uint8ToBase64(bytes) {
|
|
1100
|
+
let str = "";
|
|
1101
|
+
bytes.forEach((b) => {
|
|
1102
|
+
str += String.fromCharCode(b);
|
|
1103
|
+
});
|
|
1104
|
+
return btoa(str);
|
|
1105
|
+
}
|
|
1106
|
+
function base64ToUint82(b64) {
|
|
1107
|
+
const bin = atob(b64);
|
|
1108
|
+
const buf = new Uint8Array(bin.length);
|
|
1109
|
+
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
1110
|
+
return buf;
|
|
1111
|
+
}
|
|
1046
1112
|
|
|
1047
1113
|
// src/socket/socket.ts
|
|
1048
1114
|
var _socket = null;
|
|
@@ -1147,7 +1213,7 @@ async function _doConnect(config, getToken) {
|
|
|
1147
1213
|
"[AntzChat] Transit encryption mismatch: SDK has transitEncryption=true but server has TRANSIT_ENCRYPTION_ENABLED=false. Align the config on both sides."
|
|
1148
1214
|
);
|
|
1149
1215
|
}
|
|
1150
|
-
const algo = await detectTransitAlgo();
|
|
1216
|
+
const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
|
|
1151
1217
|
boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);
|
|
1152
1218
|
} catch (err) {
|
|
1153
1219
|
throw err;
|