@coder/mux-md-client 0.1.0-main.17 → 0.1.0-main.23

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 CHANGED
@@ -51,17 +51,41 @@ if (signature) {
51
51
 
52
52
  ### Upload with signature
53
53
 
54
+ Signed uploads embed plaintext as JSON (`{ content: string, sig: SignatureEnvelope }`), so the
55
+ content must be valid UTF-8 text.
56
+
57
+ #### Option A: let the library create the signature
58
+
59
+ ```typescript
60
+ import { upload } from '@coder/mux-md-client';
61
+
62
+ const content = new TextEncoder().encode('# Signed Message');
63
+
64
+ const result = await upload(
65
+ content,
66
+ { name: 'signed.md', type: 'text/markdown', size: content.length },
67
+ {
68
+ sign: {
69
+ privateKey, // Uint8Array (Ed25519: 32 bytes)
70
+ publicKey: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...', // OpenSSH public key
71
+ githubUser: 'username', // optional attribution
72
+ },
73
+ }
74
+ );
75
+ ```
76
+
77
+ #### Option B: provide a precomputed SignatureEnvelope
78
+
54
79
  ```typescript
55
80
  import { upload, createSignatureEnvelope } from '@coder/mux-md-client';
56
81
 
57
82
  const content = new TextEncoder().encode('# Signed Message');
58
83
 
59
- // Create signature from your SSH key
60
84
  const signature = await createSignatureEnvelope(
61
85
  content,
62
- privateKey, // 32-byte Ed25519 private key
63
- 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...', // Your public key
64
- { email: 'user@example.com' }
86
+ privateKey,
87
+ 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...',
88
+ { githubUser: 'username' }
65
89
  );
66
90
 
67
91
  const result = await upload(
@@ -71,6 +95,27 @@ const result = await upload(
71
95
  );
72
96
  ```
73
97
 
98
+ #### Option C (Node.js): sign via SSH agent (e.g., 1Password)
99
+
100
+ ```typescript
101
+ import { upload } from '@coder/mux-md-client';
102
+ import { createSshAgentSigner } from '@coder/mux-md-client/ssh-agent';
103
+
104
+ const content = new TextEncoder().encode('# Signed Message');
105
+
106
+ const signer = await createSshAgentSigner({ githubUser: 'username' });
107
+
108
+ const result = await upload(
109
+ content,
110
+ { name: 'signed.md', type: 'text/markdown', size: content.length },
111
+ { sign: { signer } }
112
+ );
113
+ ```
114
+
115
+ For implementation details and rationale, see the plan that added SSH-agent (1Password) signing:
116
+
117
+ https://mux.md/dfIDZ#9UllGCQoi2V7NQ
118
+
74
119
  ### Verify signatures
75
120
 
76
121
  ```typescript
@@ -124,6 +169,15 @@ await setExpiration(result.id, result.mutateKey, 'never');
124
169
  | `computeFingerprint(publicKey)` | Compute SHA256 fingerprint |
125
170
  | `formatFingerprint(fingerprint)` | Format fingerprint for display |
126
171
 
172
+ #### Node-only SSH agent helpers
173
+
174
+ These helpers are available from `@coder/mux-md-client/ssh-agent` (Node.js only):
175
+
176
+ | Function | Description |
177
+ |----------|-------------|
178
+ | `createSshAgentSigner(options?)` | Create a signer callback compatible with `upload(..., { sign: { signer } })` |
179
+ | `createSshAgentSignatureEnvelope(data, options?)` | One-shot helper that returns a `SignatureEnvelope` |
180
+
127
181
  ### Types
128
182
 
129
183
  ```typescript
@@ -138,8 +192,7 @@ interface FileInfo {
138
192
  interface SignatureEnvelope {
139
193
  sig: string; // Base64-encoded signature
140
194
  publicKey: string; // SSH format public key
141
- email?: string; // For GitHub identity resolution
142
- githubUser?: string;
195
+ githubUser?: string; // Optional: claimed GitHub username for attribution
143
196
  }
144
197
 
145
198
  type KeyType = 'ed25519' | 'ecdsa-p256' | 'ecdsa-p384' | 'ecdsa-p521';
package/dist/index.cjs CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  base64UrlEncode: () => base64UrlEncode,
37
37
  buildUrl: () => buildUrl,
38
38
  computeFingerprint: () => computeFingerprint,
39
+ createSignatureEnvelope: () => createSignatureEnvelope,
39
40
  decrypt: () => decrypt,
40
41
  deleteFile: () => deleteFile,
41
42
  deriveKey: () => deriveKey,
@@ -51,6 +52,8 @@ __export(index_exports, {
51
52
  parsePublicKey: () => parsePublicKey,
52
53
  parseUrl: () => parseUrl,
53
54
  setExpiration: () => setExpiration,
55
+ signECDSA: () => signECDSA,
56
+ signEd25519: () => signEd25519,
54
57
  upload: () => upload,
55
58
  verifySignature: () => verifySignature
56
59
  });
@@ -299,25 +302,90 @@ function formatFingerprint(fingerprint) {
299
302
 
300
303
  // src/client.ts
301
304
  var DEFAULT_BASE_URL = "https://mux.md";
305
+ function assert(condition, message) {
306
+ if (!condition) {
307
+ throw new Error(message);
308
+ }
309
+ }
310
+ function bytesEqual(a, b) {
311
+ if (a.byteLength !== b.byteLength) return false;
312
+ for (let i = 0; i < a.byteLength; i++) {
313
+ if (a[i] !== b[i]) return false;
314
+ }
315
+ return true;
316
+ }
317
+ function decodeUtf8Strict(data) {
318
+ try {
319
+ const decoded = new TextDecoder("utf-8", {
320
+ fatal: true,
321
+ ignoreBOM: true
322
+ }).decode(data);
323
+ const reencoded = new TextEncoder().encode(decoded);
324
+ assert(
325
+ bytesEqual(data, reencoded),
326
+ "Signed uploads require UTF-8 text content"
327
+ );
328
+ return decoded;
329
+ } catch (error) {
330
+ if (error instanceof Error && error.message === "Signed uploads require UTF-8 text content") {
331
+ throw error;
332
+ }
333
+ throw new Error("Signed uploads require UTF-8 text content");
334
+ }
335
+ }
336
+ function assertSignatureEnvelope(value) {
337
+ if (!value || typeof value !== "object") {
338
+ throw new Error("Invalid SignatureEnvelope");
339
+ }
340
+ const env = value;
341
+ if (typeof env.sig !== "string" || env.sig.length === 0) {
342
+ throw new Error("Invalid SignatureEnvelope.sig");
343
+ }
344
+ if (typeof env.publicKey !== "string" || env.publicKey.length === 0) {
345
+ throw new Error("Invalid SignatureEnvelope.publicKey");
346
+ }
347
+ if (env.githubUser !== void 0 && typeof env.githubUser !== "string") {
348
+ throw new Error("Invalid SignatureEnvelope.githubUser");
349
+ }
350
+ }
302
351
  async function upload(data, fileInfo, options = {}) {
303
352
  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
304
353
  const keyMaterial = generateKey();
305
354
  const salt = generateSalt();
306
355
  const iv = generateIV();
307
356
  const cryptoKey = await deriveKey(keyMaterial, salt);
357
+ const wantsSignature = options.signature !== void 0 || options.sign !== void 0;
358
+ const signedContent = wantsSignature ? decodeUtf8Strict(data) : void 0;
308
359
  let signatureEnvelope;
309
- if (options.sign) {
310
- signatureEnvelope = await createSignatureEnvelope(
311
- data,
312
- options.sign.privateKey,
313
- options.sign.publicKey,
314
- { githubUser: options.sign.githubUser }
360
+ if (options.signature !== void 0) {
361
+ assertSignatureEnvelope(options.signature);
362
+ signatureEnvelope = options.signature;
363
+ } else if (options.sign) {
364
+ if ("signer" in options.sign) {
365
+ const envelope = await options.sign.signer(data);
366
+ assertSignatureEnvelope(envelope);
367
+ signatureEnvelope = envelope;
368
+ } else {
369
+ signatureEnvelope = await createSignatureEnvelope(
370
+ data,
371
+ options.sign.privateKey,
372
+ options.sign.publicKey,
373
+ { githubUser: options.sign.githubUser }
374
+ );
375
+ assertSignatureEnvelope(signatureEnvelope);
376
+ }
377
+ }
378
+ if (wantsSignature) {
379
+ assert(
380
+ signatureEnvelope !== void 0,
381
+ "Signature requested but no signature envelope was produced"
315
382
  );
316
383
  }
317
384
  let plaintext;
318
385
  if (signatureEnvelope) {
386
+ assert(signedContent !== void 0, "Signed content string missing");
319
387
  const signed = {
320
- content: new TextDecoder().decode(data),
388
+ content: signedContent,
321
389
  sig: signatureEnvelope
322
390
  };
323
391
  plaintext = new TextEncoder().encode(JSON.stringify(signed));
@@ -518,6 +586,7 @@ async function setExpiration(id, mutateKey, expiresAt, options = {}) {
518
586
  base64UrlEncode,
519
587
  buildUrl,
520
588
  computeFingerprint,
589
+ createSignatureEnvelope,
521
590
  decrypt,
522
591
  deleteFile,
523
592
  deriveKey,
@@ -533,6 +602,8 @@ async function setExpiration(id, mutateKey, expiresAt, options = {}) {
533
602
  parsePublicKey,
534
603
  parseUrl,
535
604
  setExpiration,
605
+ signECDSA,
606
+ signEd25519,
536
607
  upload,
537
608
  verifySignature
538
609
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/crypto.ts","../src/signing.ts","../src/client.ts"],"sourcesContent":["/**\n * @coder/mux-md-client - Client library for mux.md encrypted file sharing\n *\n * @example\n * ```typescript\n * import { upload, download } from '@coder/mux-md-client';\n *\n * // Upload with optional signing (library handles signature creation)\n * const content = new TextEncoder().encode('# Hello World');\n * const result = await upload(\n * content,\n * { name: 'msg.md', type: 'text/markdown', size: content.length },\n * {\n * sign: {\n * privateKey, // Uint8Array\n * publicKey, // SSH format string, e.g. \"ssh-ed25519 AAAA...\"\n * githubUser: 'username', // optional attribution\n * }\n * }\n * );\n *\n * // Download and verify signature\n * const { data, info, signature } = await download(result.url);\n * if (signature) {\n * // signature.publicKey contains the signer's public key\n * // signature.githubUser contains claimed GitHub username (if provided)\n * }\n * ```\n */\n\n// High-level client operations\nexport {\n buildUrl,\n type DownloadResult,\n deleteFile,\n download,\n getMeta,\n parseUrl,\n type SetExpirationOptions,\n type SetExpirationResult,\n type SignOptions,\n setExpiration,\n type UploadOptions,\n type UploadResult,\n upload,\n} from './client';\n// Low-level crypto (for advanced use)\nexport {\n base64Decode,\n base64Encode,\n base64UrlDecode,\n base64UrlEncode,\n decrypt,\n deriveKey,\n encrypt,\n generateId,\n generateIV,\n generateKey,\n generateMutateKey,\n generateSalt,\n} from './crypto';\n// Signing & verification\nexport {\n computeFingerprint,\n formatFingerprint,\n type KeyType,\n type ParsedPublicKey,\n parsePublicKey,\n verifySignature,\n} from './signing';\n\n// Types\nexport type {\n FileInfo,\n SignatureEnvelope,\n SignedPayload,\n UploadMeta,\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 * 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 { p256, p384, p521 } from '@noble/curves/nist.js';\nimport * as ed from '@noble/ed25519';\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 // In @noble/curves v2, sign() returns a Uint8Array directly (compact r||s format)\n const sigBytes = curves[curve].sign(message, privateKey, { prehash: true });\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 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?: { 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 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","/**\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 { createSignatureEnvelope } from './signing';\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\n/** Options for signing content during upload */\nexport interface SignOptions {\n /** Private key bytes (32 bytes for Ed25519, variable for ECDSA) */\n privateKey: Uint8Array;\n /** SSH format public key string (e.g., \"ssh-ed25519 AAAA...\") */\n publicKey: string;\n /** Optional GitHub username for attribution */\n githubUser?: string;\n}\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 /**\n * Sign the content with the provided credentials.\n * The library handles creating the signature envelope internally.\n */\n sign?: SignOptions;\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 // Create signature envelope if signing credentials provided\n let signatureEnvelope: SignatureEnvelope | undefined;\n if (options.sign) {\n signatureEnvelope = await createSignatureEnvelope(\n data,\n options.sign.privateKey,\n options.sign.publicKey,\n { githubUser: options.sign.githubUser },\n );\n }\n\n // Build plaintext - either signed (JSON) or raw content\n let plaintext: Uint8Array;\n if (signatureEnvelope) {\n // Signed format: JSON with content + signature\n const signed: SignedPayload = {\n content: new TextDecoder().decode(data),\n sig: signatureEnvelope,\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"],"mappings":"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","names":["base64Decode"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/crypto.ts","../src/signing.ts","../src/client.ts"],"sourcesContent":["/**\n * @coder/mux-md-client - Client library for mux.md encrypted file sharing\n *\n * @example\n * ```typescript\n * import { upload, download } from '@coder/mux-md-client';\n *\n * // Upload with optional signing (library handles signature creation)\n * const content = new TextEncoder().encode('# Hello World');\n * const result = await upload(\n * content,\n * { name: 'msg.md', type: 'text/markdown', size: content.length },\n * {\n * sign: {\n * privateKey, // Uint8Array\n * publicKey, // SSH format string, e.g. \"ssh-ed25519 AAAA...\"\n * githubUser: 'username', // optional attribution\n * }\n * }\n * );\n *\n * // Download and verify signature\n * const { data, info, signature } = await download(result.url);\n * if (signature) {\n * // signature.publicKey contains the signer's public key\n * // signature.githubUser contains claimed GitHub username (if provided)\n * }\n * ```\n */\n\n// High-level client operations\nexport {\n buildUrl,\n type DownloadResult,\n deleteFile,\n download,\n getMeta,\n parseUrl,\n type SetExpirationOptions,\n type SetExpirationResult,\n type SignOptions,\n setExpiration,\n type UploadOptions,\n type UploadResult,\n upload,\n} from './client';\n// Low-level crypto (for advanced use)\nexport {\n base64Decode,\n base64Encode,\n base64UrlDecode,\n base64UrlEncode,\n decrypt,\n deriveKey,\n encrypt,\n generateId,\n generateIV,\n generateKey,\n generateMutateKey,\n generateSalt,\n} from './crypto';\n// Signing & verification\nexport {\n createSignatureEnvelope,\n computeFingerprint,\n formatFingerprint,\n type KeyType,\n type ParsedPublicKey,\n parsePublicKey,\n signECDSA,\n signEd25519,\n verifySignature,\n} from './signing';\n\n// Types\nexport type {\n FileInfo,\n SignatureEnvelope,\n SignedPayload,\n UploadMeta,\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 * 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 { p256, p384, p521 } from '@noble/curves/nist.js';\nimport * as ed from '@noble/ed25519';\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 // In @noble/curves v2, sign() returns a Uint8Array directly (compact r||s format)\n const sigBytes = curves[curve].sign(message, privateKey, { prehash: true });\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 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?: { 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 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","/**\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 { createSignatureEnvelope } from './signing';\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\n// ----- Internal helpers -----\n\nfunction assert(condition: unknown, message: string): asserts condition {\n if (!condition) {\n throw new Error(message);\n }\n}\n\nfunction bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.byteLength !== b.byteLength) return false;\n for (let i = 0; i < a.byteLength; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\nfunction decodeUtf8Strict(data: Uint8Array): string {\n try {\n // We embed signed payloads as JSON with `content: string`, so we must be able to\n // losslessly decode the original bytes.\n const decoded = new TextDecoder('utf-8', {\n fatal: true,\n ignoreBOM: true,\n }).decode(data);\n\n // Defensive: ensure round-trip matches original bytes.\n const reencoded = new TextEncoder().encode(decoded);\n assert(\n bytesEqual(data, reencoded),\n 'Signed uploads require UTF-8 text content',\n );\n\n return decoded;\n } catch (error) {\n if (\n error instanceof Error &&\n error.message === 'Signed uploads require UTF-8 text content'\n ) {\n throw error;\n }\n throw new Error('Signed uploads require UTF-8 text content');\n }\n}\n\nfunction assertSignatureEnvelope(\n value: unknown,\n): asserts value is SignatureEnvelope {\n if (!value || typeof value !== 'object') {\n throw new Error('Invalid SignatureEnvelope');\n }\n\n const env = value as Partial<SignatureEnvelope>;\n if (typeof env.sig !== 'string' || env.sig.length === 0) {\n throw new Error('Invalid SignatureEnvelope.sig');\n }\n\n if (typeof env.publicKey !== 'string' || env.publicKey.length === 0) {\n throw new Error('Invalid SignatureEnvelope.publicKey');\n }\n\n if (env.githubUser !== undefined && typeof env.githubUser !== 'string') {\n throw new Error('Invalid SignatureEnvelope.githubUser');\n }\n}\n\n/** Options for signing content during upload */\nexport type SignOptions =\n | {\n /** Private key bytes (32 bytes for Ed25519, variable for ECDSA) */\n privateKey: Uint8Array;\n /** SSH format public key string (e.g., \"ssh-ed25519 AAAA...\") */\n publicKey: string;\n /** Optional GitHub username for attribution */\n githubUser?: string;\n }\n | {\n /**\n * Custom signer function.\n * Useful when the private key lives outside this process (e.g., an SSH agent).\n */\n signer: (data: Uint8Array) => Promise<SignatureEnvelope>;\n };\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 /**\n * Precomputed signature envelope to embed in the encrypted payload.\n * Takes precedence over `sign`.\n */\n signature?: SignatureEnvelope;\n /**\n * Sign the content.\n *\n * When provided, the decrypted blob becomes JSON (SignedPayload) containing both\n * the content string and the signature envelope.\n */\n sign?: SignOptions;\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 const wantsSignature =\n options.signature !== undefined || options.sign !== undefined;\n\n // Signed uploads embed plaintext as JSON with `content: string`.\n // That requires the original bytes to be valid UTF-8.\n const signedContent = wantsSignature ? decodeUtf8Strict(data) : undefined;\n\n // Resolve the signature envelope.\n let signatureEnvelope: SignatureEnvelope | undefined;\n\n if (options.signature !== undefined) {\n assertSignatureEnvelope(options.signature);\n signatureEnvelope = options.signature;\n } else if (options.sign) {\n if ('signer' in options.sign) {\n const envelope = await options.sign.signer(data);\n assertSignatureEnvelope(envelope);\n signatureEnvelope = envelope;\n } else {\n signatureEnvelope = await createSignatureEnvelope(\n data,\n options.sign.privateKey,\n options.sign.publicKey,\n { githubUser: options.sign.githubUser },\n );\n assertSignatureEnvelope(signatureEnvelope);\n }\n }\n\n if (wantsSignature) {\n assert(\n signatureEnvelope !== undefined,\n 'Signature requested but no signature envelope was produced',\n );\n }\n\n // Build plaintext - either signed (JSON) or raw content\n let plaintext: Uint8Array;\n if (signatureEnvelope) {\n assert(signedContent !== undefined, 'Signed content string missing');\n\n // Signed format: JSON with content + signature\n const signed: SignedPayload = {\n content: signedContent,\n sig: signatureEnvelope,\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"],"mappings":"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","names":["base64Decode"]}
package/dist/index.d.cts CHANGED
@@ -1,41 +1,5 @@
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
- }
30
- /**
31
- * Signed content payload (decrypted).
32
- * When a signature is present, the decrypted blob is JSON with this structure.
33
- * Legacy (unsigned) content decrypts directly to the raw content bytes.
34
- */
35
- interface SignedPayload {
36
- content: string;
37
- sig: SignatureEnvelope;
38
- }
1
+ import { F as FileInfo, S as SignatureEnvelope } from './types-BZMDMbOR.cjs';
2
+ export { a as SignedPayload, U as UploadMeta } from './types-BZMDMbOR.cjs';
39
3
 
40
4
  /**
41
5
  * mux.md Client Library
@@ -45,22 +9,35 @@ interface SignedPayload {
45
9
  */
46
10
 
47
11
  /** Options for signing content during upload */
48
- interface SignOptions {
12
+ type SignOptions = {
49
13
  /** Private key bytes (32 bytes for Ed25519, variable for ECDSA) */
50
14
  privateKey: Uint8Array;
51
15
  /** SSH format public key string (e.g., "ssh-ed25519 AAAA...") */
52
16
  publicKey: string;
53
17
  /** Optional GitHub username for attribution */
54
18
  githubUser?: string;
55
- }
19
+ } | {
20
+ /**
21
+ * Custom signer function.
22
+ * Useful when the private key lives outside this process (e.g., an SSH agent).
23
+ */
24
+ signer: (data: Uint8Array) => Promise<SignatureEnvelope>;
25
+ };
56
26
  interface UploadOptions {
57
27
  /** Base URL of the mux.md service */
58
28
  baseUrl?: string;
59
29
  /** Expiration time (unix timestamp ms, ISO date string, or Date object) */
60
30
  expiresAt?: number | string | Date;
61
31
  /**
62
- * Sign the content with the provided credentials.
63
- * The library handles creating the signature envelope internally.
32
+ * Precomputed signature envelope to embed in the encrypted payload.
33
+ * Takes precedence over `sign`.
34
+ */
35
+ signature?: SignatureEnvelope;
36
+ /**
37
+ * Sign the content.
38
+ *
39
+ * When provided, the decrypted blob becomes JSON (SignedPayload) containing both
40
+ * the content string and the signature envelope.
64
41
  */
65
42
  sign?: SignOptions;
66
43
  }
@@ -239,6 +216,34 @@ interface ParsedPublicKey {
239
216
  * - Raw base64 (32 bytes when decoded = Ed25519)
240
217
  */
241
218
  declare function parsePublicKey(keyString: string): ParsedPublicKey;
219
+ /**
220
+ * Sign a message with Ed25519 private key.
221
+ * @param message - The message bytes to sign
222
+ * @param privateKey - 32-byte Ed25519 private key
223
+ * @returns Base64-encoded signature (64 bytes)
224
+ */
225
+ declare function signEd25519(message: Uint8Array, privateKey: Uint8Array): Promise<string>;
226
+ /**
227
+ * Sign a message with ECDSA private key (P-256/384/521).
228
+ * @param message - The message bytes to sign (will be hashed)
229
+ * @param privateKey - ECDSA private key bytes
230
+ * @param curve - Which curve to use
231
+ * @returns Base64-encoded signature
232
+ */
233
+ declare function signECDSA(message: Uint8Array, privateKey: Uint8Array, curve: 'p256' | 'p384' | 'p521'): string;
234
+ /**
235
+ * Helper: Create a SignatureEnvelope from content + private key.
236
+ * This is the high-level API for signing before upload.
237
+ *
238
+ * @param content - The content bytes to sign
239
+ * @param privateKey - Private key bytes (32 bytes for Ed25519, variable for ECDSA)
240
+ * @param publicKey - SSH format public key string (e.g., "ssh-ed25519 AAAA...")
241
+ * @param options - Optional GitHub username for attribution
242
+ * @returns SignatureEnvelope ready for upload
243
+ */
244
+ declare function createSignatureEnvelope(content: Uint8Array, privateKey: Uint8Array, publicKey: string, options?: {
245
+ githubUser?: string;
246
+ }): Promise<SignatureEnvelope>;
242
247
  /**
243
248
  * Verify a signature using the appropriate algorithm based on key type.
244
249
  * For Ed25519: signature is raw 64 bytes
@@ -262,4 +267,4 @@ declare function computeFingerprint(publicKey: Uint8Array): Promise<string>;
262
267
  */
263
268
  declare function formatFingerprint(fingerprint: string): string;
264
269
 
265
- export { type DownloadResult, type FileInfo, type KeyType, type ParsedPublicKey, type SetExpirationOptions, type SetExpirationResult, type SignOptions, type SignatureEnvelope, type SignedPayload, type UploadMeta, type UploadOptions, type UploadResult, base64Decode, base64Encode, base64UrlDecode, base64UrlEncode, buildUrl, computeFingerprint, decrypt, deleteFile, deriveKey, download, encrypt, formatFingerprint, generateIV, generateId, generateKey, generateMutateKey, generateSalt, getMeta, parsePublicKey, parseUrl, setExpiration, upload, verifySignature };
270
+ export { type DownloadResult, FileInfo, type KeyType, type ParsedPublicKey, type SetExpirationOptions, type SetExpirationResult, type SignOptions, SignatureEnvelope, 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 };