@coder/mux-md-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # @coder/mux-md-client
2
+
3
+ Client library for [mux.md](https://mux.md) encrypted file sharing with signature support.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @coder/mux-md-client
9
+ # or
10
+ bun add @coder/mux-md-client
11
+ ```
12
+
13
+ ## Features
14
+
15
+ - **End-to-end encryption** — AES-256-GCM with HKDF key derivation
16
+ - **Message signing** — Ed25519 and ECDSA (P-256, P-384, P-521) support
17
+ - **SSH key format** — Parse and use standard OpenSSH public keys
18
+ - **Zero-knowledge** — Server never sees plaintext content or signatures
19
+ - **Minimal dependencies** — Only `@noble/*` for cryptographic primitives
20
+
21
+ ## Usage
22
+
23
+ ### Upload content
24
+
25
+ ```typescript
26
+ import { upload } from '@coder/mux-md-client';
27
+
28
+ const content = new TextEncoder().encode('# Hello World');
29
+ const result = await upload(
30
+ content,
31
+ { name: 'message.md', type: 'text/markdown', size: content.length },
32
+ { expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000 } // 7 days
33
+ );
34
+
35
+ console.log(result.url); // https://mux.md/Ab3Xy#key...
36
+ console.log(result.mutateKey); // Store for deletion/expiration updates
37
+ ```
38
+
39
+ ### Download content
40
+
41
+ ```typescript
42
+ import { download } from '@coder/mux-md-client';
43
+
44
+ const { data, info, signature } = await download('https://mux.md/Ab3Xy#key...');
45
+ console.log(new TextDecoder().decode(data));
46
+
47
+ if (signature) {
48
+ console.log('Signed by:', signature.publicKey);
49
+ }
50
+ ```
51
+
52
+ ### Upload with signature
53
+
54
+ ```typescript
55
+ import { upload, createSignatureEnvelope } from '@coder/mux-md-client';
56
+
57
+ const content = new TextEncoder().encode('# Signed Message');
58
+
59
+ // Create signature from your SSH key
60
+ const signature = await createSignatureEnvelope(
61
+ content,
62
+ privateKey, // 32-byte Ed25519 private key
63
+ 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...', // Your public key
64
+ { email: 'user@example.com' }
65
+ );
66
+
67
+ const result = await upload(
68
+ content,
69
+ { name: 'signed.md', type: 'text/markdown', size: content.length },
70
+ { signature }
71
+ );
72
+ ```
73
+
74
+ ### Verify signatures
75
+
76
+ ```typescript
77
+ import { parsePublicKey, verifySignature, base64Decode } from '@coder/mux-md-client';
78
+
79
+ const parsedKey = parsePublicKey('ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...');
80
+ const sigBytes = base64Decode(signature.sig);
81
+ const messageBytes = new TextEncoder().encode(content);
82
+
83
+ const valid = await verifySignature(parsedKey, messageBytes, sigBytes);
84
+ ```
85
+
86
+ ### Delete or update expiration
87
+
88
+ ```typescript
89
+ import { deleteFile, setExpiration } from '@coder/mux-md-client';
90
+
91
+ // Delete file
92
+ await deleteFile(result.id, result.mutateKey);
93
+
94
+ // Update expiration
95
+ await setExpiration(result.id, result.mutateKey, Date.now() + 30 * 24 * 60 * 60 * 1000);
96
+
97
+ // Remove expiration
98
+ await setExpiration(result.id, result.mutateKey, 'never');
99
+ ```
100
+
101
+ ## API Reference
102
+
103
+ ### Client Operations
104
+
105
+ | Function | Description |
106
+ |----------|-------------|
107
+ | `upload(data, fileInfo, options?)` | Encrypt and upload content |
108
+ | `download(url, key?, options?)` | Download and decrypt content |
109
+ | `getMeta(url, key?, options?)` | Get file metadata without downloading |
110
+ | `deleteFile(id, mutateKey, options?)` | Delete a file |
111
+ | `setExpiration(id, mutateKey, expiresAt, options?)` | Update file expiration |
112
+ | `parseUrl(url)` | Parse mux.md URL into id + key |
113
+ | `buildUrl(id, key, baseUrl?)` | Build mux.md URL from components |
114
+
115
+ ### Signing
116
+
117
+ | Function | Description |
118
+ |----------|-------------|
119
+ | `createSignatureEnvelope(content, privateKey, publicKey, options?)` | Create a signature envelope for upload |
120
+ | `signEd25519(message, privateKey)` | Sign with Ed25519 |
121
+ | `signECDSA(message, privateKey, curve)` | Sign with ECDSA |
122
+ | `verifySignature(parsedKey, message, signature)` | Verify a signature |
123
+ | `parsePublicKey(keyString)` | Parse SSH public key |
124
+ | `computeFingerprint(publicKey)` | Compute SHA256 fingerprint |
125
+ | `formatFingerprint(fingerprint)` | Format fingerprint for display |
126
+
127
+ ### Types
128
+
129
+ ```typescript
130
+ interface FileInfo {
131
+ name: string;
132
+ type: string;
133
+ size: number;
134
+ model?: string; // AI model (e.g., "claude-sonnet-4-20250514")
135
+ thinking?: string; // Thinking level (e.g., "medium")
136
+ }
137
+
138
+ interface SignatureEnvelope {
139
+ sig: string; // Base64-encoded signature
140
+ publicKey: string; // SSH format public key
141
+ email?: string; // For GitHub identity resolution
142
+ githubUser?: string;
143
+ }
144
+
145
+ type KeyType = 'ed25519' | 'ecdsa-p256' | 'ecdsa-p384' | 'ecdsa-p521';
146
+ ```
147
+
148
+ ## License
149
+
150
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,538 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ base64Decode: () => base64Decode,
34
+ base64Encode: () => base64Encode,
35
+ base64UrlDecode: () => base64UrlDecode,
36
+ base64UrlEncode: () => base64UrlEncode,
37
+ buildUrl: () => buildUrl,
38
+ computeFingerprint: () => computeFingerprint,
39
+ createSignatureEnvelope: () => createSignatureEnvelope,
40
+ decrypt: () => decrypt,
41
+ deleteFile: () => deleteFile,
42
+ deriveKey: () => deriveKey,
43
+ download: () => download,
44
+ encrypt: () => encrypt,
45
+ formatFingerprint: () => formatFingerprint,
46
+ generateIV: () => generateIV,
47
+ generateId: () => generateId,
48
+ generateKey: () => generateKey,
49
+ generateMutateKey: () => generateMutateKey,
50
+ generateSalt: () => generateSalt,
51
+ getMeta: () => getMeta,
52
+ parsePublicKey: () => parsePublicKey,
53
+ parseUrl: () => parseUrl,
54
+ setExpiration: () => setExpiration,
55
+ signECDSA: () => signECDSA,
56
+ signEd25519: () => signEd25519,
57
+ upload: () => upload,
58
+ verifySignature: () => verifySignature
59
+ });
60
+ module.exports = __toCommonJS(index_exports);
61
+
62
+ // src/crypto.ts
63
+ var SALT_BYTES = 16;
64
+ var IV_BYTES = 12;
65
+ var KEY_BYTES = 10;
66
+ var ID_BYTES = 4;
67
+ var MUTATE_KEY_BYTES = 16;
68
+ var BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
69
+ function generateId() {
70
+ const bytes = new Uint8Array(ID_BYTES);
71
+ crypto.getRandomValues(bytes);
72
+ const num = bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3];
73
+ let result = "";
74
+ let n = num >>> 0;
75
+ for (let i = 0; i < 5; i++) {
76
+ result = BASE62[n % 62] + result;
77
+ n = Math.floor(n / 62);
78
+ }
79
+ return result;
80
+ }
81
+ function generateKey() {
82
+ const bytes = new Uint8Array(KEY_BYTES);
83
+ crypto.getRandomValues(bytes);
84
+ return base64UrlEncode(bytes);
85
+ }
86
+ function generateMutateKey() {
87
+ const bytes = new Uint8Array(MUTATE_KEY_BYTES);
88
+ crypto.getRandomValues(bytes);
89
+ return base64UrlEncode(bytes);
90
+ }
91
+ function generateSalt() {
92
+ const salt = new Uint8Array(SALT_BYTES);
93
+ crypto.getRandomValues(salt);
94
+ return salt;
95
+ }
96
+ function generateIV() {
97
+ const iv = new Uint8Array(IV_BYTES);
98
+ crypto.getRandomValues(iv);
99
+ return iv;
100
+ }
101
+ async function deriveKey(keyMaterial, salt) {
102
+ const rawKey = base64UrlDecode(keyMaterial);
103
+ const baseKey = await crypto.subtle.importKey(
104
+ "raw",
105
+ rawKey.buffer,
106
+ "HKDF",
107
+ false,
108
+ ["deriveKey"]
109
+ );
110
+ return crypto.subtle.deriveKey(
111
+ {
112
+ name: "HKDF",
113
+ salt: salt.buffer,
114
+ info: new Uint8Array(0),
115
+ // No additional context needed
116
+ hash: "SHA-256"
117
+ },
118
+ baseKey,
119
+ { name: "AES-GCM", length: 256 },
120
+ false,
121
+ ["encrypt", "decrypt"]
122
+ );
123
+ }
124
+ async function encrypt(data, key, iv) {
125
+ const ciphertext = await crypto.subtle.encrypt(
126
+ { name: "AES-GCM", iv: iv.buffer },
127
+ key,
128
+ data.buffer
129
+ );
130
+ return new Uint8Array(ciphertext);
131
+ }
132
+ async function decrypt(ciphertext, key, iv) {
133
+ const plaintext = await crypto.subtle.decrypt(
134
+ { name: "AES-GCM", iv: iv.buffer },
135
+ key,
136
+ ciphertext.buffer
137
+ );
138
+ return new Uint8Array(plaintext);
139
+ }
140
+ function base64UrlEncode(data) {
141
+ const base64 = btoa(String.fromCharCode(...data));
142
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
143
+ }
144
+ function base64UrlDecode(str) {
145
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
146
+ while (base64.length % 4) {
147
+ base64 += "=";
148
+ }
149
+ const binary = atob(base64);
150
+ const bytes = new Uint8Array(binary.length);
151
+ for (let i = 0; i < binary.length; i++) {
152
+ bytes[i] = binary.charCodeAt(i);
153
+ }
154
+ return bytes;
155
+ }
156
+ function base64Encode(data) {
157
+ return btoa(String.fromCharCode(...data));
158
+ }
159
+ function base64Decode(str) {
160
+ const binary = atob(str);
161
+ const bytes = new Uint8Array(binary.length);
162
+ for (let i = 0; i < binary.length; i++) {
163
+ bytes[i] = binary.charCodeAt(i);
164
+ }
165
+ return bytes;
166
+ }
167
+
168
+ // src/client.ts
169
+ var DEFAULT_BASE_URL = "https://mux.md";
170
+ async function upload(data, fileInfo, options = {}) {
171
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
172
+ const keyMaterial = generateKey();
173
+ const salt = generateSalt();
174
+ const iv = generateIV();
175
+ const cryptoKey = await deriveKey(keyMaterial, salt);
176
+ let plaintext;
177
+ if (options.signature) {
178
+ const signed = {
179
+ content: new TextDecoder().decode(data),
180
+ sig: options.signature
181
+ };
182
+ plaintext = new TextEncoder().encode(JSON.stringify(signed));
183
+ } else {
184
+ plaintext = data;
185
+ }
186
+ const payload = await encrypt(plaintext, cryptoKey, iv);
187
+ const metaJson = JSON.stringify(fileInfo);
188
+ const metaBytes = new TextEncoder().encode(metaJson);
189
+ const metaIv = generateIV();
190
+ const encryptedMeta = await encrypt(metaBytes, cryptoKey, metaIv);
191
+ const uploadMeta = {
192
+ salt: base64Encode(salt),
193
+ iv: base64Encode(iv),
194
+ encryptedMeta: base64Encode(new Uint8Array([...metaIv, ...encryptedMeta]))
195
+ };
196
+ const headers = {
197
+ "Content-Type": "application/octet-stream",
198
+ "X-Mux-Meta": btoa(JSON.stringify(uploadMeta))
199
+ };
200
+ if (options.expiresAt !== void 0) {
201
+ let expiresDate;
202
+ if (options.expiresAt instanceof Date) {
203
+ expiresDate = options.expiresAt;
204
+ } else if (typeof options.expiresAt === "string") {
205
+ expiresDate = new Date(options.expiresAt);
206
+ } else {
207
+ expiresDate = new Date(options.expiresAt);
208
+ }
209
+ headers["X-Mux-Expires"] = expiresDate.toISOString();
210
+ }
211
+ const response = await fetch(`${baseUrl}/`, {
212
+ method: "POST",
213
+ headers,
214
+ body: payload.buffer
215
+ });
216
+ if (!response.ok) {
217
+ const error = await response.json().catch(() => ({ error: "Upload failed" }));
218
+ throw new Error(error.error || "Upload failed");
219
+ }
220
+ const result = await response.json();
221
+ return {
222
+ url: `${baseUrl}/${result.id}#${keyMaterial}`,
223
+ id: result.id,
224
+ key: keyMaterial,
225
+ mutateKey: result.mutateKey,
226
+ ...result.expiresAt && { expiresAt: result.expiresAt }
227
+ };
228
+ }
229
+ async function download(url, key, options = {}) {
230
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
231
+ let id;
232
+ let keyMaterial;
233
+ if (url.includes("#")) {
234
+ const urlObj = new URL(url);
235
+ id = urlObj.pathname.slice(1);
236
+ keyMaterial = urlObj.hash.slice(1);
237
+ } else if (url.includes("/")) {
238
+ const parts = url.split("/");
239
+ id = parts[parts.length - 1];
240
+ if (!key) throw new Error("Key required when URL has no fragment");
241
+ keyMaterial = key;
242
+ } else {
243
+ id = url;
244
+ if (!key) throw new Error("Key required when only ID is provided");
245
+ keyMaterial = key;
246
+ }
247
+ const response = await fetch(`${baseUrl}/${id}`);
248
+ if (!response.ok) {
249
+ const error = await response.json().catch(() => ({ error: "Download failed" }));
250
+ throw new Error(error.error || "Download failed");
251
+ }
252
+ const metaHeader = response.headers.get("X-Mux-Meta");
253
+ if (!metaHeader) {
254
+ throw new Error("Missing metadata header");
255
+ }
256
+ const uploadMeta = JSON.parse(atob(metaHeader));
257
+ const encryptedData = new Uint8Array(await response.arrayBuffer());
258
+ const salt = base64Decode(uploadMeta.salt);
259
+ const iv = base64Decode(uploadMeta.iv);
260
+ const cryptoKey = await deriveKey(keyMaterial, salt);
261
+ const encryptedMetaWithIv = base64Decode(uploadMeta.encryptedMeta);
262
+ const metaIv = encryptedMetaWithIv.slice(0, 12);
263
+ const encryptedMetaData = encryptedMetaWithIv.slice(12);
264
+ const metaBytes = await decrypt(encryptedMetaData, cryptoKey, metaIv);
265
+ const info = JSON.parse(new TextDecoder().decode(metaBytes));
266
+ const decrypted = await decrypt(encryptedData, cryptoKey, iv);
267
+ if (decrypted[0] === 123) {
268
+ try {
269
+ const jsonStr = new TextDecoder().decode(decrypted);
270
+ const parsed = JSON.parse(jsonStr);
271
+ if (typeof parsed.content === "string" && parsed.sig) {
272
+ const data = new TextEncoder().encode(parsed.content);
273
+ const signature = parsed.sig;
274
+ return { data, info, signature };
275
+ }
276
+ } catch {
277
+ }
278
+ }
279
+ return { data: decrypted, info };
280
+ }
281
+ async function getMeta(url, key, options = {}) {
282
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
283
+ let id;
284
+ let keyMaterial;
285
+ if (url.includes("#")) {
286
+ const urlObj = new URL(url);
287
+ id = urlObj.pathname.slice(1);
288
+ keyMaterial = urlObj.hash.slice(1);
289
+ } else if (url.includes("/")) {
290
+ const parts = url.split("/");
291
+ id = parts[parts.length - 1];
292
+ if (!key) throw new Error("Key required when URL has no fragment");
293
+ keyMaterial = key;
294
+ } else {
295
+ id = url;
296
+ if (!key) throw new Error("Key required when only ID is provided");
297
+ keyMaterial = key;
298
+ }
299
+ const response = await fetch(`${baseUrl}/${id}/meta`);
300
+ if (!response.ok) {
301
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
302
+ throw new Error(error.error || "Request failed");
303
+ }
304
+ const meta = await response.json();
305
+ const salt = base64Decode(meta.salt);
306
+ const cryptoKey = await deriveKey(keyMaterial, salt);
307
+ const encryptedMetaWithIv = base64Decode(meta.encryptedMeta);
308
+ const metaIv = encryptedMetaWithIv.slice(0, 12);
309
+ const encryptedMetaData = encryptedMetaWithIv.slice(12);
310
+ const metaBytes = await decrypt(encryptedMetaData, cryptoKey, metaIv);
311
+ const info = JSON.parse(new TextDecoder().decode(metaBytes));
312
+ return {
313
+ info,
314
+ size: meta.size
315
+ };
316
+ }
317
+ function parseUrl(url) {
318
+ try {
319
+ const urlObj = new URL(url);
320
+ if (!urlObj.hash) return null;
321
+ const id = urlObj.pathname.slice(1);
322
+ const key = urlObj.hash.slice(1);
323
+ if (!id || !key) return null;
324
+ return { id, key };
325
+ } catch {
326
+ return null;
327
+ }
328
+ }
329
+ function buildUrl(id, key, baseUrl = DEFAULT_BASE_URL) {
330
+ return `${baseUrl}/${id}#${key}`;
331
+ }
332
+ async function deleteFile(id, mutateKey, options = {}) {
333
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
334
+ const response = await fetch(`${baseUrl}/${id}`, {
335
+ method: "DELETE",
336
+ headers: {
337
+ "X-Mux-Mutate-Key": mutateKey
338
+ }
339
+ });
340
+ if (!response.ok) {
341
+ const error = await response.json().catch(() => ({ error: "Delete failed" }));
342
+ throw new Error(error.error || "Delete failed");
343
+ }
344
+ }
345
+ async function setExpiration(id, mutateKey, expiresAt, options = {}) {
346
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
347
+ let expiresHeader;
348
+ if (expiresAt === "never") {
349
+ expiresHeader = "never";
350
+ } else if (expiresAt instanceof Date) {
351
+ expiresHeader = expiresAt.toISOString();
352
+ } else if (typeof expiresAt === "string") {
353
+ expiresHeader = new Date(expiresAt).toISOString();
354
+ } else {
355
+ expiresHeader = new Date(expiresAt).toISOString();
356
+ }
357
+ const response = await fetch(`${baseUrl}/${id}`, {
358
+ method: "PATCH",
359
+ headers: {
360
+ "X-Mux-Mutate-Key": mutateKey,
361
+ "X-Mux-Expires": expiresHeader
362
+ }
363
+ });
364
+ if (!response.ok) {
365
+ const error = await response.json().catch(() => ({ error: "Set expiration failed" }));
366
+ throw new Error(
367
+ error.error || "Set expiration failed"
368
+ );
369
+ }
370
+ return response.json();
371
+ }
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
+ // Annotate the CommonJS export names for ESM import in node:
510
+ 0 && (module.exports = {
511
+ base64Decode,
512
+ base64Encode,
513
+ base64UrlDecode,
514
+ base64UrlEncode,
515
+ buildUrl,
516
+ computeFingerprint,
517
+ createSignatureEnvelope,
518
+ decrypt,
519
+ deleteFile,
520
+ deriveKey,
521
+ download,
522
+ encrypt,
523
+ formatFingerprint,
524
+ generateIV,
525
+ generateId,
526
+ generateKey,
527
+ generateMutateKey,
528
+ generateSalt,
529
+ getMeta,
530
+ parsePublicKey,
531
+ parseUrl,
532
+ setExpiration,
533
+ signECDSA,
534
+ signEd25519,
535
+ upload,
536
+ verifySignature
537
+ });
538
+ //# sourceMappingURL=index.cjs.map