fix: resolve TypeScript 5.5 Uint8Array generic errors and missing vite/client types
Build & Release / Docker / frontend (push) Has been cancelled
Build & Release / Desktop / Linux & Windows (push) Has been cancelled
Build & Release / Deploy to server (push) Has been cancelled
Build & Release / Create release (push) Has been cancelled
Build & Release / Docker / backend (push) Has been cancelled

TS 5.5 made typed arrays generic — Uint8Array now defaults to
Uint8Array<ArrayBufferLike>, which no longer satisfies Web Crypto's
BufferSource (requires ArrayBufferView<ArrayBuffer>).

- Add u8() helper that wraps .slice() results in new Uint8Array(),
  producing Uint8Array<ArrayBuffer> throughout crypto/index.ts
- Annotate all Uint8Array parameters/returns with <ArrayBuffer>
- Add 'vite/client' to tsconfig types so import.meta.env is recognized
- ws.ts: change socket.on() cleanup return to void (fixes useEffect type)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-28 10:56:34 -04:00
1 parent d92e899fc7
commit e2d9bf1514
3 files changed
+26 -42

No files matched your search

+24 -41
View File
@@ -5,6 +5,11 @@
const ENC = new TextEncoder();
const DEC = new TextDecoder();
// Wrap .slice() result so TypeScript sees Uint8Array<ArrayBuffer>, not Uint8Array<ArrayBufferLike>
function u8(arr: Uint8Array, start?: number, end?: number): Uint8Array<ArrayBuffer> {
return new Uint8Array(start !== undefined ? arr.slice(start, end) : arr);
}
// ── Key derivation ──────────────────────────────────────────────────────────
export async function generateKdfSalt(): Promise<string> {
@@ -47,21 +52,15 @@ export async function decryptMasterKey(encB64: string, derived: CryptoKey): Prom
// ── X25519 ECDH keypair ──────────────────────────────────────────────────────
/**
* Generate an X25519 keypair.
* Returns the public key as base64 and the private key as a non-extractable CryptoKey.
* Also returns pkcs8Bytes so the caller can encrypt and store the private key once.
*/
export async function generateX25519Keypair(): Promise<{
publicKeyB64: string;
privateKey: CryptoKey;
pkcs8Bytes: Uint8Array;
pkcs8Bytes: Uint8Array<ArrayBuffer>;
}> {
const kp = await crypto.subtle.generateKey({ name: 'X25519' } as any, true, ['deriveBits']);
const publicRaw = await crypto.subtle.exportKey('raw', (kp as CryptoKeyPair).publicKey);
const pkcs8 = await crypto.subtle.exportKey('pkcs8', (kp as CryptoKeyPair).privateKey);
// Re-import as non-extractable for runtime use
const privateKey = await crypto.subtle.importKey(
'pkcs8',
pkcs8,
@@ -77,12 +76,10 @@ export async function generateX25519Keypair(): Promise<{
};
}
/** Encrypt the pkcs8-encoded private key with the master key for server storage */
export async function encryptPrivateKey(pkcs8: Uint8Array, master: CryptoKey): Promise<string> {
export async function encryptPrivateKey(pkcs8: Uint8Array<ArrayBuffer>, master: CryptoKey): Promise<string> {
return aesEncryptRaw(pkcs8, master);
}
/** Decrypt and import the private key, returning a non-extractable CryptoKey */
export async function decryptPrivateKeyToCryptoKey(
encB64: string,
master: CryptoKey
@@ -93,20 +90,14 @@ export async function decryptPrivateKeyToCryptoKey(
// ── Conversation key exchange (ephemeral X25519 ECDH sealed box) ─────────────
//
// Wire format for sealConvKey output: ephemeral_pub_key(32) | nonce(12) | ciphertext
// The ephemeral public key is used by the recipient to reconstruct the shared secret.
// Wire format: ephemeral_pub_key(32) | nonce(12) | ciphertext
export function generateConvKeyRaw(): Uint8Array {
export function generateConvKeyRaw(): Uint8Array<ArrayBuffer> {
return crypto.getRandomValues(new Uint8Array(32));
}
/**
* Seal a conversation key for a recipient using their X25519 public key.
* Uses an ephemeral ECDH keypair so the sender stays anonymous.
* Only the holder of the matching private key can open it.
*/
export async function sealConvKey(
keyRaw: Uint8Array,
keyRaw: Uint8Array<ArrayBuffer>,
recipientPublicKeyB64: string
): Promise<string> {
const recipientPubKey = await crypto.subtle.importKey(
@@ -117,7 +108,6 @@ export async function sealConvKey(
[]
);
// Ephemeral keypair for this seal operation
const ephemeral = await crypto.subtle.generateKey({ name: 'X25519' } as any, true, [
'deriveBits',
]);
@@ -125,20 +115,17 @@ export async function sealConvKey(
await crypto.subtle.exportKey('raw', (ephemeral as CryptoKeyPair).publicKey)
);
// ECDH: derive shared secret between ephemeral private key and recipient public key
const sharedBits = await crypto.subtle.deriveBits(
{ name: 'X25519', public: recipientPubKey } as any,
(ephemeral as CryptoKeyPair).privateKey,
256
);
// HKDF to derive AES key — use ephemeral public key as salt for domain separation
const aesKey = await hkdfToAes(sharedBits, ephemeralPubRaw, ['encrypt']);
const nonce = crypto.getRandomValues(new Uint8Array(12));
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, aesKey, keyRaw);
// Output: ephemeralPubKey(32) | nonce(12) | ciphertext
const out = new Uint8Array(32 + 12 + ct.byteLength);
out.set(ephemeralPubRaw, 0);
out.set(nonce, 32);
@@ -146,18 +133,14 @@ export async function sealConvKey(
return bufToB64(out);
}
/**
* Open a sealed conversation key using own X25519 private key.
* Returns the raw 32-byte AES key.
*/
export async function openSealedConvKey(
sealedB64: string,
myPrivateKey: CryptoKey
): Promise<Uint8Array> {
): Promise<Uint8Array<ArrayBuffer>> {
const data = b64ToBuf(sealedB64);
const ephemeralPubRaw = data.slice(0, 32);
const nonce = data.slice(32, 44);
const ct = data.slice(44);
const ephemeralPubRaw = u8(data, 0, 32);
const nonce = u8(data, 32, 44);
const ct = u8(data, 44);
const ephemeralPubKey = await crypto.subtle.importKey(
'raw',
@@ -178,7 +161,7 @@ export async function openSealedConvKey(
return new Uint8Array(pt);
}
export async function importConvKey(raw: Uint8Array): Promise<CryptoKey> {
export async function importConvKey(raw: Uint8Array<ArrayBuffer>): Promise<CryptoKey> {
return crypto.subtle.importKey('raw', raw, { name: 'AES-GCM', length: 256 }, true, [
'encrypt',
'decrypt',
@@ -211,9 +194,9 @@ export async function encryptFile(file: File, key: CryptoKey): Promise<Blob> {
return new Blob([combined], { type: 'application/octet-stream' });
}
export async function decryptFile(blob: Blob, key: CryptoKey): Promise<Uint8Array> {
export async function decryptFile(blob: Blob, key: CryptoKey): Promise<Uint8Array<ArrayBuffer>> {
const data = new Uint8Array(await blob.arrayBuffer());
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: data.slice(0, 12) }, key, data.slice(12));
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: u8(data, 0, 12) }, key, u8(data, 12));
return new Uint8Array(pt);
}
@@ -229,7 +212,7 @@ export async function decryptFileKey(encB64: string, masterKey: CryptoKey): Prom
async function hkdfToAes(
sharedBits: ArrayBuffer,
salt: Uint8Array,
salt: Uint8Array<ArrayBuffer>,
usages: KeyUsage[]
): Promise<CryptoKey> {
const keyMaterial = await crypto.subtle.importKey('raw', sharedBits, 'HKDF', false, [
@@ -244,7 +227,7 @@ async function hkdfToAes(
);
}
async function aesEncryptRaw(data: Uint8Array, key: CryptoKey): Promise<string> {
async function aesEncryptRaw(data: Uint8Array<ArrayBuffer>, key: CryptoKey): Promise<string> {
const nonce = crypto.getRandomValues(new Uint8Array(12));
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, key, data);
const combined = new Uint8Array(12 + ct.byteLength);
@@ -253,17 +236,17 @@ async function aesEncryptRaw(data: Uint8Array, key: CryptoKey): Promise<string>
return bufToB64(combined);
}
async function aesDecryptRaw(b64: string, key: CryptoKey): Promise<Uint8Array> {
async function aesDecryptRaw(b64: string, key: CryptoKey): Promise<Uint8Array<ArrayBuffer>> {
const data = b64ToBuf(b64);
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: data.slice(0, 12) }, key, data.slice(12));
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: u8(data, 0, 12) }, key, u8(data, 12));
return new Uint8Array(pt);
}
export function bufToB64(buf: ArrayBuffer | Uint8Array): string {
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
return btoa(String.fromCharCode(...u8));
const u8arr = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
return btoa(String.fromCharCode(...u8arr));
}
export function b64ToBuf(b64: string): Uint8Array {
export function b64ToBuf(b64: string): Uint8Array<ArrayBuffer> {
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
}
+1 -1
View File
@@ -37,7 +37,7 @@ class CloudLinkSocket {
on(handler: MessageHandler) {
this.handlers.add(handler);
return () => this.handlers.delete(handler);
return () => { this.handlers.delete(handler); };
}
}
+1
View File
@@ -2,6 +2,7 @@
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",