@coder/mux-md-client 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/crypto.ts","../src/client.ts","../src/signing.ts"],"sourcesContent":["/**\n * @coder/mux-md-client - Client library for mux.md encrypted file sharing\n *\n * @example\n * ```typescript\n * import { upload, download, createSignatureEnvelope } from '@coder/mux-md-client';\n *\n * // Upload with optional signature\n * const content = new TextEncoder().encode('# Hello World');\n * const signature = await createSignatureEnvelope(content, privateKey, publicKey);\n * const result = await upload(content, { name: 'msg.md', type: 'text/markdown', size: content.length }, { signature });\n *\n * // Download\n * const { data, info, signature } = await download(result.url);\n * ```\n */\n\n// High-level client operations\nexport {\n upload,\n download,\n getMeta,\n deleteFile,\n setExpiration,\n parseUrl,\n buildUrl,\n type UploadOptions,\n type UploadResult,\n type DownloadResult,\n type SetExpirationOptions,\n type SetExpirationResult,\n} from './client';\n\n// Signing & verification\nexport {\n signEd25519,\n signECDSA,\n verifySignature,\n createSignatureEnvelope,\n parsePublicKey,\n computeFingerprint,\n formatFingerprint,\n type KeyType,\n type ParsedPublicKey,\n} from './signing';\n\n// Low-level crypto (for advanced use)\nexport {\n deriveKey,\n encrypt,\n decrypt,\n generateKey,\n generateSalt,\n generateIV,\n generateId,\n generateMutateKey,\n base64Encode,\n base64Decode,\n base64UrlEncode,\n base64UrlDecode,\n} from './crypto';\n\n// Types\nexport type {\n FileInfo,\n UploadMeta,\n SignatureEnvelope,\n SignedPayload,\n} from './types';\n","/**\n * Cryptographic utilities for mux.md\n *\n * Security parameters:\n * - Key: 80 bits entropy (14 chars base64url)\n * - ID: 30 bits entropy (5 chars base62) - not security critical\n * - Salt: 128 bits (16 bytes) - HKDF salt\n * - IV: 96 bits (12 bytes) for AES-GCM\n */\nconst SALT_BYTES = 16;\nconst IV_BYTES = 12;\nconst KEY_BYTES = 10; // 80 bits\nconst ID_BYTES = 4; // 32 bits, we'll use 30\nconst MUTATE_KEY_BYTES = 16; // 128 bits for mutate key\n\n// Base62 alphabet for URL-safe IDs (no special chars)\nconst BASE62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n\n/**\n * Generate a random file ID (30 bits, 5 chars base62)\n */\nexport function generateId(): string {\n const bytes = new Uint8Array(ID_BYTES);\n crypto.getRandomValues(bytes);\n\n // Convert to number (32 bits), then encode as base62\n const num = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];\n\n // Use modulo to get 5 base62 characters (covers ~30 bits)\n let result = '';\n let n = num >>> 0; // Ensure unsigned\n for (let i = 0; i < 5; i++) {\n result = BASE62[n % 62] + result;\n n = Math.floor(n / 62);\n }\n\n return result;\n}\n\n/**\n * Generate encryption key material (80 bits, 14 chars base64url)\n */\nexport function generateKey(): string {\n const bytes = new Uint8Array(KEY_BYTES);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(bytes);\n}\n\n/**\n * Generate mutate key (128 bits, 22 chars base64url)\n * Used for mutation operations: delete, set expiration\n */\nexport function generateMutateKey(): string {\n const bytes = new Uint8Array(MUTATE_KEY_BYTES);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(bytes);\n}\n\n/**\n * Generate random salt for HKDF (128 bits)\n */\nexport function generateSalt(): Uint8Array {\n const salt = new Uint8Array(SALT_BYTES);\n crypto.getRandomValues(salt);\n return salt;\n}\n\n/**\n * Generate random IV for AES-GCM (96 bits)\n */\nexport function generateIV(): Uint8Array {\n const iv = new Uint8Array(IV_BYTES);\n crypto.getRandomValues(iv);\n return iv;\n}\n\n/**\n * Derive AES-256 key from key material using HKDF\n *\n * HKDF is the correct choice for deriving keys from high-entropy\n * random key material. Unlike PBKDF2, it doesn't need iterations\n * since we're not stretching a weak password.\n */\nexport async function deriveKey(\n keyMaterial: string,\n salt: Uint8Array,\n): Promise<CryptoKey> {\n // Import the raw key material\n const rawKey = base64UrlDecode(keyMaterial);\n const baseKey = await crypto.subtle.importKey(\n 'raw',\n rawKey.buffer as ArrayBuffer,\n 'HKDF',\n false,\n ['deriveKey'],\n );\n\n // Derive AES-256 key using HKDF\n return crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n salt: salt.buffer as ArrayBuffer,\n info: new Uint8Array(0), // No additional context needed\n hash: 'SHA-256',\n },\n baseKey,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n );\n}\n\n/**\n * Encrypt data using AES-256-GCM\n */\nexport async function encrypt(\n data: Uint8Array,\n key: CryptoKey,\n iv: Uint8Array,\n): Promise<Uint8Array> {\n const ciphertext = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv: iv.buffer as ArrayBuffer },\n key,\n data.buffer as ArrayBuffer,\n );\n return new Uint8Array(ciphertext);\n}\n\n/**\n * Decrypt data using AES-256-GCM\n */\nexport async function decrypt(\n ciphertext: Uint8Array,\n key: CryptoKey,\n iv: Uint8Array,\n): Promise<Uint8Array> {\n const plaintext = await crypto.subtle.decrypt(\n { name: 'AES-GCM', iv: iv.buffer as ArrayBuffer },\n key,\n ciphertext.buffer as ArrayBuffer,\n );\n return new Uint8Array(plaintext);\n}\n\n/**\n * Base64url encode (URL-safe, no padding)\n */\nexport function base64UrlEncode(data: Uint8Array): string {\n const base64 = btoa(String.fromCharCode(...data));\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\n/**\n * Base64url decode\n */\nexport function base64UrlDecode(str: string): Uint8Array {\n // Restore standard base64\n let base64 = str.replace(/-/g, '+').replace(/_/g, '/');\n // Add padding if needed\n while (base64.length % 4) {\n base64 += '=';\n }\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\n/**\n * Standard base64 encode\n */\nexport function base64Encode(data: Uint8Array): string {\n return btoa(String.fromCharCode(...data));\n}\n\n/**\n * Standard base64 decode\n */\nexport function base64Decode(str: string): Uint8Array {\n const binary = atob(str);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n","/**\n * mux.md Client Library\n *\n * Reference implementation for encrypting, uploading, downloading, and decrypting files.\n * Works in both Node.js (with webcrypto) and browser environments.\n */\n\nimport {\n base64Decode,\n base64Encode,\n decrypt,\n deriveKey,\n encrypt,\n generateIV,\n generateKey,\n generateSalt,\n} from './crypto';\nimport type {\n FileInfo,\n SignatureEnvelope,\n SignedPayload,\n UploadMeta,\n} from './types';\n\n// Internal types (not exported from package)\ninterface UploadResponse {\n id: string;\n url: string;\n mutateKey: string;\n expiresAt?: number;\n}\n\ninterface MetaResponse {\n salt: string;\n iv: string;\n encryptedMeta: string;\n size: number;\n}\n\nconst DEFAULT_BASE_URL = 'https://mux.md';\n\nexport interface UploadOptions {\n /** Base URL of the mux.md service */\n baseUrl?: string;\n /** Expiration time (unix timestamp ms, ISO date string, or Date object) */\n expiresAt?: number | string | Date;\n /** Signature envelope (will be encrypted with content) */\n signature?: SignatureEnvelope;\n}\n\nexport interface UploadResult {\n /** Full URL with encryption key in fragment */\n url: string;\n /** File ID (without key) */\n id: string;\n /** Encryption key (base64url) */\n key: string;\n /** Mutate key (base64url) - store this to mutate (delete, change expiration) the file later */\n mutateKey: string;\n /** Expiration timestamp (ms), if set */\n expiresAt?: number;\n}\n\nexport interface DownloadResult {\n /** Decrypted file content */\n data: Uint8Array;\n /** Original file info (name, type, size) */\n info: FileInfo;\n /** Decrypted signature envelope (if present) */\n signature?: SignatureEnvelope;\n}\n\n/**\n * Encrypt and upload a file to mux.md\n *\n * @param data - File contents as Uint8Array\n * @param fileInfo - Original file metadata (name, type, size)\n * @param options - Upload options (including optional signature)\n * @returns Upload result with URL containing encryption key\n */\nexport async function upload(\n data: Uint8Array,\n fileInfo: FileInfo,\n options: UploadOptions = {},\n): Promise<UploadResult> {\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;\n\n // Generate encryption parameters\n const keyMaterial = generateKey();\n const salt = generateSalt();\n const iv = generateIV();\n const cryptoKey = await deriveKey(keyMaterial, salt);\n\n // Build plaintext - either signed (JSON) or raw content\n let plaintext: Uint8Array;\n if (options.signature) {\n // Signed format: JSON with content + signature\n const signed: SignedPayload = {\n content: new TextDecoder().decode(data),\n sig: options.signature,\n };\n plaintext = new TextEncoder().encode(JSON.stringify(signed));\n } else {\n // Unsigned: raw content bytes\n plaintext = data;\n }\n\n // Single encryption for the payload\n const payload = await encrypt(plaintext, cryptoKey, iv);\n\n // Encrypt file metadata (always the same way)\n const metaJson = JSON.stringify(fileInfo);\n const metaBytes = new TextEncoder().encode(metaJson);\n const metaIv = generateIV();\n const encryptedMeta = await encrypt(metaBytes, cryptoKey, metaIv);\n\n // Prepare upload metadata\n const uploadMeta: UploadMeta = {\n salt: base64Encode(salt),\n iv: base64Encode(iv),\n encryptedMeta: base64Encode(new Uint8Array([...metaIv, ...encryptedMeta])),\n };\n\n // Build headers\n const headers: Record<string, string> = {\n 'Content-Type': 'application/octet-stream',\n 'X-Mux-Meta': btoa(JSON.stringify(uploadMeta)),\n };\n\n // Add expiration header if specified (convert to ISO 8601)\n if (options.expiresAt !== undefined) {\n let expiresDate: Date;\n if (options.expiresAt instanceof Date) {\n expiresDate = options.expiresAt;\n } else if (typeof options.expiresAt === 'string') {\n expiresDate = new Date(options.expiresAt);\n } else {\n expiresDate = new Date(options.expiresAt);\n }\n headers['X-Mux-Expires'] = expiresDate.toISOString();\n }\n\n // Upload to server\n const response = await fetch(`${baseUrl}/`, {\n method: 'POST',\n headers,\n body: payload.buffer as ArrayBuffer,\n });\n\n if (!response.ok) {\n const error = await response\n .json()\n .catch(() => ({ error: 'Upload failed' }));\n throw new Error((error as { error: string }).error || 'Upload failed');\n }\n\n const result: UploadResponse = await response.json();\n\n return {\n url: `${baseUrl}/${result.id}#${keyMaterial}`,\n id: result.id,\n key: keyMaterial,\n mutateKey: result.mutateKey,\n ...(result.expiresAt && { expiresAt: result.expiresAt }),\n };\n}\n\n/**\n * Download and decrypt a file from mux.md\n *\n * @param url - Full URL with encryption key in fragment, or just the ID\n * @param key - Encryption key (required if url doesn't contain fragment)\n * @param options - Download options\n * @returns Decrypted file data and metadata\n */\nexport async function download(\n url: string,\n key?: string,\n options: { baseUrl?: string } = {},\n): Promise<DownloadResult> {\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;\n\n // Parse URL to extract ID and key\n let id: string;\n let keyMaterial: string;\n\n if (url.includes('#')) {\n // Full URL with fragment\n const urlObj = new URL(url);\n id = urlObj.pathname.slice(1); // Remove leading /\n keyMaterial = urlObj.hash.slice(1); // Remove leading #\n } else if (url.includes('/')) {\n // URL path without fragment\n const parts = url.split('/');\n id = parts[parts.length - 1];\n if (!key) throw new Error('Key required when URL has no fragment');\n keyMaterial = key;\n } else {\n // Just the ID\n id = url;\n if (!key) throw new Error('Key required when only ID is provided');\n keyMaterial = key;\n }\n\n // Download encrypted file\n const response = await fetch(`${baseUrl}/${id}`);\n\n if (!response.ok) {\n const error = await response\n .json()\n .catch(() => ({ error: 'Download failed' }));\n throw new Error((error as { error: string }).error || 'Download failed');\n }\n\n // Parse metadata header\n const metaHeader = response.headers.get('X-Mux-Meta');\n if (!metaHeader) {\n throw new Error('Missing metadata header');\n }\n\n const uploadMeta: UploadMeta = JSON.parse(atob(metaHeader));\n\n // Get encrypted data\n const encryptedData = new Uint8Array(await response.arrayBuffer());\n\n // Derive decryption key\n const salt = base64Decode(uploadMeta.salt);\n const iv = base64Decode(uploadMeta.iv);\n const cryptoKey = await deriveKey(keyMaterial, salt);\n\n // Decrypt metadata (same for both formats)\n const encryptedMetaWithIv = base64Decode(uploadMeta.encryptedMeta);\n const metaIv = encryptedMetaWithIv.slice(0, 12);\n const encryptedMetaData = encryptedMetaWithIv.slice(12);\n const metaBytes = await decrypt(encryptedMetaData, cryptoKey, metaIv);\n const info: FileInfo = JSON.parse(new TextDecoder().decode(metaBytes));\n\n // Decrypt the payload\n const decrypted = await decrypt(encryptedData, cryptoKey, iv);\n\n // Check if decrypted content is signed (JSON with content + sig fields)\n if (decrypted[0] === 0x7b) {\n // '{' character - might be signed JSON\n try {\n const jsonStr = new TextDecoder().decode(decrypted);\n const parsed = JSON.parse(jsonStr);\n\n if (typeof parsed.content === 'string' && parsed.sig) {\n // Signed format\n const data = new TextEncoder().encode(parsed.content);\n const signature: SignatureEnvelope = parsed.sig;\n return { data, info, signature };\n }\n } catch {\n // Not valid JSON - treat as raw content\n }\n }\n\n // Unsigned content (raw bytes)\n return { data: decrypted, info };\n}\n\n/**\n * Get file metadata without downloading the full file\n *\n * @param url - Full URL or ID\n * @param key - Encryption key (required to decrypt metadata)\n * @param options - Request options\n * @returns Decrypted file info and server metadata\n */\nexport async function getMeta(\n url: string,\n key?: string,\n options: { baseUrl?: string } = {},\n): Promise<{ info: FileInfo; size: number }> {\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;\n\n // Parse URL to extract ID and key\n let id: string;\n let keyMaterial: string;\n\n if (url.includes('#')) {\n const urlObj = new URL(url);\n id = urlObj.pathname.slice(1);\n keyMaterial = urlObj.hash.slice(1);\n } else if (url.includes('/')) {\n const parts = url.split('/');\n id = parts[parts.length - 1];\n if (!key) throw new Error('Key required when URL has no fragment');\n keyMaterial = key;\n } else {\n id = url;\n if (!key) throw new Error('Key required when only ID is provided');\n keyMaterial = key;\n }\n\n // Fetch metadata\n const response = await fetch(`${baseUrl}/${id}/meta`);\n\n if (!response.ok) {\n const error = await response\n .json()\n .catch(() => ({ error: 'Request failed' }));\n throw new Error((error as { error: string }).error || 'Request failed');\n }\n\n const meta: MetaResponse = await response.json();\n\n // Derive key and decrypt metadata\n const salt = base64Decode(meta.salt);\n const cryptoKey = await deriveKey(keyMaterial, salt);\n\n const encryptedMetaWithIv = base64Decode(meta.encryptedMeta);\n const metaIv = encryptedMetaWithIv.slice(0, 12);\n const encryptedMetaData = encryptedMetaWithIv.slice(12);\n const metaBytes = await decrypt(encryptedMetaData, cryptoKey, metaIv);\n const info: FileInfo = JSON.parse(new TextDecoder().decode(metaBytes));\n\n return {\n info,\n size: meta.size,\n };\n}\n\n/**\n * Parse a mux.md URL into its components\n */\nexport function parseUrl(url: string): { id: string; key: string } | null {\n try {\n const urlObj = new URL(url);\n if (!urlObj.hash) return null;\n\n const id = urlObj.pathname.slice(1);\n const key = urlObj.hash.slice(1);\n\n if (!id || !key) return null;\n\n return { id, key };\n } catch {\n return null;\n }\n}\n\n/**\n * Build a mux.md URL from components\n */\nexport function buildUrl(\n id: string,\n key: string,\n baseUrl = DEFAULT_BASE_URL,\n): string {\n return `${baseUrl}/${id}#${key}`;\n}\n\n/**\n * Delete a file from mux.md using its mutate key\n *\n * @param id - File ID\n * @param mutateKey - Mutate key returned from upload\n * @param options - Request options\n */\nexport async function deleteFile(\n id: string,\n mutateKey: string,\n options: { baseUrl?: string } = {},\n): Promise<void> {\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;\n\n const response = await fetch(`${baseUrl}/${id}`, {\n method: 'DELETE',\n headers: {\n 'X-Mux-Mutate-Key': mutateKey,\n },\n });\n\n if (!response.ok) {\n const error = await response\n .json()\n .catch(() => ({ error: 'Delete failed' }));\n throw new Error((error as { error: string }).error || 'Delete failed');\n }\n}\n\nexport interface SetExpirationOptions {\n /** Base URL of the mux.md service */\n baseUrl?: string;\n}\n\nexport interface SetExpirationResult {\n /** Whether the operation succeeded */\n success: boolean;\n /** File ID */\n id: string;\n /** New expiration timestamp (ms), or undefined if expiration was removed */\n expiresAt?: number;\n}\n\n/**\n * Set or remove the expiration of a file using its mutate key\n *\n * @param id - File ID\n * @param mutateKey - Mutate key returned from upload\n * @param expiresAt - New expiration time (unix timestamp ms, ISO date string, Date object, or \"never\" to remove expiration)\n * @param options - Request options\n * @returns Result with new expiration info\n */\nexport async function setExpiration(\n id: string,\n mutateKey: string,\n expiresAt: number | string | Date | 'never',\n options: SetExpirationOptions = {},\n): Promise<SetExpirationResult> {\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;\n\n // Convert expiration to ISO 8601 string\n let expiresHeader: string;\n if (expiresAt === 'never') {\n expiresHeader = 'never';\n } else if (expiresAt instanceof Date) {\n expiresHeader = expiresAt.toISOString();\n } else if (typeof expiresAt === 'string') {\n expiresHeader = new Date(expiresAt).toISOString();\n } else {\n expiresHeader = new Date(expiresAt).toISOString();\n }\n\n const response = await fetch(`${baseUrl}/${id}`, {\n method: 'PATCH',\n headers: {\n 'X-Mux-Mutate-Key': mutateKey,\n 'X-Mux-Expires': expiresHeader,\n },\n });\n\n if (!response.ok) {\n const error = await response\n .json()\n .catch(() => ({ error: 'Set expiration failed' }));\n throw new Error(\n (error as { error: string }).error || 'Set expiration failed',\n );\n }\n\n return response.json();\n}\n","/**\n * Signing and verification utilities for mux.md\n *\n * Supports Ed25519 and ECDSA (P-256, P-384, P-521) keys in SSH format.\n */\n\nimport * as ed from '@noble/ed25519';\nimport { p256, p384, p521 } from '@noble/curves/nist.js';\nimport { sha512 } from '@noble/hashes/sha2.js';\nimport type { SignatureEnvelope } from './types';\n\n// Configure @noble/ed25519 to use sha512 from @noble/hashes\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\n(ed.etc as any).sha512Sync = (...m: Uint8Array[]) =>\n sha512(ed.etc.concatBytes(...m));\n\n// ----- Types -----\n\n/** Supported key types */\nexport type KeyType = 'ed25519' | 'ecdsa-p256' | 'ecdsa-p384' | 'ecdsa-p521';\n\n/** Parsed public key with type and raw bytes */\nexport interface ParsedPublicKey {\n type: KeyType;\n keyBytes: Uint8Array;\n}\n\n// SSH key type identifiers\nconst SSH_KEY_TYPES: Record<string, KeyType> = {\n 'ssh-ed25519': 'ed25519',\n 'ecdsa-sha2-nistp256': 'ecdsa-p256',\n 'ecdsa-sha2-nistp384': 'ecdsa-p384',\n 'ecdsa-sha2-nistp521': 'ecdsa-p521',\n};\n\n// ----- SSH Key Parsing -----\n\n/**\n * Read a length-prefixed string from SSH wire format\n */\nfunction readSSHString(\n data: Uint8Array,\n offset: number,\n): { value: Uint8Array; nextOffset: number } {\n const view = new DataView(data.buffer, data.byteOffset);\n const len = view.getUint32(offset);\n const value = data.slice(offset + 4, offset + 4 + len);\n return { value, nextOffset: offset + 4 + len };\n}\n\n/**\n * Decode standard base64 string to Uint8Array\n */\nfunction base64Decode(str: string): Uint8Array {\n // Handle base64url as well\n let base64 = str.replace(/-/g, '+').replace(/_/g, '/');\n while (base64.length % 4) {\n base64 += '=';\n }\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\n/**\n * Parse an SSH public key and extract the key bytes and type.\n * Supports formats:\n * - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... [comment]\n * - ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY... [comment]\n * - ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQ... [comment]\n * - ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjE... [comment]\n * - Raw base64 (32 bytes when decoded = Ed25519)\n */\nexport function parsePublicKey(keyString: string): ParsedPublicKey {\n const trimmed = keyString.trim();\n\n // Check for SSH format by looking for known key type prefixes\n for (const [sshType, keyType] of Object.entries(SSH_KEY_TYPES)) {\n if (trimmed.startsWith(`${sshType} `)) {\n const parts = trimmed.split(' ');\n if (parts.length < 2) {\n throw new Error('Invalid SSH key format');\n }\n const keyData = base64Decode(parts[1]);\n\n // SSH key format: [4 byte len][type string][4 byte len][key data...]\n // For ECDSA: [4 byte len][type][4 byte len][curve name][4 byte len][point]\n const { value: typeBytes, nextOffset: afterType } = readSSHString(\n keyData,\n 0,\n );\n const typeStr = new TextDecoder().decode(typeBytes);\n\n if (typeStr !== sshType) {\n throw new Error(\n `Key type mismatch: expected ${sshType}, got ${typeStr}`,\n );\n }\n\n if (keyType === 'ed25519') {\n // Ed25519: [type][32 byte key]\n const { value: rawKey } = readSSHString(keyData, afterType);\n if (rawKey.length !== 32) {\n throw new Error('Invalid Ed25519 key length');\n }\n return { type: 'ed25519', keyBytes: rawKey };\n }\n // ECDSA: [type][curve name][point]\n const { nextOffset: afterCurve } = readSSHString(keyData, afterType);\n const { value: point } = readSSHString(keyData, afterCurve);\n return { type: keyType, keyBytes: point };\n }\n }\n\n // Try raw base64\n const decoded = base64Decode(trimmed);\n if (decoded.length === 32) {\n return { type: 'ed25519', keyBytes: decoded };\n }\n\n throw new Error('Unsupported public key format');\n}\n\n// ----- Signing -----\n\n/**\n * Sign a message with Ed25519 private key.\n * @param message - The message bytes to sign\n * @param privateKey - 32-byte Ed25519 private key\n * @returns Base64-encoded signature (64 bytes)\n */\nexport async function signEd25519(\n message: Uint8Array,\n privateKey: Uint8Array,\n): Promise<string> {\n const sig = await ed.signAsync(message, privateKey);\n return btoa(String.fromCharCode(...sig));\n}\n\n/**\n * Sign a message with ECDSA private key (P-256/384/521).\n * @param message - The message bytes to sign (will be hashed)\n * @param privateKey - ECDSA private key bytes\n * @param curve - Which curve to use\n * @returns Base64-encoded signature\n */\nexport function signECDSA(\n message: Uint8Array,\n privateKey: Uint8Array,\n curve: 'p256' | 'p384' | 'p521',\n): string {\n const curves = { p256, p384, p521 };\n const sig = curves[curve].sign(message, privateKey, { prehash: true });\n // sig is a Signature object with toCompactRawBytes() method\n const sigBytes = (\n sig as unknown as { toCompactRawBytes: () => Uint8Array }\n ).toCompactRawBytes();\n return btoa(String.fromCharCode(...sigBytes));\n}\n\n/**\n * Helper: Create a SignatureEnvelope from content + private key.\n * This is the high-level API for signing before upload.\n *\n * @param content - The content bytes to sign\n * @param privateKey - Private key bytes (32 bytes for Ed25519, variable for ECDSA)\n * @param publicKey - SSH format public key string (e.g., \"ssh-ed25519 AAAA...\")\n * @param options - Optional email or GitHub username for attribution\n * @returns SignatureEnvelope ready for upload\n */\nexport async function createSignatureEnvelope(\n content: Uint8Array,\n privateKey: Uint8Array,\n publicKey: string,\n options?: { email?: string; githubUser?: string },\n): Promise<SignatureEnvelope> {\n const parsed = parsePublicKey(publicKey);\n let sig: string;\n\n if (parsed.type === 'ed25519') {\n sig = await signEd25519(content, privateKey);\n } else {\n const curve = parsed.type.replace('ecdsa-', '') as 'p256' | 'p384' | 'p521';\n sig = signECDSA(content, privateKey, curve);\n }\n\n return {\n sig,\n publicKey,\n email: options?.email,\n githubUser: options?.githubUser,\n };\n}\n\n// ----- Verification -----\n\n/**\n * Verify a signature using the appropriate algorithm based on key type.\n * For Ed25519: signature is raw 64 bytes\n * For ECDSA: signature is DER-encoded or raw r||s format\n *\n * @param parsedKey - Parsed public key (from parsePublicKey)\n * @param message - Original message bytes\n * @param signature - Signature bytes (not base64)\n * @returns true if signature is valid\n */\nexport async function verifySignature(\n parsedKey: ParsedPublicKey,\n message: Uint8Array,\n signature: Uint8Array,\n): Promise<boolean> {\n try {\n switch (parsedKey.type) {\n case 'ed25519':\n return await ed.verifyAsync(signature, message, parsedKey.keyBytes);\n\n case 'ecdsa-p256':\n return p256.verify(signature, message, parsedKey.keyBytes, {\n prehash: true,\n });\n\n case 'ecdsa-p384':\n return p384.verify(signature, message, parsedKey.keyBytes, {\n prehash: true,\n });\n\n case 'ecdsa-p521':\n return p521.verify(signature, message, parsedKey.keyBytes, {\n prehash: true,\n });\n\n default:\n return false;\n }\n } catch {\n return false;\n }\n}\n\n// ----- Fingerprint -----\n\n/**\n * Compute SHA256 fingerprint of a public key (matches ssh-keygen -l format)\n * @param publicKey - Raw public key bytes\n * @returns Fingerprint string like \"SHA256:abc123...\"\n */\nexport async function computeFingerprint(\n publicKey: Uint8Array,\n): Promise<string> {\n const hash = await crypto.subtle.digest(\n 'SHA-256',\n publicKey.buffer as ArrayBuffer,\n );\n const hashArray = new Uint8Array(hash);\n const base64 = btoa(String.fromCharCode(...hashArray));\n return `SHA256:${base64.replace(/=+$/, '')}`;\n}\n\n/**\n * Format a fingerprint for nice display.\n * Converts base64 fingerprint to uppercase hex groups like \"DEAD BEEF 1234 5678\"\n */\nexport function formatFingerprint(fingerprint: string): string {\n // Remove \"SHA256:\" prefix if present\n const base64Part = fingerprint.startsWith('SHA256:')\n ? fingerprint.slice(7)\n : fingerprint;\n\n // Decode base64 to bytes, then to hex\n try {\n const binary = atob(base64Part);\n const hex = Array.from(binary)\n .map((c) => c.charCodeAt(0).toString(16).padStart(2, '0'))\n .join('')\n .toUpperCase();\n\n // Take first 16 chars (8 bytes) and format as \"DEAD BEEF 1234 5678\"\n const short = hex.slice(0, 16);\n return short.match(/.{4}/g)?.join(' ') || short;\n } catch {\n // Fallback: just show first part of the fingerprint\n return fingerprint.slice(0, 16).toUpperCase();\n }\n}\n"],"mappings":"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","names":["base64Decode"]}
@@ -0,0 +1,283 @@
1
+ /**
2
+ * File info encrypted client-side (never seen by server in plaintext)
3
+ */
4
+ interface FileInfo {
5
+ name: string;
6
+ type: string;
7
+ size: number;
8
+ model?: string;
9
+ thinking?: string;
10
+ }
11
+ /**
12
+ * Metadata header sent with upload (X-Mux-Meta)
13
+ * Also used in response header with createdAt added
14
+ */
15
+ interface UploadMeta {
16
+ salt: string;
17
+ iv: string;
18
+ encryptedMeta: string;
19
+ createdAt?: string;
20
+ expiresAt?: string;
21
+ }
22
+ /**
23
+ * Signature envelope - contains all signature-related data.
24
+ */
25
+ interface SignatureEnvelope {
26
+ sig: string;
27
+ publicKey: string;
28
+ githubUser?: string;
29
+ email?: string;
30
+ }
31
+ /**
32
+ * Signed content payload (decrypted).
33
+ * When a signature is present, the decrypted blob is JSON with this structure.
34
+ * Legacy (unsigned) content decrypts directly to the raw content bytes.
35
+ */
36
+ interface SignedPayload {
37
+ content: string;
38
+ sig: SignatureEnvelope;
39
+ }
40
+
41
+ /**
42
+ * mux.md Client Library
43
+ *
44
+ * Reference implementation for encrypting, uploading, downloading, and decrypting files.
45
+ * Works in both Node.js (with webcrypto) and browser environments.
46
+ */
47
+
48
+ interface UploadOptions {
49
+ /** Base URL of the mux.md service */
50
+ baseUrl?: string;
51
+ /** Expiration time (unix timestamp ms, ISO date string, or Date object) */
52
+ expiresAt?: number | string | Date;
53
+ /** Signature envelope (will be encrypted with content) */
54
+ signature?: SignatureEnvelope;
55
+ }
56
+ interface UploadResult {
57
+ /** Full URL with encryption key in fragment */
58
+ url: string;
59
+ /** File ID (without key) */
60
+ id: string;
61
+ /** Encryption key (base64url) */
62
+ key: string;
63
+ /** Mutate key (base64url) - store this to mutate (delete, change expiration) the file later */
64
+ mutateKey: string;
65
+ /** Expiration timestamp (ms), if set */
66
+ expiresAt?: number;
67
+ }
68
+ interface DownloadResult {
69
+ /** Decrypted file content */
70
+ data: Uint8Array;
71
+ /** Original file info (name, type, size) */
72
+ info: FileInfo;
73
+ /** Decrypted signature envelope (if present) */
74
+ signature?: SignatureEnvelope;
75
+ }
76
+ /**
77
+ * Encrypt and upload a file to mux.md
78
+ *
79
+ * @param data - File contents as Uint8Array
80
+ * @param fileInfo - Original file metadata (name, type, size)
81
+ * @param options - Upload options (including optional signature)
82
+ * @returns Upload result with URL containing encryption key
83
+ */
84
+ declare function upload(data: Uint8Array, fileInfo: FileInfo, options?: UploadOptions): Promise<UploadResult>;
85
+ /**
86
+ * Download and decrypt a file from mux.md
87
+ *
88
+ * @param url - Full URL with encryption key in fragment, or just the ID
89
+ * @param key - Encryption key (required if url doesn't contain fragment)
90
+ * @param options - Download options
91
+ * @returns Decrypted file data and metadata
92
+ */
93
+ declare function download(url: string, key?: string, options?: {
94
+ baseUrl?: string;
95
+ }): Promise<DownloadResult>;
96
+ /**
97
+ * Get file metadata without downloading the full file
98
+ *
99
+ * @param url - Full URL or ID
100
+ * @param key - Encryption key (required to decrypt metadata)
101
+ * @param options - Request options
102
+ * @returns Decrypted file info and server metadata
103
+ */
104
+ declare function getMeta(url: string, key?: string, options?: {
105
+ baseUrl?: string;
106
+ }): Promise<{
107
+ info: FileInfo;
108
+ size: number;
109
+ }>;
110
+ /**
111
+ * Parse a mux.md URL into its components
112
+ */
113
+ declare function parseUrl(url: string): {
114
+ id: string;
115
+ key: string;
116
+ } | null;
117
+ /**
118
+ * Build a mux.md URL from components
119
+ */
120
+ declare function buildUrl(id: string, key: string, baseUrl?: string): string;
121
+ /**
122
+ * Delete a file from mux.md using its mutate key
123
+ *
124
+ * @param id - File ID
125
+ * @param mutateKey - Mutate key returned from upload
126
+ * @param options - Request options
127
+ */
128
+ declare function deleteFile(id: string, mutateKey: string, options?: {
129
+ baseUrl?: string;
130
+ }): Promise<void>;
131
+ interface SetExpirationOptions {
132
+ /** Base URL of the mux.md service */
133
+ baseUrl?: string;
134
+ }
135
+ interface SetExpirationResult {
136
+ /** Whether the operation succeeded */
137
+ success: boolean;
138
+ /** File ID */
139
+ id: string;
140
+ /** New expiration timestamp (ms), or undefined if expiration was removed */
141
+ expiresAt?: number;
142
+ }
143
+ /**
144
+ * Set or remove the expiration of a file using its mutate key
145
+ *
146
+ * @param id - File ID
147
+ * @param mutateKey - Mutate key returned from upload
148
+ * @param expiresAt - New expiration time (unix timestamp ms, ISO date string, Date object, or "never" to remove expiration)
149
+ * @param options - Request options
150
+ * @returns Result with new expiration info
151
+ */
152
+ declare function setExpiration(id: string, mutateKey: string, expiresAt: number | string | Date | 'never', options?: SetExpirationOptions): Promise<SetExpirationResult>;
153
+
154
+ /**
155
+ * Signing and verification utilities for mux.md
156
+ *
157
+ * Supports Ed25519 and ECDSA (P-256, P-384, P-521) keys in SSH format.
158
+ */
159
+
160
+ /** Supported key types */
161
+ type KeyType = 'ed25519' | 'ecdsa-p256' | 'ecdsa-p384' | 'ecdsa-p521';
162
+ /** Parsed public key with type and raw bytes */
163
+ interface ParsedPublicKey {
164
+ type: KeyType;
165
+ keyBytes: Uint8Array;
166
+ }
167
+ /**
168
+ * Parse an SSH public key and extract the key bytes and type.
169
+ * Supports formats:
170
+ * - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... [comment]
171
+ * - ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY... [comment]
172
+ * - ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQ... [comment]
173
+ * - ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjE... [comment]
174
+ * - Raw base64 (32 bytes when decoded = Ed25519)
175
+ */
176
+ declare function parsePublicKey(keyString: string): ParsedPublicKey;
177
+ /**
178
+ * Sign a message with Ed25519 private key.
179
+ * @param message - The message bytes to sign
180
+ * @param privateKey - 32-byte Ed25519 private key
181
+ * @returns Base64-encoded signature (64 bytes)
182
+ */
183
+ declare function signEd25519(message: Uint8Array, privateKey: Uint8Array): Promise<string>;
184
+ /**
185
+ * Sign a message with ECDSA private key (P-256/384/521).
186
+ * @param message - The message bytes to sign (will be hashed)
187
+ * @param privateKey - ECDSA private key bytes
188
+ * @param curve - Which curve to use
189
+ * @returns Base64-encoded signature
190
+ */
191
+ declare function signECDSA(message: Uint8Array, privateKey: Uint8Array, curve: 'p256' | 'p384' | 'p521'): string;
192
+ /**
193
+ * Helper: Create a SignatureEnvelope from content + private key.
194
+ * This is the high-level API for signing before upload.
195
+ *
196
+ * @param content - The content bytes to sign
197
+ * @param privateKey - Private key bytes (32 bytes for Ed25519, variable for ECDSA)
198
+ * @param publicKey - SSH format public key string (e.g., "ssh-ed25519 AAAA...")
199
+ * @param options - Optional email or GitHub username for attribution
200
+ * @returns SignatureEnvelope ready for upload
201
+ */
202
+ declare function createSignatureEnvelope(content: Uint8Array, privateKey: Uint8Array, publicKey: string, options?: {
203
+ email?: string;
204
+ githubUser?: string;
205
+ }): Promise<SignatureEnvelope>;
206
+ /**
207
+ * Verify a signature using the appropriate algorithm based on key type.
208
+ * For Ed25519: signature is raw 64 bytes
209
+ * For ECDSA: signature is DER-encoded or raw r||s format
210
+ *
211
+ * @param parsedKey - Parsed public key (from parsePublicKey)
212
+ * @param message - Original message bytes
213
+ * @param signature - Signature bytes (not base64)
214
+ * @returns true if signature is valid
215
+ */
216
+ declare function verifySignature(parsedKey: ParsedPublicKey, message: Uint8Array, signature: Uint8Array): Promise<boolean>;
217
+ /**
218
+ * Compute SHA256 fingerprint of a public key (matches ssh-keygen -l format)
219
+ * @param publicKey - Raw public key bytes
220
+ * @returns Fingerprint string like "SHA256:abc123..."
221
+ */
222
+ declare function computeFingerprint(publicKey: Uint8Array): Promise<string>;
223
+ /**
224
+ * Format a fingerprint for nice display.
225
+ * Converts base64 fingerprint to uppercase hex groups like "DEAD BEEF 1234 5678"
226
+ */
227
+ declare function formatFingerprint(fingerprint: string): string;
228
+
229
+ /**
230
+ * Generate a random file ID (30 bits, 5 chars base62)
231
+ */
232
+ declare function generateId(): string;
233
+ /**
234
+ * Generate encryption key material (80 bits, 14 chars base64url)
235
+ */
236
+ declare function generateKey(): string;
237
+ /**
238
+ * Generate mutate key (128 bits, 22 chars base64url)
239
+ * Used for mutation operations: delete, set expiration
240
+ */
241
+ declare function generateMutateKey(): string;
242
+ /**
243
+ * Generate random salt for HKDF (128 bits)
244
+ */
245
+ declare function generateSalt(): Uint8Array;
246
+ /**
247
+ * Generate random IV for AES-GCM (96 bits)
248
+ */
249
+ declare function generateIV(): Uint8Array;
250
+ /**
251
+ * Derive AES-256 key from key material using HKDF
252
+ *
253
+ * HKDF is the correct choice for deriving keys from high-entropy
254
+ * random key material. Unlike PBKDF2, it doesn't need iterations
255
+ * since we're not stretching a weak password.
256
+ */
257
+ declare function deriveKey(keyMaterial: string, salt: Uint8Array): Promise<CryptoKey>;
258
+ /**
259
+ * Encrypt data using AES-256-GCM
260
+ */
261
+ declare function encrypt(data: Uint8Array, key: CryptoKey, iv: Uint8Array): Promise<Uint8Array>;
262
+ /**
263
+ * Decrypt data using AES-256-GCM
264
+ */
265
+ declare function decrypt(ciphertext: Uint8Array, key: CryptoKey, iv: Uint8Array): Promise<Uint8Array>;
266
+ /**
267
+ * Base64url encode (URL-safe, no padding)
268
+ */
269
+ declare function base64UrlEncode(data: Uint8Array): string;
270
+ /**
271
+ * Base64url decode
272
+ */
273
+ declare function base64UrlDecode(str: string): Uint8Array;
274
+ /**
275
+ * Standard base64 encode
276
+ */
277
+ declare function base64Encode(data: Uint8Array): string;
278
+ /**
279
+ * Standard base64 decode
280
+ */
281
+ declare function base64Decode(str: string): Uint8Array;
282
+
283
+ export { type DownloadResult, type FileInfo, type KeyType, type ParsedPublicKey, type SetExpirationOptions, type SetExpirationResult, type SignatureEnvelope, type SignedPayload, type UploadMeta, type UploadOptions, type UploadResult, base64Decode, base64Encode, base64UrlDecode, base64UrlEncode, buildUrl, computeFingerprint, createSignatureEnvelope, decrypt, deleteFile, deriveKey, download, encrypt, formatFingerprint, generateIV, generateId, generateKey, generateMutateKey, generateSalt, getMeta, parsePublicKey, parseUrl, setExpiration, signECDSA, signEd25519, upload, verifySignature };
@@ -0,0 +1,283 @@
1
+ /**
2
+ * File info encrypted client-side (never seen by server in plaintext)
3
+ */
4
+ interface FileInfo {
5
+ name: string;
6
+ type: string;
7
+ size: number;
8
+ model?: string;
9
+ thinking?: string;
10
+ }
11
+ /**
12
+ * Metadata header sent with upload (X-Mux-Meta)
13
+ * Also used in response header with createdAt added
14
+ */
15
+ interface UploadMeta {
16
+ salt: string;
17
+ iv: string;
18
+ encryptedMeta: string;
19
+ createdAt?: string;
20
+ expiresAt?: string;
21
+ }
22
+ /**
23
+ * Signature envelope - contains all signature-related data.
24
+ */
25
+ interface SignatureEnvelope {
26
+ sig: string;
27
+ publicKey: string;
28
+ githubUser?: string;
29
+ email?: string;
30
+ }
31
+ /**
32
+ * Signed content payload (decrypted).
33
+ * When a signature is present, the decrypted blob is JSON with this structure.
34
+ * Legacy (unsigned) content decrypts directly to the raw content bytes.
35
+ */
36
+ interface SignedPayload {
37
+ content: string;
38
+ sig: SignatureEnvelope;
39
+ }
40
+
41
+ /**
42
+ * mux.md Client Library
43
+ *
44
+ * Reference implementation for encrypting, uploading, downloading, and decrypting files.
45
+ * Works in both Node.js (with webcrypto) and browser environments.
46
+ */
47
+
48
+ interface UploadOptions {
49
+ /** Base URL of the mux.md service */
50
+ baseUrl?: string;
51
+ /** Expiration time (unix timestamp ms, ISO date string, or Date object) */
52
+ expiresAt?: number | string | Date;
53
+ /** Signature envelope (will be encrypted with content) */
54
+ signature?: SignatureEnvelope;
55
+ }
56
+ interface UploadResult {
57
+ /** Full URL with encryption key in fragment */
58
+ url: string;
59
+ /** File ID (without key) */
60
+ id: string;
61
+ /** Encryption key (base64url) */
62
+ key: string;
63
+ /** Mutate key (base64url) - store this to mutate (delete, change expiration) the file later */
64
+ mutateKey: string;
65
+ /** Expiration timestamp (ms), if set */
66
+ expiresAt?: number;
67
+ }
68
+ interface DownloadResult {
69
+ /** Decrypted file content */
70
+ data: Uint8Array;
71
+ /** Original file info (name, type, size) */
72
+ info: FileInfo;
73
+ /** Decrypted signature envelope (if present) */
74
+ signature?: SignatureEnvelope;
75
+ }
76
+ /**
77
+ * Encrypt and upload a file to mux.md
78
+ *
79
+ * @param data - File contents as Uint8Array
80
+ * @param fileInfo - Original file metadata (name, type, size)
81
+ * @param options - Upload options (including optional signature)
82
+ * @returns Upload result with URL containing encryption key
83
+ */
84
+ declare function upload(data: Uint8Array, fileInfo: FileInfo, options?: UploadOptions): Promise<UploadResult>;
85
+ /**
86
+ * Download and decrypt a file from mux.md
87
+ *
88
+ * @param url - Full URL with encryption key in fragment, or just the ID
89
+ * @param key - Encryption key (required if url doesn't contain fragment)
90
+ * @param options - Download options
91
+ * @returns Decrypted file data and metadata
92
+ */
93
+ declare function download(url: string, key?: string, options?: {
94
+ baseUrl?: string;
95
+ }): Promise<DownloadResult>;
96
+ /**
97
+ * Get file metadata without downloading the full file
98
+ *
99
+ * @param url - Full URL or ID
100
+ * @param key - Encryption key (required to decrypt metadata)
101
+ * @param options - Request options
102
+ * @returns Decrypted file info and server metadata
103
+ */
104
+ declare function getMeta(url: string, key?: string, options?: {
105
+ baseUrl?: string;
106
+ }): Promise<{
107
+ info: FileInfo;
108
+ size: number;
109
+ }>;
110
+ /**
111
+ * Parse a mux.md URL into its components
112
+ */
113
+ declare function parseUrl(url: string): {
114
+ id: string;
115
+ key: string;
116
+ } | null;
117
+ /**
118
+ * Build a mux.md URL from components
119
+ */
120
+ declare function buildUrl(id: string, key: string, baseUrl?: string): string;
121
+ /**
122
+ * Delete a file from mux.md using its mutate key
123
+ *
124
+ * @param id - File ID
125
+ * @param mutateKey - Mutate key returned from upload
126
+ * @param options - Request options
127
+ */
128
+ declare function deleteFile(id: string, mutateKey: string, options?: {
129
+ baseUrl?: string;
130
+ }): Promise<void>;
131
+ interface SetExpirationOptions {
132
+ /** Base URL of the mux.md service */
133
+ baseUrl?: string;
134
+ }
135
+ interface SetExpirationResult {
136
+ /** Whether the operation succeeded */
137
+ success: boolean;
138
+ /** File ID */
139
+ id: string;
140
+ /** New expiration timestamp (ms), or undefined if expiration was removed */
141
+ expiresAt?: number;
142
+ }
143
+ /**
144
+ * Set or remove the expiration of a file using its mutate key
145
+ *
146
+ * @param id - File ID
147
+ * @param mutateKey - Mutate key returned from upload
148
+ * @param expiresAt - New expiration time (unix timestamp ms, ISO date string, Date object, or "never" to remove expiration)
149
+ * @param options - Request options
150
+ * @returns Result with new expiration info
151
+ */
152
+ declare function setExpiration(id: string, mutateKey: string, expiresAt: number | string | Date | 'never', options?: SetExpirationOptions): Promise<SetExpirationResult>;
153
+
154
+ /**
155
+ * Signing and verification utilities for mux.md
156
+ *
157
+ * Supports Ed25519 and ECDSA (P-256, P-384, P-521) keys in SSH format.
158
+ */
159
+
160
+ /** Supported key types */
161
+ type KeyType = 'ed25519' | 'ecdsa-p256' | 'ecdsa-p384' | 'ecdsa-p521';
162
+ /** Parsed public key with type and raw bytes */
163
+ interface ParsedPublicKey {
164
+ type: KeyType;
165
+ keyBytes: Uint8Array;
166
+ }
167
+ /**
168
+ * Parse an SSH public key and extract the key bytes and type.
169
+ * Supports formats:
170
+ * - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... [comment]
171
+ * - ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY... [comment]
172
+ * - ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQ... [comment]
173
+ * - ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjE... [comment]
174
+ * - Raw base64 (32 bytes when decoded = Ed25519)
175
+ */
176
+ declare function parsePublicKey(keyString: string): ParsedPublicKey;
177
+ /**
178
+ * Sign a message with Ed25519 private key.
179
+ * @param message - The message bytes to sign
180
+ * @param privateKey - 32-byte Ed25519 private key
181
+ * @returns Base64-encoded signature (64 bytes)
182
+ */
183
+ declare function signEd25519(message: Uint8Array, privateKey: Uint8Array): Promise<string>;
184
+ /**
185
+ * Sign a message with ECDSA private key (P-256/384/521).
186
+ * @param message - The message bytes to sign (will be hashed)
187
+ * @param privateKey - ECDSA private key bytes
188
+ * @param curve - Which curve to use
189
+ * @returns Base64-encoded signature
190
+ */
191
+ declare function signECDSA(message: Uint8Array, privateKey: Uint8Array, curve: 'p256' | 'p384' | 'p521'): string;
192
+ /**
193
+ * Helper: Create a SignatureEnvelope from content + private key.
194
+ * This is the high-level API for signing before upload.
195
+ *
196
+ * @param content - The content bytes to sign
197
+ * @param privateKey - Private key bytes (32 bytes for Ed25519, variable for ECDSA)
198
+ * @param publicKey - SSH format public key string (e.g., "ssh-ed25519 AAAA...")
199
+ * @param options - Optional email or GitHub username for attribution
200
+ * @returns SignatureEnvelope ready for upload
201
+ */
202
+ declare function createSignatureEnvelope(content: Uint8Array, privateKey: Uint8Array, publicKey: string, options?: {
203
+ email?: string;
204
+ githubUser?: string;
205
+ }): Promise<SignatureEnvelope>;
206
+ /**
207
+ * Verify a signature using the appropriate algorithm based on key type.
208
+ * For Ed25519: signature is raw 64 bytes
209
+ * For ECDSA: signature is DER-encoded or raw r||s format
210
+ *
211
+ * @param parsedKey - Parsed public key (from parsePublicKey)
212
+ * @param message - Original message bytes
213
+ * @param signature - Signature bytes (not base64)
214
+ * @returns true if signature is valid
215
+ */
216
+ declare function verifySignature(parsedKey: ParsedPublicKey, message: Uint8Array, signature: Uint8Array): Promise<boolean>;
217
+ /**
218
+ * Compute SHA256 fingerprint of a public key (matches ssh-keygen -l format)
219
+ * @param publicKey - Raw public key bytes
220
+ * @returns Fingerprint string like "SHA256:abc123..."
221
+ */
222
+ declare function computeFingerprint(publicKey: Uint8Array): Promise<string>;
223
+ /**
224
+ * Format a fingerprint for nice display.
225
+ * Converts base64 fingerprint to uppercase hex groups like "DEAD BEEF 1234 5678"
226
+ */
227
+ declare function formatFingerprint(fingerprint: string): string;
228
+
229
+ /**
230
+ * Generate a random file ID (30 bits, 5 chars base62)
231
+ */
232
+ declare function generateId(): string;
233
+ /**
234
+ * Generate encryption key material (80 bits, 14 chars base64url)
235
+ */
236
+ declare function generateKey(): string;
237
+ /**
238
+ * Generate mutate key (128 bits, 22 chars base64url)
239
+ * Used for mutation operations: delete, set expiration
240
+ */
241
+ declare function generateMutateKey(): string;
242
+ /**
243
+ * Generate random salt for HKDF (128 bits)
244
+ */
245
+ declare function generateSalt(): Uint8Array;
246
+ /**
247
+ * Generate random IV for AES-GCM (96 bits)
248
+ */
249
+ declare function generateIV(): Uint8Array;
250
+ /**
251
+ * Derive AES-256 key from key material using HKDF
252
+ *
253
+ * HKDF is the correct choice for deriving keys from high-entropy
254
+ * random key material. Unlike PBKDF2, it doesn't need iterations
255
+ * since we're not stretching a weak password.
256
+ */
257
+ declare function deriveKey(keyMaterial: string, salt: Uint8Array): Promise<CryptoKey>;
258
+ /**
259
+ * Encrypt data using AES-256-GCM
260
+ */
261
+ declare function encrypt(data: Uint8Array, key: CryptoKey, iv: Uint8Array): Promise<Uint8Array>;
262
+ /**
263
+ * Decrypt data using AES-256-GCM
264
+ */
265
+ declare function decrypt(ciphertext: Uint8Array, key: CryptoKey, iv: Uint8Array): Promise<Uint8Array>;
266
+ /**
267
+ * Base64url encode (URL-safe, no padding)
268
+ */
269
+ declare function base64UrlEncode(data: Uint8Array): string;
270
+ /**
271
+ * Base64url decode
272
+ */
273
+ declare function base64UrlDecode(str: string): Uint8Array;
274
+ /**
275
+ * Standard base64 encode
276
+ */
277
+ declare function base64Encode(data: Uint8Array): string;
278
+ /**
279
+ * Standard base64 decode
280
+ */
281
+ declare function base64Decode(str: string): Uint8Array;
282
+
283
+ export { type DownloadResult, type FileInfo, type KeyType, type ParsedPublicKey, type SetExpirationOptions, type SetExpirationResult, type SignatureEnvelope, type SignedPayload, type UploadMeta, type UploadOptions, type UploadResult, base64Decode, base64Encode, base64UrlDecode, base64UrlEncode, buildUrl, computeFingerprint, createSignatureEnvelope, decrypt, deleteFile, deriveKey, download, encrypt, formatFingerprint, generateIV, generateId, generateKey, generateMutateKey, generateSalt, getMeta, parsePublicKey, parseUrl, setExpiration, signECDSA, signEd25519, upload, verifySignature };