@coder/mux-md-client 0.1.0-main.13 → 0.1.0-main.14

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.d.cts CHANGED
@@ -45,13 +45,27 @@ interface SignedPayload {
45
45
  * Works in both Node.js (with webcrypto) and browser environments.
46
46
  */
47
47
 
48
+ /** Options for signing content during upload */
49
+ interface SignOptions {
50
+ /** Private key bytes (32 bytes for Ed25519, variable for ECDSA) */
51
+ privateKey: Uint8Array;
52
+ /** SSH format public key string (e.g., "ssh-ed25519 AAAA...") */
53
+ publicKey: string;
54
+ /** Optional email for attribution */
55
+ email?: string;
56
+ /** Optional GitHub username for attribution */
57
+ githubUser?: string;
58
+ }
48
59
  interface UploadOptions {
49
60
  /** Base URL of the mux.md service */
50
61
  baseUrl?: string;
51
62
  /** Expiration time (unix timestamp ms, ISO date string, or Date object) */
52
63
  expiresAt?: number | string | Date;
53
- /** Signature envelope (will be encrypted with content) */
54
- signature?: SignatureEnvelope;
64
+ /**
65
+ * Sign the content with the provided credentials.
66
+ * The library handles creating the signature envelope internally.
67
+ */
68
+ sign?: SignOptions;
55
69
  }
56
70
  interface UploadResult {
57
71
  /** Full URL with encryption key in fragment */
@@ -151,81 +165,6 @@ interface SetExpirationResult {
151
165
  */
152
166
  declare function setExpiration(id: string, mutateKey: string, expiresAt: number | string | Date | 'never', options?: SetExpirationOptions): Promise<SetExpirationResult>;
153
167
 
154
- /**
155
- * Signing and verification utilities for mux.md
156
- *
157
- * Supports Ed25519 and ECDSA (P-256, P-384, P-521) keys in SSH format.
158
- */
159
-
160
- /** Supported key types */
161
- type KeyType = 'ed25519' | 'ecdsa-p256' | 'ecdsa-p384' | 'ecdsa-p521';
162
- /** Parsed public key with type and raw bytes */
163
- interface ParsedPublicKey {
164
- type: KeyType;
165
- keyBytes: Uint8Array;
166
- }
167
- /**
168
- * Parse an SSH public key and extract the key bytes and type.
169
- * Supports formats:
170
- * - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... [comment]
171
- * - ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY... [comment]
172
- * - ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQ... [comment]
173
- * - ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjE... [comment]
174
- * - Raw base64 (32 bytes when decoded = Ed25519)
175
- */
176
- declare function parsePublicKey(keyString: string): ParsedPublicKey;
177
- /**
178
- * Sign a message with Ed25519 private key.
179
- * @param message - The message bytes to sign
180
- * @param privateKey - 32-byte Ed25519 private key
181
- * @returns Base64-encoded signature (64 bytes)
182
- */
183
- declare function signEd25519(message: Uint8Array, privateKey: Uint8Array): Promise<string>;
184
- /**
185
- * Sign a message with ECDSA private key (P-256/384/521).
186
- * @param message - The message bytes to sign (will be hashed)
187
- * @param privateKey - ECDSA private key bytes
188
- * @param curve - Which curve to use
189
- * @returns Base64-encoded signature
190
- */
191
- declare function signECDSA(message: Uint8Array, privateKey: Uint8Array, curve: 'p256' | 'p384' | 'p521'): string;
192
- /**
193
- * Helper: Create a SignatureEnvelope from content + private key.
194
- * This is the high-level API for signing before upload.
195
- *
196
- * @param content - The content bytes to sign
197
- * @param privateKey - Private key bytes (32 bytes for Ed25519, variable for ECDSA)
198
- * @param publicKey - SSH format public key string (e.g., "ssh-ed25519 AAAA...")
199
- * @param options - Optional email or GitHub username for attribution
200
- * @returns SignatureEnvelope ready for upload
201
- */
202
- declare function createSignatureEnvelope(content: Uint8Array, privateKey: Uint8Array, publicKey: string, options?: {
203
- email?: string;
204
- githubUser?: string;
205
- }): Promise<SignatureEnvelope>;
206
- /**
207
- * Verify a signature using the appropriate algorithm based on key type.
208
- * For Ed25519: signature is raw 64 bytes
209
- * For ECDSA: signature is DER-encoded or raw r||s format
210
- *
211
- * @param parsedKey - Parsed public key (from parsePublicKey)
212
- * @param message - Original message bytes
213
- * @param signature - Signature bytes (not base64)
214
- * @returns true if signature is valid
215
- */
216
- declare function verifySignature(parsedKey: ParsedPublicKey, message: Uint8Array, signature: Uint8Array): Promise<boolean>;
217
- /**
218
- * Compute SHA256 fingerprint of a public key (matches ssh-keygen -l format)
219
- * @param publicKey - Raw public key bytes
220
- * @returns Fingerprint string like "SHA256:abc123..."
221
- */
222
- declare function computeFingerprint(publicKey: Uint8Array): Promise<string>;
223
- /**
224
- * Format a fingerprint for nice display.
225
- * Converts base64 fingerprint to uppercase hex groups like "DEAD BEEF 1234 5678"
226
- */
227
- declare function formatFingerprint(fingerprint: string): string;
228
-
229
168
  /**
230
169
  * Generate a random file ID (30 bits, 5 chars base62)
231
170
  */
@@ -280,4 +219,50 @@ declare function base64Encode(data: Uint8Array): string;
280
219
  */
281
220
  declare function base64Decode(str: string): Uint8Array;
282
221
 
283
- export { type DownloadResult, type FileInfo, type KeyType, type ParsedPublicKey, type SetExpirationOptions, type SetExpirationResult, type SignatureEnvelope, type SignedPayload, type UploadMeta, type UploadOptions, type UploadResult, base64Decode, base64Encode, base64UrlDecode, base64UrlEncode, buildUrl, computeFingerprint, createSignatureEnvelope, decrypt, deleteFile, deriveKey, download, encrypt, formatFingerprint, generateIV, generateId, generateKey, generateMutateKey, generateSalt, getMeta, parsePublicKey, parseUrl, setExpiration, signECDSA, signEd25519, upload, verifySignature };
222
+ /**
223
+ * Signing and verification utilities for mux.md
224
+ *
225
+ * Supports Ed25519 and ECDSA (P-256, P-384, P-521) keys in SSH format.
226
+ */
227
+
228
+ /** Supported key types */
229
+ type KeyType = 'ed25519' | 'ecdsa-p256' | 'ecdsa-p384' | 'ecdsa-p521';
230
+ /** Parsed public key with type and raw bytes */
231
+ interface ParsedPublicKey {
232
+ type: KeyType;
233
+ keyBytes: Uint8Array;
234
+ }
235
+ /**
236
+ * Parse an SSH public key and extract the key bytes and type.
237
+ * Supports formats:
238
+ * - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... [comment]
239
+ * - ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY... [comment]
240
+ * - ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQ... [comment]
241
+ * - ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjE... [comment]
242
+ * - Raw base64 (32 bytes when decoded = Ed25519)
243
+ */
244
+ declare function parsePublicKey(keyString: string): ParsedPublicKey;
245
+ /**
246
+ * Verify a signature using the appropriate algorithm based on key type.
247
+ * For Ed25519: signature is raw 64 bytes
248
+ * For ECDSA: signature is DER-encoded or raw r||s format
249
+ *
250
+ * @param parsedKey - Parsed public key (from parsePublicKey)
251
+ * @param message - Original message bytes
252
+ * @param signature - Signature bytes (not base64)
253
+ * @returns true if signature is valid
254
+ */
255
+ declare function verifySignature(parsedKey: ParsedPublicKey, message: Uint8Array, signature: Uint8Array): Promise<boolean>;
256
+ /**
257
+ * Compute SHA256 fingerprint of a public key (matches ssh-keygen -l format)
258
+ * @param publicKey - Raw public key bytes
259
+ * @returns Fingerprint string like "SHA256:abc123..."
260
+ */
261
+ declare function computeFingerprint(publicKey: Uint8Array): Promise<string>;
262
+ /**
263
+ * Format a fingerprint for nice display.
264
+ * Converts base64 fingerprint to uppercase hex groups like "DEAD BEEF 1234 5678"
265
+ */
266
+ declare function formatFingerprint(fingerprint: string): string;
267
+
268
+ export { type DownloadResult, type FileInfo, type KeyType, type ParsedPublicKey, type SetExpirationOptions, type SetExpirationResult, type SignOptions, type SignatureEnvelope, type SignedPayload, type UploadMeta, type UploadOptions, type UploadResult, base64Decode, base64Encode, base64UrlDecode, base64UrlEncode, buildUrl, computeFingerprint, decrypt, deleteFile, deriveKey, download, encrypt, formatFingerprint, generateIV, generateId, generateKey, generateMutateKey, generateSalt, getMeta, parsePublicKey, parseUrl, setExpiration, upload, verifySignature };
package/dist/index.d.ts CHANGED
@@ -45,13 +45,27 @@ interface SignedPayload {
45
45
  * Works in both Node.js (with webcrypto) and browser environments.
46
46
  */
47
47
 
48
+ /** Options for signing content during upload */
49
+ interface SignOptions {
50
+ /** Private key bytes (32 bytes for Ed25519, variable for ECDSA) */
51
+ privateKey: Uint8Array;
52
+ /** SSH format public key string (e.g., "ssh-ed25519 AAAA...") */
53
+ publicKey: string;
54
+ /** Optional email for attribution */
55
+ email?: string;
56
+ /** Optional GitHub username for attribution */
57
+ githubUser?: string;
58
+ }
48
59
  interface UploadOptions {
49
60
  /** Base URL of the mux.md service */
50
61
  baseUrl?: string;
51
62
  /** Expiration time (unix timestamp ms, ISO date string, or Date object) */
52
63
  expiresAt?: number | string | Date;
53
- /** Signature envelope (will be encrypted with content) */
54
- signature?: SignatureEnvelope;
64
+ /**
65
+ * Sign the content with the provided credentials.
66
+ * The library handles creating the signature envelope internally.
67
+ */
68
+ sign?: SignOptions;
55
69
  }
56
70
  interface UploadResult {
57
71
  /** Full URL with encryption key in fragment */
@@ -151,81 +165,6 @@ interface SetExpirationResult {
151
165
  */
152
166
  declare function setExpiration(id: string, mutateKey: string, expiresAt: number | string | Date | 'never', options?: SetExpirationOptions): Promise<SetExpirationResult>;
153
167
 
154
- /**
155
- * Signing and verification utilities for mux.md
156
- *
157
- * Supports Ed25519 and ECDSA (P-256, P-384, P-521) keys in SSH format.
158
- */
159
-
160
- /** Supported key types */
161
- type KeyType = 'ed25519' | 'ecdsa-p256' | 'ecdsa-p384' | 'ecdsa-p521';
162
- /** Parsed public key with type and raw bytes */
163
- interface ParsedPublicKey {
164
- type: KeyType;
165
- keyBytes: Uint8Array;
166
- }
167
- /**
168
- * Parse an SSH public key and extract the key bytes and type.
169
- * Supports formats:
170
- * - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... [comment]
171
- * - ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY... [comment]
172
- * - ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQ... [comment]
173
- * - ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjE... [comment]
174
- * - Raw base64 (32 bytes when decoded = Ed25519)
175
- */
176
- declare function parsePublicKey(keyString: string): ParsedPublicKey;
177
- /**
178
- * Sign a message with Ed25519 private key.
179
- * @param message - The message bytes to sign
180
- * @param privateKey - 32-byte Ed25519 private key
181
- * @returns Base64-encoded signature (64 bytes)
182
- */
183
- declare function signEd25519(message: Uint8Array, privateKey: Uint8Array): Promise<string>;
184
- /**
185
- * Sign a message with ECDSA private key (P-256/384/521).
186
- * @param message - The message bytes to sign (will be hashed)
187
- * @param privateKey - ECDSA private key bytes
188
- * @param curve - Which curve to use
189
- * @returns Base64-encoded signature
190
- */
191
- declare function signECDSA(message: Uint8Array, privateKey: Uint8Array, curve: 'p256' | 'p384' | 'p521'): string;
192
- /**
193
- * Helper: Create a SignatureEnvelope from content + private key.
194
- * This is the high-level API for signing before upload.
195
- *
196
- * @param content - The content bytes to sign
197
- * @param privateKey - Private key bytes (32 bytes for Ed25519, variable for ECDSA)
198
- * @param publicKey - SSH format public key string (e.g., "ssh-ed25519 AAAA...")
199
- * @param options - Optional email or GitHub username for attribution
200
- * @returns SignatureEnvelope ready for upload
201
- */
202
- declare function createSignatureEnvelope(content: Uint8Array, privateKey: Uint8Array, publicKey: string, options?: {
203
- email?: string;
204
- githubUser?: string;
205
- }): Promise<SignatureEnvelope>;
206
- /**
207
- * Verify a signature using the appropriate algorithm based on key type.
208
- * For Ed25519: signature is raw 64 bytes
209
- * For ECDSA: signature is DER-encoded or raw r||s format
210
- *
211
- * @param parsedKey - Parsed public key (from parsePublicKey)
212
- * @param message - Original message bytes
213
- * @param signature - Signature bytes (not base64)
214
- * @returns true if signature is valid
215
- */
216
- declare function verifySignature(parsedKey: ParsedPublicKey, message: Uint8Array, signature: Uint8Array): Promise<boolean>;
217
- /**
218
- * Compute SHA256 fingerprint of a public key (matches ssh-keygen -l format)
219
- * @param publicKey - Raw public key bytes
220
- * @returns Fingerprint string like "SHA256:abc123..."
221
- */
222
- declare function computeFingerprint(publicKey: Uint8Array): Promise<string>;
223
- /**
224
- * Format a fingerprint for nice display.
225
- * Converts base64 fingerprint to uppercase hex groups like "DEAD BEEF 1234 5678"
226
- */
227
- declare function formatFingerprint(fingerprint: string): string;
228
-
229
168
  /**
230
169
  * Generate a random file ID (30 bits, 5 chars base62)
231
170
  */
@@ -280,4 +219,50 @@ declare function base64Encode(data: Uint8Array): string;
280
219
  */
281
220
  declare function base64Decode(str: string): Uint8Array;
282
221
 
283
- export { type DownloadResult, type FileInfo, type KeyType, type ParsedPublicKey, type SetExpirationOptions, type SetExpirationResult, type SignatureEnvelope, type SignedPayload, type UploadMeta, type UploadOptions, type UploadResult, base64Decode, base64Encode, base64UrlDecode, base64UrlEncode, buildUrl, computeFingerprint, createSignatureEnvelope, decrypt, deleteFile, deriveKey, download, encrypt, formatFingerprint, generateIV, generateId, generateKey, generateMutateKey, generateSalt, getMeta, parsePublicKey, parseUrl, setExpiration, signECDSA, signEd25519, upload, verifySignature };
222
+ /**
223
+ * Signing and verification utilities for mux.md
224
+ *
225
+ * Supports Ed25519 and ECDSA (P-256, P-384, P-521) keys in SSH format.
226
+ */
227
+
228
+ /** Supported key types */
229
+ type KeyType = 'ed25519' | 'ecdsa-p256' | 'ecdsa-p384' | 'ecdsa-p521';
230
+ /** Parsed public key with type and raw bytes */
231
+ interface ParsedPublicKey {
232
+ type: KeyType;
233
+ keyBytes: Uint8Array;
234
+ }
235
+ /**
236
+ * Parse an SSH public key and extract the key bytes and type.
237
+ * Supports formats:
238
+ * - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... [comment]
239
+ * - ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY... [comment]
240
+ * - ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQ... [comment]
241
+ * - ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjE... [comment]
242
+ * - Raw base64 (32 bytes when decoded = Ed25519)
243
+ */
244
+ declare function parsePublicKey(keyString: string): ParsedPublicKey;
245
+ /**
246
+ * Verify a signature using the appropriate algorithm based on key type.
247
+ * For Ed25519: signature is raw 64 bytes
248
+ * For ECDSA: signature is DER-encoded or raw r||s format
249
+ *
250
+ * @param parsedKey - Parsed public key (from parsePublicKey)
251
+ * @param message - Original message bytes
252
+ * @param signature - Signature bytes (not base64)
253
+ * @returns true if signature is valid
254
+ */
255
+ declare function verifySignature(parsedKey: ParsedPublicKey, message: Uint8Array, signature: Uint8Array): Promise<boolean>;
256
+ /**
257
+ * Compute SHA256 fingerprint of a public key (matches ssh-keygen -l format)
258
+ * @param publicKey - Raw public key bytes
259
+ * @returns Fingerprint string like "SHA256:abc123..."
260
+ */
261
+ declare function computeFingerprint(publicKey: Uint8Array): Promise<string>;
262
+ /**
263
+ * Format a fingerprint for nice display.
264
+ * Converts base64 fingerprint to uppercase hex groups like "DEAD BEEF 1234 5678"
265
+ */
266
+ declare function formatFingerprint(fingerprint: string): string;
267
+
268
+ export { type DownloadResult, type FileInfo, type KeyType, type ParsedPublicKey, type SetExpirationOptions, type SetExpirationResult, type SignOptions, type SignatureEnvelope, type SignedPayload, type UploadMeta, type UploadOptions, type UploadResult, base64Decode, base64Encode, base64UrlDecode, base64UrlEncode, buildUrl, computeFingerprint, decrypt, deleteFile, deriveKey, download, encrypt, formatFingerprint, generateIV, generateId, generateKey, generateMutateKey, generateSalt, getMeta, parsePublicKey, parseUrl, setExpiration, upload, verifySignature };
package/dist/index.js CHANGED
@@ -104,6 +104,142 @@ function base64Decode(str) {
104
104
  return bytes;
105
105
  }
106
106
 
107
+ // src/signing.ts
108
+ import { p256, p384, p521 } from "@noble/curves/nist.js";
109
+ import * as ed from "@noble/ed25519";
110
+ import { sha512 } from "@noble/hashes/sha2.js";
111
+ ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
112
+ var SSH_KEY_TYPES = {
113
+ "ssh-ed25519": "ed25519",
114
+ "ecdsa-sha2-nistp256": "ecdsa-p256",
115
+ "ecdsa-sha2-nistp384": "ecdsa-p384",
116
+ "ecdsa-sha2-nistp521": "ecdsa-p521"
117
+ };
118
+ function readSSHString(data, offset) {
119
+ const view = new DataView(data.buffer, data.byteOffset);
120
+ const len = view.getUint32(offset);
121
+ const value = data.slice(offset + 4, offset + 4 + len);
122
+ return { value, nextOffset: offset + 4 + len };
123
+ }
124
+ function base64Decode2(str) {
125
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
126
+ while (base64.length % 4) {
127
+ base64 += "=";
128
+ }
129
+ const binary = atob(base64);
130
+ const bytes = new Uint8Array(binary.length);
131
+ for (let i = 0; i < binary.length; i++) {
132
+ bytes[i] = binary.charCodeAt(i);
133
+ }
134
+ return bytes;
135
+ }
136
+ function parsePublicKey(keyString) {
137
+ const trimmed = keyString.trim();
138
+ for (const [sshType, keyType] of Object.entries(SSH_KEY_TYPES)) {
139
+ if (trimmed.startsWith(`${sshType} `)) {
140
+ const parts = trimmed.split(" ");
141
+ if (parts.length < 2) {
142
+ throw new Error("Invalid SSH key format");
143
+ }
144
+ const keyData = base64Decode2(parts[1]);
145
+ const { value: typeBytes, nextOffset: afterType } = readSSHString(
146
+ keyData,
147
+ 0
148
+ );
149
+ const typeStr = new TextDecoder().decode(typeBytes);
150
+ if (typeStr !== sshType) {
151
+ throw new Error(
152
+ `Key type mismatch: expected ${sshType}, got ${typeStr}`
153
+ );
154
+ }
155
+ if (keyType === "ed25519") {
156
+ const { value: rawKey } = readSSHString(keyData, afterType);
157
+ if (rawKey.length !== 32) {
158
+ throw new Error("Invalid Ed25519 key length");
159
+ }
160
+ return { type: "ed25519", keyBytes: rawKey };
161
+ }
162
+ const { nextOffset: afterCurve } = readSSHString(keyData, afterType);
163
+ const { value: point } = readSSHString(keyData, afterCurve);
164
+ return { type: keyType, keyBytes: point };
165
+ }
166
+ }
167
+ const decoded = base64Decode2(trimmed);
168
+ if (decoded.length === 32) {
169
+ return { type: "ed25519", keyBytes: decoded };
170
+ }
171
+ throw new Error("Unsupported public key format");
172
+ }
173
+ async function signEd25519(message, privateKey) {
174
+ const sig = await ed.signAsync(message, privateKey);
175
+ return btoa(String.fromCharCode(...sig));
176
+ }
177
+ function signECDSA(message, privateKey, curve) {
178
+ const curves = { p256, p384, p521 };
179
+ const sigBytes = curves[curve].sign(message, privateKey, { prehash: true });
180
+ return btoa(String.fromCharCode(...sigBytes));
181
+ }
182
+ async function createSignatureEnvelope(content, privateKey, publicKey, options) {
183
+ const parsed = parsePublicKey(publicKey);
184
+ let sig;
185
+ if (parsed.type === "ed25519") {
186
+ sig = await signEd25519(content, privateKey);
187
+ } else {
188
+ const curve = parsed.type.replace("ecdsa-", "");
189
+ sig = signECDSA(content, privateKey, curve);
190
+ }
191
+ return {
192
+ sig,
193
+ publicKey,
194
+ email: options?.email,
195
+ githubUser: options?.githubUser
196
+ };
197
+ }
198
+ async function verifySignature(parsedKey, message, signature) {
199
+ try {
200
+ switch (parsedKey.type) {
201
+ case "ed25519":
202
+ return await ed.verifyAsync(signature, message, parsedKey.keyBytes);
203
+ case "ecdsa-p256":
204
+ return p256.verify(signature, message, parsedKey.keyBytes, {
205
+ prehash: true
206
+ });
207
+ case "ecdsa-p384":
208
+ return p384.verify(signature, message, parsedKey.keyBytes, {
209
+ prehash: true
210
+ });
211
+ case "ecdsa-p521":
212
+ return p521.verify(signature, message, parsedKey.keyBytes, {
213
+ prehash: true
214
+ });
215
+ default:
216
+ return false;
217
+ }
218
+ } catch {
219
+ return false;
220
+ }
221
+ }
222
+ async function computeFingerprint(publicKey) {
223
+ const hash = await crypto.subtle.digest(
224
+ "SHA-256",
225
+ publicKey.buffer
226
+ );
227
+ const hashArray = new Uint8Array(hash);
228
+ const base64 = btoa(String.fromCharCode(...hashArray));
229
+ return `SHA256:${base64.replace(/=+$/, "")}`;
230
+ }
231
+ function formatFingerprint(fingerprint) {
232
+ const base64Part = fingerprint.startsWith("SHA256:") ? fingerprint.slice(7) : fingerprint;
233
+ try {
234
+ const binary = atob(base64Part);
235
+ const hex = Array.from(binary).map((c) => c.charCodeAt(0).toString(16).padStart(2, "0")).join("").toUpperCase();
236
+ const short = hex.slice(0, 16);
237
+ return short.match(/.{4}/g)?.join(" ") || short;
238
+ } catch {
239
+ return fingerprint.slice(0, 16).toUpperCase();
240
+ }
241
+ }
242
+
107
243
  // src/client.ts
108
244
  var DEFAULT_BASE_URL = "https://mux.md";
109
245
  async function upload(data, fileInfo, options = {}) {
@@ -112,11 +248,20 @@ async function upload(data, fileInfo, options = {}) {
112
248
  const salt = generateSalt();
113
249
  const iv = generateIV();
114
250
  const cryptoKey = await deriveKey(keyMaterial, salt);
251
+ let signatureEnvelope;
252
+ if (options.sign) {
253
+ signatureEnvelope = await createSignatureEnvelope(
254
+ data,
255
+ options.sign.privateKey,
256
+ options.sign.publicKey,
257
+ { email: options.sign.email, githubUser: options.sign.githubUser }
258
+ );
259
+ }
115
260
  let plaintext;
116
- if (options.signature) {
261
+ if (signatureEnvelope) {
117
262
  const signed = {
118
263
  content: new TextDecoder().decode(data),
119
- sig: options.signature
264
+ sig: signatureEnvelope
120
265
  };
121
266
  plaintext = new TextEncoder().encode(JSON.stringify(signed));
122
267
  } else {
@@ -308,143 +453,6 @@ async function setExpiration(id, mutateKey, expiresAt, options = {}) {
308
453
  }
309
454
  return response.json();
310
455
  }
311
-
312
- // src/signing.ts
313
- import * as ed from "@noble/ed25519";
314
- import { p256, p384, p521 } from "@noble/curves/nist.js";
315
- import { sha512 } from "@noble/hashes/sha2.js";
316
- ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
317
- var SSH_KEY_TYPES = {
318
- "ssh-ed25519": "ed25519",
319
- "ecdsa-sha2-nistp256": "ecdsa-p256",
320
- "ecdsa-sha2-nistp384": "ecdsa-p384",
321
- "ecdsa-sha2-nistp521": "ecdsa-p521"
322
- };
323
- function readSSHString(data, offset) {
324
- const view = new DataView(data.buffer, data.byteOffset);
325
- const len = view.getUint32(offset);
326
- const value = data.slice(offset + 4, offset + 4 + len);
327
- return { value, nextOffset: offset + 4 + len };
328
- }
329
- function base64Decode2(str) {
330
- let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
331
- while (base64.length % 4) {
332
- base64 += "=";
333
- }
334
- const binary = atob(base64);
335
- const bytes = new Uint8Array(binary.length);
336
- for (let i = 0; i < binary.length; i++) {
337
- bytes[i] = binary.charCodeAt(i);
338
- }
339
- return bytes;
340
- }
341
- function parsePublicKey(keyString) {
342
- const trimmed = keyString.trim();
343
- for (const [sshType, keyType] of Object.entries(SSH_KEY_TYPES)) {
344
- if (trimmed.startsWith(`${sshType} `)) {
345
- const parts = trimmed.split(" ");
346
- if (parts.length < 2) {
347
- throw new Error("Invalid SSH key format");
348
- }
349
- const keyData = base64Decode2(parts[1]);
350
- const { value: typeBytes, nextOffset: afterType } = readSSHString(
351
- keyData,
352
- 0
353
- );
354
- const typeStr = new TextDecoder().decode(typeBytes);
355
- if (typeStr !== sshType) {
356
- throw new Error(
357
- `Key type mismatch: expected ${sshType}, got ${typeStr}`
358
- );
359
- }
360
- if (keyType === "ed25519") {
361
- const { value: rawKey } = readSSHString(keyData, afterType);
362
- if (rawKey.length !== 32) {
363
- throw new Error("Invalid Ed25519 key length");
364
- }
365
- return { type: "ed25519", keyBytes: rawKey };
366
- }
367
- const { nextOffset: afterCurve } = readSSHString(keyData, afterType);
368
- const { value: point } = readSSHString(keyData, afterCurve);
369
- return { type: keyType, keyBytes: point };
370
- }
371
- }
372
- const decoded = base64Decode2(trimmed);
373
- if (decoded.length === 32) {
374
- return { type: "ed25519", keyBytes: decoded };
375
- }
376
- throw new Error("Unsupported public key format");
377
- }
378
- async function signEd25519(message, privateKey) {
379
- const sig = await ed.signAsync(message, privateKey);
380
- return btoa(String.fromCharCode(...sig));
381
- }
382
- function signECDSA(message, privateKey, curve) {
383
- const curves = { p256, p384, p521 };
384
- const sig = curves[curve].sign(message, privateKey, { prehash: true });
385
- const sigBytes = sig.toCompactRawBytes();
386
- return btoa(String.fromCharCode(...sigBytes));
387
- }
388
- async function createSignatureEnvelope(content, privateKey, publicKey, options) {
389
- const parsed = parsePublicKey(publicKey);
390
- let sig;
391
- if (parsed.type === "ed25519") {
392
- sig = await signEd25519(content, privateKey);
393
- } else {
394
- const curve = parsed.type.replace("ecdsa-", "");
395
- sig = signECDSA(content, privateKey, curve);
396
- }
397
- return {
398
- sig,
399
- publicKey,
400
- email: options?.email,
401
- githubUser: options?.githubUser
402
- };
403
- }
404
- async function verifySignature(parsedKey, message, signature) {
405
- try {
406
- switch (parsedKey.type) {
407
- case "ed25519":
408
- return await ed.verifyAsync(signature, message, parsedKey.keyBytes);
409
- case "ecdsa-p256":
410
- return p256.verify(signature, message, parsedKey.keyBytes, {
411
- prehash: true
412
- });
413
- case "ecdsa-p384":
414
- return p384.verify(signature, message, parsedKey.keyBytes, {
415
- prehash: true
416
- });
417
- case "ecdsa-p521":
418
- return p521.verify(signature, message, parsedKey.keyBytes, {
419
- prehash: true
420
- });
421
- default:
422
- return false;
423
- }
424
- } catch {
425
- return false;
426
- }
427
- }
428
- async function computeFingerprint(publicKey) {
429
- const hash = await crypto.subtle.digest(
430
- "SHA-256",
431
- publicKey.buffer
432
- );
433
- const hashArray = new Uint8Array(hash);
434
- const base64 = btoa(String.fromCharCode(...hashArray));
435
- return `SHA256:${base64.replace(/=+$/, "")}`;
436
- }
437
- function formatFingerprint(fingerprint) {
438
- const base64Part = fingerprint.startsWith("SHA256:") ? fingerprint.slice(7) : fingerprint;
439
- try {
440
- const binary = atob(base64Part);
441
- const hex = Array.from(binary).map((c) => c.charCodeAt(0).toString(16).padStart(2, "0")).join("").toUpperCase();
442
- const short = hex.slice(0, 16);
443
- return short.match(/.{4}/g)?.join(" ") || short;
444
- } catch {
445
- return fingerprint.slice(0, 16).toUpperCase();
446
- }
447
- }
448
456
  export {
449
457
  base64Decode,
450
458
  base64Encode,
@@ -452,7 +460,6 @@ export {
452
460
  base64UrlEncode,
453
461
  buildUrl,
454
462
  computeFingerprint,
455
- createSignatureEnvelope,
456
463
  decrypt,
457
464
  deleteFile,
458
465
  deriveKey,
@@ -468,8 +475,6 @@ export {
468
475
  parsePublicKey,
469
476
  parseUrl,
470
477
  setExpiration,
471
- signECDSA,
472
- signEd25519,
473
478
  upload,
474
479
  verifySignature
475
480
  };