@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 +150 -0
- package/dist/index.cjs +538 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +283 -0
- package/dist/index.d.ts +283 -0
- package/dist/index.js +476 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
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
|