@coder/mux-md-client 0.1.0-main.13 → 0.1.0-main.15
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/dist/index.cjs +146 -145
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -79
- package/dist/index.d.ts +61 -79
- package/dist/index.js +146 -142
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -36,7 +36,6 @@ __export(index_exports, {
|
|
|
36
36
|
base64UrlEncode: () => base64UrlEncode,
|
|
37
37
|
buildUrl: () => buildUrl,
|
|
38
38
|
computeFingerprint: () => computeFingerprint,
|
|
39
|
-
createSignatureEnvelope: () => createSignatureEnvelope,
|
|
40
39
|
decrypt: () => decrypt,
|
|
41
40
|
deleteFile: () => deleteFile,
|
|
42
41
|
deriveKey: () => deriveKey,
|
|
@@ -52,8 +51,6 @@ __export(index_exports, {
|
|
|
52
51
|
parsePublicKey: () => parsePublicKey,
|
|
53
52
|
parseUrl: () => parseUrl,
|
|
54
53
|
setExpiration: () => setExpiration,
|
|
55
|
-
signECDSA: () => signECDSA,
|
|
56
|
-
signEd25519: () => signEd25519,
|
|
57
54
|
upload: () => upload,
|
|
58
55
|
verifySignature: () => verifySignature
|
|
59
56
|
});
|
|
@@ -165,6 +162,141 @@ function base64Decode(str) {
|
|
|
165
162
|
return bytes;
|
|
166
163
|
}
|
|
167
164
|
|
|
165
|
+
// src/signing.ts
|
|
166
|
+
var import_nist = require("@noble/curves/nist.js");
|
|
167
|
+
var ed = __toESM(require("@noble/ed25519"), 1);
|
|
168
|
+
var import_sha2 = require("@noble/hashes/sha2.js");
|
|
169
|
+
ed.etc.sha512Sync = (...m) => (0, import_sha2.sha512)(ed.etc.concatBytes(...m));
|
|
170
|
+
var SSH_KEY_TYPES = {
|
|
171
|
+
"ssh-ed25519": "ed25519",
|
|
172
|
+
"ecdsa-sha2-nistp256": "ecdsa-p256",
|
|
173
|
+
"ecdsa-sha2-nistp384": "ecdsa-p384",
|
|
174
|
+
"ecdsa-sha2-nistp521": "ecdsa-p521"
|
|
175
|
+
};
|
|
176
|
+
function readSSHString(data, offset) {
|
|
177
|
+
const view = new DataView(data.buffer, data.byteOffset);
|
|
178
|
+
const len = view.getUint32(offset);
|
|
179
|
+
const value = data.slice(offset + 4, offset + 4 + len);
|
|
180
|
+
return { value, nextOffset: offset + 4 + len };
|
|
181
|
+
}
|
|
182
|
+
function base64Decode2(str) {
|
|
183
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
184
|
+
while (base64.length % 4) {
|
|
185
|
+
base64 += "=";
|
|
186
|
+
}
|
|
187
|
+
const binary = atob(base64);
|
|
188
|
+
const bytes = new Uint8Array(binary.length);
|
|
189
|
+
for (let i = 0; i < binary.length; i++) {
|
|
190
|
+
bytes[i] = binary.charCodeAt(i);
|
|
191
|
+
}
|
|
192
|
+
return bytes;
|
|
193
|
+
}
|
|
194
|
+
function parsePublicKey(keyString) {
|
|
195
|
+
const trimmed = keyString.trim();
|
|
196
|
+
for (const [sshType, keyType] of Object.entries(SSH_KEY_TYPES)) {
|
|
197
|
+
if (trimmed.startsWith(`${sshType} `)) {
|
|
198
|
+
const parts = trimmed.split(" ");
|
|
199
|
+
if (parts.length < 2) {
|
|
200
|
+
throw new Error("Invalid SSH key format");
|
|
201
|
+
}
|
|
202
|
+
const keyData = base64Decode2(parts[1]);
|
|
203
|
+
const { value: typeBytes, nextOffset: afterType } = readSSHString(
|
|
204
|
+
keyData,
|
|
205
|
+
0
|
|
206
|
+
);
|
|
207
|
+
const typeStr = new TextDecoder().decode(typeBytes);
|
|
208
|
+
if (typeStr !== sshType) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Key type mismatch: expected ${sshType}, got ${typeStr}`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (keyType === "ed25519") {
|
|
214
|
+
const { value: rawKey } = readSSHString(keyData, afterType);
|
|
215
|
+
if (rawKey.length !== 32) {
|
|
216
|
+
throw new Error("Invalid Ed25519 key length");
|
|
217
|
+
}
|
|
218
|
+
return { type: "ed25519", keyBytes: rawKey };
|
|
219
|
+
}
|
|
220
|
+
const { nextOffset: afterCurve } = readSSHString(keyData, afterType);
|
|
221
|
+
const { value: point } = readSSHString(keyData, afterCurve);
|
|
222
|
+
return { type: keyType, keyBytes: point };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const decoded = base64Decode2(trimmed);
|
|
226
|
+
if (decoded.length === 32) {
|
|
227
|
+
return { type: "ed25519", keyBytes: decoded };
|
|
228
|
+
}
|
|
229
|
+
throw new Error("Unsupported public key format");
|
|
230
|
+
}
|
|
231
|
+
async function signEd25519(message, privateKey) {
|
|
232
|
+
const sig = await ed.signAsync(message, privateKey);
|
|
233
|
+
return btoa(String.fromCharCode(...sig));
|
|
234
|
+
}
|
|
235
|
+
function signECDSA(message, privateKey, curve) {
|
|
236
|
+
const curves = { p256: import_nist.p256, p384: import_nist.p384, p521: import_nist.p521 };
|
|
237
|
+
const sigBytes = curves[curve].sign(message, privateKey, { prehash: true });
|
|
238
|
+
return btoa(String.fromCharCode(...sigBytes));
|
|
239
|
+
}
|
|
240
|
+
async function createSignatureEnvelope(content, privateKey, publicKey, options) {
|
|
241
|
+
const parsed = parsePublicKey(publicKey);
|
|
242
|
+
let sig;
|
|
243
|
+
if (parsed.type === "ed25519") {
|
|
244
|
+
sig = await signEd25519(content, privateKey);
|
|
245
|
+
} else {
|
|
246
|
+
const curve = parsed.type.replace("ecdsa-", "");
|
|
247
|
+
sig = signECDSA(content, privateKey, curve);
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
sig,
|
|
251
|
+
publicKey,
|
|
252
|
+
githubUser: options?.githubUser
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async function verifySignature(parsedKey, message, signature) {
|
|
256
|
+
try {
|
|
257
|
+
switch (parsedKey.type) {
|
|
258
|
+
case "ed25519":
|
|
259
|
+
return await ed.verifyAsync(signature, message, parsedKey.keyBytes);
|
|
260
|
+
case "ecdsa-p256":
|
|
261
|
+
return import_nist.p256.verify(signature, message, parsedKey.keyBytes, {
|
|
262
|
+
prehash: true
|
|
263
|
+
});
|
|
264
|
+
case "ecdsa-p384":
|
|
265
|
+
return import_nist.p384.verify(signature, message, parsedKey.keyBytes, {
|
|
266
|
+
prehash: true
|
|
267
|
+
});
|
|
268
|
+
case "ecdsa-p521":
|
|
269
|
+
return import_nist.p521.verify(signature, message, parsedKey.keyBytes, {
|
|
270
|
+
prehash: true
|
|
271
|
+
});
|
|
272
|
+
default:
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function computeFingerprint(publicKey) {
|
|
280
|
+
const hash = await crypto.subtle.digest(
|
|
281
|
+
"SHA-256",
|
|
282
|
+
publicKey.buffer
|
|
283
|
+
);
|
|
284
|
+
const hashArray = new Uint8Array(hash);
|
|
285
|
+
const base64 = btoa(String.fromCharCode(...hashArray));
|
|
286
|
+
return `SHA256:${base64.replace(/=+$/, "")}`;
|
|
287
|
+
}
|
|
288
|
+
function formatFingerprint(fingerprint) {
|
|
289
|
+
const base64Part = fingerprint.startsWith("SHA256:") ? fingerprint.slice(7) : fingerprint;
|
|
290
|
+
try {
|
|
291
|
+
const binary = atob(base64Part);
|
|
292
|
+
const hex = Array.from(binary).map((c) => c.charCodeAt(0).toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
293
|
+
const short = hex.slice(0, 16);
|
|
294
|
+
return short.match(/.{4}/g)?.join(" ") || short;
|
|
295
|
+
} catch {
|
|
296
|
+
return fingerprint.slice(0, 16).toUpperCase();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
168
300
|
// src/client.ts
|
|
169
301
|
var DEFAULT_BASE_URL = "https://mux.md";
|
|
170
302
|
async function upload(data, fileInfo, options = {}) {
|
|
@@ -173,11 +305,20 @@ async function upload(data, fileInfo, options = {}) {
|
|
|
173
305
|
const salt = generateSalt();
|
|
174
306
|
const iv = generateIV();
|
|
175
307
|
const cryptoKey = await deriveKey(keyMaterial, salt);
|
|
308
|
+
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 }
|
|
315
|
+
);
|
|
316
|
+
}
|
|
176
317
|
let plaintext;
|
|
177
|
-
if (
|
|
318
|
+
if (signatureEnvelope) {
|
|
178
319
|
const signed = {
|
|
179
320
|
content: new TextDecoder().decode(data),
|
|
180
|
-
sig:
|
|
321
|
+
sig: signatureEnvelope
|
|
181
322
|
};
|
|
182
323
|
plaintext = new TextEncoder().encode(JSON.stringify(signed));
|
|
183
324
|
} else {
|
|
@@ -369,143 +510,6 @@ async function setExpiration(id, mutateKey, expiresAt, options = {}) {
|
|
|
369
510
|
}
|
|
370
511
|
return response.json();
|
|
371
512
|
}
|
|
372
|
-
|
|
373
|
-
// src/signing.ts
|
|
374
|
-
var ed = __toESM(require("@noble/ed25519"), 1);
|
|
375
|
-
var import_nist = require("@noble/curves/nist.js");
|
|
376
|
-
var import_sha2 = require("@noble/hashes/sha2.js");
|
|
377
|
-
ed.etc.sha512Sync = (...m) => (0, import_sha2.sha512)(ed.etc.concatBytes(...m));
|
|
378
|
-
var SSH_KEY_TYPES = {
|
|
379
|
-
"ssh-ed25519": "ed25519",
|
|
380
|
-
"ecdsa-sha2-nistp256": "ecdsa-p256",
|
|
381
|
-
"ecdsa-sha2-nistp384": "ecdsa-p384",
|
|
382
|
-
"ecdsa-sha2-nistp521": "ecdsa-p521"
|
|
383
|
-
};
|
|
384
|
-
function readSSHString(data, offset) {
|
|
385
|
-
const view = new DataView(data.buffer, data.byteOffset);
|
|
386
|
-
const len = view.getUint32(offset);
|
|
387
|
-
const value = data.slice(offset + 4, offset + 4 + len);
|
|
388
|
-
return { value, nextOffset: offset + 4 + len };
|
|
389
|
-
}
|
|
390
|
-
function base64Decode2(str) {
|
|
391
|
-
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
392
|
-
while (base64.length % 4) {
|
|
393
|
-
base64 += "=";
|
|
394
|
-
}
|
|
395
|
-
const binary = atob(base64);
|
|
396
|
-
const bytes = new Uint8Array(binary.length);
|
|
397
|
-
for (let i = 0; i < binary.length; i++) {
|
|
398
|
-
bytes[i] = binary.charCodeAt(i);
|
|
399
|
-
}
|
|
400
|
-
return bytes;
|
|
401
|
-
}
|
|
402
|
-
function parsePublicKey(keyString) {
|
|
403
|
-
const trimmed = keyString.trim();
|
|
404
|
-
for (const [sshType, keyType] of Object.entries(SSH_KEY_TYPES)) {
|
|
405
|
-
if (trimmed.startsWith(`${sshType} `)) {
|
|
406
|
-
const parts = trimmed.split(" ");
|
|
407
|
-
if (parts.length < 2) {
|
|
408
|
-
throw new Error("Invalid SSH key format");
|
|
409
|
-
}
|
|
410
|
-
const keyData = base64Decode2(parts[1]);
|
|
411
|
-
const { value: typeBytes, nextOffset: afterType } = readSSHString(
|
|
412
|
-
keyData,
|
|
413
|
-
0
|
|
414
|
-
);
|
|
415
|
-
const typeStr = new TextDecoder().decode(typeBytes);
|
|
416
|
-
if (typeStr !== sshType) {
|
|
417
|
-
throw new Error(
|
|
418
|
-
`Key type mismatch: expected ${sshType}, got ${typeStr}`
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
if (keyType === "ed25519") {
|
|
422
|
-
const { value: rawKey } = readSSHString(keyData, afterType);
|
|
423
|
-
if (rawKey.length !== 32) {
|
|
424
|
-
throw new Error("Invalid Ed25519 key length");
|
|
425
|
-
}
|
|
426
|
-
return { type: "ed25519", keyBytes: rawKey };
|
|
427
|
-
}
|
|
428
|
-
const { nextOffset: afterCurve } = readSSHString(keyData, afterType);
|
|
429
|
-
const { value: point } = readSSHString(keyData, afterCurve);
|
|
430
|
-
return { type: keyType, keyBytes: point };
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
const decoded = base64Decode2(trimmed);
|
|
434
|
-
if (decoded.length === 32) {
|
|
435
|
-
return { type: "ed25519", keyBytes: decoded };
|
|
436
|
-
}
|
|
437
|
-
throw new Error("Unsupported public key format");
|
|
438
|
-
}
|
|
439
|
-
async function signEd25519(message, privateKey) {
|
|
440
|
-
const sig = await ed.signAsync(message, privateKey);
|
|
441
|
-
return btoa(String.fromCharCode(...sig));
|
|
442
|
-
}
|
|
443
|
-
function signECDSA(message, privateKey, curve) {
|
|
444
|
-
const curves = { p256: import_nist.p256, p384: import_nist.p384, p521: import_nist.p521 };
|
|
445
|
-
const sig = curves[curve].sign(message, privateKey, { prehash: true });
|
|
446
|
-
const sigBytes = sig.toCompactRawBytes();
|
|
447
|
-
return btoa(String.fromCharCode(...sigBytes));
|
|
448
|
-
}
|
|
449
|
-
async function createSignatureEnvelope(content, privateKey, publicKey, options) {
|
|
450
|
-
const parsed = parsePublicKey(publicKey);
|
|
451
|
-
let sig;
|
|
452
|
-
if (parsed.type === "ed25519") {
|
|
453
|
-
sig = await signEd25519(content, privateKey);
|
|
454
|
-
} else {
|
|
455
|
-
const curve = parsed.type.replace("ecdsa-", "");
|
|
456
|
-
sig = signECDSA(content, privateKey, curve);
|
|
457
|
-
}
|
|
458
|
-
return {
|
|
459
|
-
sig,
|
|
460
|
-
publicKey,
|
|
461
|
-
email: options?.email,
|
|
462
|
-
githubUser: options?.githubUser
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
async function verifySignature(parsedKey, message, signature) {
|
|
466
|
-
try {
|
|
467
|
-
switch (parsedKey.type) {
|
|
468
|
-
case "ed25519":
|
|
469
|
-
return await ed.verifyAsync(signature, message, parsedKey.keyBytes);
|
|
470
|
-
case "ecdsa-p256":
|
|
471
|
-
return import_nist.p256.verify(signature, message, parsedKey.keyBytes, {
|
|
472
|
-
prehash: true
|
|
473
|
-
});
|
|
474
|
-
case "ecdsa-p384":
|
|
475
|
-
return import_nist.p384.verify(signature, message, parsedKey.keyBytes, {
|
|
476
|
-
prehash: true
|
|
477
|
-
});
|
|
478
|
-
case "ecdsa-p521":
|
|
479
|
-
return import_nist.p521.verify(signature, message, parsedKey.keyBytes, {
|
|
480
|
-
prehash: true
|
|
481
|
-
});
|
|
482
|
-
default:
|
|
483
|
-
return false;
|
|
484
|
-
}
|
|
485
|
-
} catch {
|
|
486
|
-
return false;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
async function computeFingerprint(publicKey) {
|
|
490
|
-
const hash = await crypto.subtle.digest(
|
|
491
|
-
"SHA-256",
|
|
492
|
-
publicKey.buffer
|
|
493
|
-
);
|
|
494
|
-
const hashArray = new Uint8Array(hash);
|
|
495
|
-
const base64 = btoa(String.fromCharCode(...hashArray));
|
|
496
|
-
return `SHA256:${base64.replace(/=+$/, "")}`;
|
|
497
|
-
}
|
|
498
|
-
function formatFingerprint(fingerprint) {
|
|
499
|
-
const base64Part = fingerprint.startsWith("SHA256:") ? fingerprint.slice(7) : fingerprint;
|
|
500
|
-
try {
|
|
501
|
-
const binary = atob(base64Part);
|
|
502
|
-
const hex = Array.from(binary).map((c) => c.charCodeAt(0).toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
503
|
-
const short = hex.slice(0, 16);
|
|
504
|
-
return short.match(/.{4}/g)?.join(" ") || short;
|
|
505
|
-
} catch {
|
|
506
|
-
return fingerprint.slice(0, 16).toUpperCase();
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
513
|
// Annotate the CommonJS export names for ESM import in node:
|
|
510
514
|
0 && (module.exports = {
|
|
511
515
|
base64Decode,
|
|
@@ -514,7 +518,6 @@ function formatFingerprint(fingerprint) {
|
|
|
514
518
|
base64UrlEncode,
|
|
515
519
|
buildUrl,
|
|
516
520
|
computeFingerprint,
|
|
517
|
-
createSignatureEnvelope,
|
|
518
521
|
decrypt,
|
|
519
522
|
deleteFile,
|
|
520
523
|
deriveKey,
|
|
@@ -530,8 +533,6 @@ function formatFingerprint(fingerprint) {
|
|
|
530
533
|
parsePublicKey,
|
|
531
534
|
parseUrl,
|
|
532
535
|
setExpiration,
|
|
533
|
-
signECDSA,
|
|
534
|
-
signEd25519,
|
|
535
536
|
upload,
|
|
536
537
|
verifySignature
|
|
537
538
|
});
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +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"]}
|
|
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"]}
|