@xtr-dev/rondevu-server 0.5.12 → 0.5.13
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 +9 -21
- package/dist/index.js +8 -1
- package/dist/index.js.map +2 -2
- package/migrations/0009_public_key_auth.sql +74 -0
- package/migrations/fresh_schema.sql +20 -21
- package/package.json +2 -1
- package/src/config.ts +1 -47
- package/src/crypto.ts +70 -304
- package/src/index.ts +2 -3
- package/src/rpc.ts +90 -272
- package/src/storage/d1.ts +72 -235
- package/src/storage/factory.ts +4 -17
- package/src/storage/memory.ts +46 -151
- package/src/storage/mysql.ts +66 -187
- package/src/storage/postgres.ts +66 -186
- package/src/storage/sqlite.ts +65 -194
- package/src/storage/types.ts +30 -88
- package/src/worker.ts +4 -9
package/src/crypto.ts
CHANGED
|
@@ -1,86 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Crypto utilities for
|
|
3
|
-
* Uses
|
|
2
|
+
* Crypto utilities for Ed25519 signature verification and validation
|
|
3
|
+
* Uses @noble/ed25519 for Ed25519 operations and Web Crypto API for SHA-512
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Buffer } from 'node:buffer';
|
|
7
|
+
import * as ed from '@noble/ed25519';
|
|
7
8
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Generates a random credential name
|
|
16
|
-
* Format: {adjective}-{noun}-{random}
|
|
17
|
-
* Example: "brave-tiger-7a3f2b1c9d8e", "quick-river-9b2e4c1a5f3d"
|
|
18
|
-
*/
|
|
19
|
-
export function generateCredentialName(): string {
|
|
20
|
-
const adjectives = [
|
|
21
|
-
'brave', 'calm', 'eager', 'fancy', 'gentle', 'happy', 'jolly', 'kind',
|
|
22
|
-
'lively', 'merry', 'nice', 'proud', 'quiet', 'swift', 'witty', 'young',
|
|
23
|
-
'bright', 'clever', 'daring', 'fair', 'grand', 'humble', 'noble', 'quick'
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
const nouns = [
|
|
27
|
-
'tiger', 'eagle', 'river', 'mountain', 'ocean', 'forest', 'desert', 'valley',
|
|
28
|
-
'thunder', 'wind', 'fire', 'stone', 'cloud', 'star', 'moon', 'sun',
|
|
29
|
-
'wolf', 'bear', 'hawk', 'lion', 'fox', 'deer', 'owl', 'swan'
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
|
|
33
|
-
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
|
34
|
-
|
|
35
|
-
// Generate 16-character hex suffix for uniqueness (8 bytes = 2^64 combinations)
|
|
36
|
-
// With 576 adjective-noun pairs, total space: 576 × 2^64 ≈ 1.06 × 10^22 names
|
|
37
|
-
// Birthday paradox collision at ~4.3 billion credentials (extremely safe for large deployments)
|
|
38
|
-
// Increased from 6 bytes to 8 bytes for maximum collision resistance
|
|
39
|
-
const random = crypto.getRandomValues(new Uint8Array(8));
|
|
40
|
-
const hex = Array.from(random).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
41
|
-
|
|
42
|
-
return `${adjective}-${noun}-${hex}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Generates a random secret (API key style)
|
|
47
|
-
* Format: 64-character hex string (256 bits of entropy)
|
|
48
|
-
* 256 bits provides optimal security for HMAC-SHA256 and future-proofs against brute force
|
|
49
|
-
*/
|
|
50
|
-
export function generateSecret(): string {
|
|
51
|
-
const bytes = crypto.getRandomValues(new Uint8Array(32)); // 32 bytes = 256 bits
|
|
52
|
-
const secret = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
53
|
-
|
|
54
|
-
// Validation: Ensure output is exactly 64 characters and valid hex
|
|
55
|
-
if (secret.length !== 64) {
|
|
56
|
-
throw new Error('Secret generation failed: invalid length');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Validate all characters are valid hex digits (0-9, a-f)
|
|
60
|
-
for (let i = 0; i < secret.length; i++) {
|
|
61
|
-
const c = secret[i];
|
|
62
|
-
if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) {
|
|
63
|
-
throw new Error(`Secret generation failed: invalid hex character at position ${i}: '${c}'`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return secret;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ===== Secret Encryption/Decryption (Database Storage) =====
|
|
9
|
+
// Configure @noble/ed25519 to use Web Crypto API's SHA-512
|
|
10
|
+
// Required for both Node.js and Cloudflare Workers compatibility
|
|
11
|
+
ed.hashes.sha512Async = async (message: Uint8Array): Promise<Uint8Array> => {
|
|
12
|
+
const hashBuffer = await crypto.subtle.digest('SHA-512', message);
|
|
13
|
+
return new Uint8Array(hashBuffer);
|
|
14
|
+
};
|
|
71
15
|
|
|
72
16
|
/**
|
|
73
17
|
* Convert hex string to byte array with validation
|
|
74
|
-
* @param hex Hex string (must be even length)
|
|
75
|
-
* @returns Uint8Array of bytes
|
|
76
18
|
*/
|
|
77
19
|
function hexToBytes(hex: string): Uint8Array {
|
|
78
20
|
if (hex.length % 2 !== 0) {
|
|
79
21
|
throw new Error('Hex string must have even length');
|
|
80
22
|
}
|
|
81
23
|
|
|
82
|
-
// Pre-validate that all characters are valid hex digits (0-9, a-f, A-F)
|
|
83
|
-
// This prevents parseInt from silently truncating invalid input like "0z" -> 0
|
|
84
24
|
for (let i = 0; i < hex.length; i++) {
|
|
85
25
|
const c = hex[i].toLowerCase();
|
|
86
26
|
if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) {
|
|
@@ -103,192 +43,9 @@ function hexToBytes(hex: string): Uint8Array {
|
|
|
103
43
|
}
|
|
104
44
|
|
|
105
45
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* @param secret The plaintext secret to encrypt
|
|
110
|
-
* @param masterKeyHex The master encryption key (64-char hex = 32 bytes)
|
|
111
|
-
* @returns Encrypted secret in format "iv:ciphertext"
|
|
112
|
-
*/
|
|
113
|
-
export async function encryptSecret(secret: string, masterKeyHex: string): Promise<string> {
|
|
114
|
-
// Validate master key
|
|
115
|
-
if (!masterKeyHex || masterKeyHex.length !== 64) {
|
|
116
|
-
throw new Error('Master key must be 64-character hex string (32 bytes)');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Convert master key from hex to bytes (with validation)
|
|
120
|
-
const keyBytes = hexToBytes(masterKeyHex);
|
|
121
|
-
|
|
122
|
-
// Import master key
|
|
123
|
-
const key = await crypto.subtle.importKey(
|
|
124
|
-
'raw',
|
|
125
|
-
keyBytes,
|
|
126
|
-
{ name: 'AES-GCM', length: 256 },
|
|
127
|
-
false,
|
|
128
|
-
['encrypt']
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
// Generate random IV (12 bytes for AES-GCM)
|
|
132
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
133
|
-
|
|
134
|
-
// Encrypt secret
|
|
135
|
-
const encoder = new TextEncoder();
|
|
136
|
-
const secretBytes = encoder.encode(secret);
|
|
137
|
-
|
|
138
|
-
// AES-GCM returns ciphertext with auth tag already appended (no manual splitting needed)
|
|
139
|
-
const ciphertext = await crypto.subtle.encrypt(
|
|
140
|
-
{ name: 'AES-GCM', iv, tagLength: 128 },
|
|
141
|
-
key,
|
|
142
|
-
secretBytes
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
// Convert to hex: iv:ciphertext (ciphertext includes 16-byte auth tag at end)
|
|
146
|
-
const ivHex = Array.from(iv).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
147
|
-
const ciphertextHex = Array.from(new Uint8Array(ciphertext))
|
|
148
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
149
|
-
.join('');
|
|
150
|
-
|
|
151
|
-
return `${ivHex}:${ciphertextHex}`;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Decrypt a secret using AES-256-GCM with master key
|
|
156
|
-
*
|
|
157
|
-
* @param encryptedSecret Encrypted secret in format "iv:ciphertext" (ciphertext includes auth tag)
|
|
158
|
-
* @param masterKeyHex The master encryption key (64-char hex = 32 bytes)
|
|
159
|
-
* @returns Decrypted plaintext secret
|
|
160
|
-
*/
|
|
161
|
-
export async function decryptSecret(encryptedSecret: string, masterKeyHex: string): Promise<string> {
|
|
162
|
-
// Validate master key
|
|
163
|
-
if (!masterKeyHex || masterKeyHex.length !== 64) {
|
|
164
|
-
throw new Error('Master key must be 64-character hex string (32 bytes)');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Parse encrypted format: iv:ciphertext
|
|
168
|
-
const parts = encryptedSecret.split(':');
|
|
169
|
-
if (parts.length !== 2) {
|
|
170
|
-
throw new Error('Invalid encrypted secret format (expected iv:ciphertext)');
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const [ivHex, ciphertextHex] = parts;
|
|
174
|
-
|
|
175
|
-
// Validate IV length (must be 12 bytes = 24 hex characters for AES-GCM)
|
|
176
|
-
if (ivHex.length !== 24) {
|
|
177
|
-
throw new Error('Invalid IV length (expected 12 bytes = 24 hex characters)');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Validate ciphertext length (must include at least 16-byte auth tag)
|
|
181
|
-
// Minimum: 16 bytes for auth tag = 32 hex characters
|
|
182
|
-
if (ciphertextHex.length < 32) {
|
|
183
|
-
throw new Error('Invalid ciphertext length (must include 16-byte auth tag)');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Convert from hex to bytes (with validation)
|
|
187
|
-
const iv = hexToBytes(ivHex);
|
|
188
|
-
const ciphertext = hexToBytes(ciphertextHex);
|
|
189
|
-
|
|
190
|
-
// Convert master key from hex to bytes (with validation)
|
|
191
|
-
const keyBytes = hexToBytes(masterKeyHex);
|
|
192
|
-
|
|
193
|
-
// Import master key
|
|
194
|
-
const key = await crypto.subtle.importKey(
|
|
195
|
-
'raw',
|
|
196
|
-
keyBytes,
|
|
197
|
-
{ name: 'AES-GCM', length: 256 },
|
|
198
|
-
false,
|
|
199
|
-
['decrypt']
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
// Decrypt (ciphertext already includes 16-byte auth tag at end)
|
|
203
|
-
const decryptedBytes = await crypto.subtle.decrypt(
|
|
204
|
-
{ name: 'AES-GCM', iv, tagLength: 128 },
|
|
205
|
-
key,
|
|
206
|
-
ciphertext
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
// Convert to string
|
|
210
|
-
const decoder = new TextDecoder();
|
|
211
|
-
return decoder.decode(decryptedBytes);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// ===== HMAC Signature Generation and Verification =====
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Generate HMAC-SHA256 signature for request authentication
|
|
218
|
-
* Uses Web Crypto API for compatibility with both Node.js and Cloudflare Workers
|
|
219
|
-
*
|
|
220
|
-
* @param secret The credential secret (hex string)
|
|
221
|
-
* @param message The message to sign (typically: timestamp + method + params)
|
|
222
|
-
* @returns Promise<string> Base64-encoded signature
|
|
223
|
-
*/
|
|
224
|
-
export async function generateSignature(secret: string, message: string): Promise<string> {
|
|
225
|
-
// Convert secret from hex to bytes (with validation)
|
|
226
|
-
const secretBytes = hexToBytes(secret);
|
|
227
|
-
|
|
228
|
-
// Import secret as HMAC key
|
|
229
|
-
const key = await crypto.subtle.importKey(
|
|
230
|
-
'raw',
|
|
231
|
-
secretBytes,
|
|
232
|
-
{ name: 'HMAC', hash: 'SHA-256' },
|
|
233
|
-
false,
|
|
234
|
-
['sign']
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
// Convert message to bytes
|
|
238
|
-
const encoder = new TextEncoder();
|
|
239
|
-
const messageBytes = encoder.encode(message);
|
|
240
|
-
|
|
241
|
-
// Generate HMAC signature
|
|
242
|
-
const signatureBytes = await crypto.subtle.sign('HMAC', key, messageBytes);
|
|
243
|
-
|
|
244
|
-
// Convert to base64
|
|
245
|
-
return Buffer.from(signatureBytes).toString('base64');
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Verify HMAC-SHA256 signature for request authentication
|
|
250
|
-
* Uses crypto.subtle.verify() for constant-time comparison
|
|
251
|
-
*
|
|
252
|
-
* @param secret The credential secret (hex string)
|
|
253
|
-
* @param message The message that was signed
|
|
254
|
-
* @param signature The signature to verify (base64)
|
|
255
|
-
* @returns Promise<boolean> True if signature is valid
|
|
256
|
-
*/
|
|
257
|
-
export async function verifySignature(secret: string, message: string, signature: string): Promise<boolean> {
|
|
258
|
-
try {
|
|
259
|
-
// Convert secret from hex to bytes (with validation)
|
|
260
|
-
const secretBytes = hexToBytes(secret);
|
|
261
|
-
|
|
262
|
-
// Import secret as HMAC key for verification
|
|
263
|
-
const key = await crypto.subtle.importKey(
|
|
264
|
-
'raw',
|
|
265
|
-
secretBytes,
|
|
266
|
-
{ name: 'HMAC', hash: 'SHA-256' },
|
|
267
|
-
false,
|
|
268
|
-
['verify']
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
// Convert message to bytes
|
|
272
|
-
const encoder = new TextEncoder();
|
|
273
|
-
const messageBytes = encoder.encode(message);
|
|
274
|
-
|
|
275
|
-
// Convert signature from base64 to bytes
|
|
276
|
-
const signatureBytes = Buffer.from(signature, 'base64');
|
|
277
|
-
|
|
278
|
-
// Use Web Crypto API's verify() for constant-time comparison
|
|
279
|
-
// This is cryptographically secure and resistant to timing attacks
|
|
280
|
-
return await crypto.subtle.verify('HMAC', key, signatureBytes, messageBytes);
|
|
281
|
-
} catch (error) {
|
|
282
|
-
// Log error for debugging (helps identify implementation bugs)
|
|
283
|
-
console.error('Signature verification error:', error);
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Canonical JSON serialization with sorted keys
|
|
290
|
-
* Ensures deterministic output regardless of property insertion order
|
|
291
|
-
* Must match client's canonicalJSON implementation exactly
|
|
46
|
+
* Canonical JSON serialization with sorted keys.
|
|
47
|
+
* Ensures deterministic output regardless of property insertion order.
|
|
48
|
+
* Must match client's canonicalJSON implementation exactly.
|
|
292
49
|
*/
|
|
293
50
|
function canonicalJSON(obj: any, depth: number = 0): string {
|
|
294
51
|
const MAX_DEPTH = 100;
|
|
@@ -318,46 +75,29 @@ function canonicalJSON(obj: any, depth: number = 0): string {
|
|
|
318
75
|
}
|
|
319
76
|
|
|
320
77
|
/**
|
|
321
|
-
* Build the message string for signing
|
|
78
|
+
* Build the message string for signing.
|
|
322
79
|
* Format: timestamp:nonce:method:canonicalJSON(params || {})
|
|
323
|
-
* Uses colons as delimiters to prevent collision attacks
|
|
324
|
-
* Includes nonce to prevent signature reuse within timestamp window
|
|
325
|
-
* Uses canonical JSON (sorted keys) for deterministic serialization
|
|
326
|
-
*
|
|
327
|
-
* @param timestamp Unix timestamp in milliseconds
|
|
328
|
-
* @param nonce Cryptographic nonce (UUID v4) to prevent replay attacks
|
|
329
|
-
* @param method RPC method name
|
|
330
|
-
* @param params RPC method parameters (optional)
|
|
331
|
-
* @returns String to be signed
|
|
332
80
|
*/
|
|
333
81
|
export function buildSignatureMessage(timestamp: number, nonce: string, method: string, params?: any): string {
|
|
334
|
-
// Validate nonce is UUID v4 format
|
|
335
|
-
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (8-4-4-4-12 hex digits with dashes)
|
|
336
|
-
// Use simple format checks instead of regex to avoid any timing or ReDoS concerns
|
|
337
|
-
|
|
338
|
-
// Check total length (36 characters for UUID v4)
|
|
82
|
+
// Validate nonce is UUID v4 format
|
|
339
83
|
if (nonce.length !== 36) {
|
|
340
84
|
throw new Error('Nonce must be a valid UUID v4 (use crypto.randomUUID())');
|
|
341
85
|
}
|
|
342
86
|
|
|
343
|
-
// Check dash positions (indices 8, 13, 18, 23)
|
|
344
87
|
if (nonce[8] !== '-' || nonce[13] !== '-' || nonce[18] !== '-' || nonce[23] !== '-') {
|
|
345
88
|
throw new Error('Nonce must be a valid UUID v4 (use crypto.randomUUID())');
|
|
346
89
|
}
|
|
347
90
|
|
|
348
|
-
// Check version (character at index 14 must be '4')
|
|
349
91
|
if (nonce[14] !== '4') {
|
|
350
92
|
throw new Error('Nonce must be a valid UUID v4 (use crypto.randomUUID())');
|
|
351
93
|
}
|
|
352
94
|
|
|
353
|
-
// Check variant (character at index 19 must be 8, 9, a, or b)
|
|
354
95
|
const variant = nonce[19].toLowerCase();
|
|
355
96
|
if (variant !== '8' && variant !== '9' && variant !== 'a' && variant !== 'b') {
|
|
356
97
|
throw new Error('Nonce must be a valid UUID v4 (use crypto.randomUUID())');
|
|
357
98
|
}
|
|
358
99
|
|
|
359
|
-
|
|
360
|
-
const hexChars = nonce.replace(/-/g, ''); // Remove dashes
|
|
100
|
+
const hexChars = nonce.replace(/-/g, '');
|
|
361
101
|
for (let i = 0; i < hexChars.length; i++) {
|
|
362
102
|
const c = hexChars[i].toLowerCase();
|
|
363
103
|
if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) {
|
|
@@ -365,53 +105,83 @@ export function buildSignatureMessage(timestamp: number, nonce: string, method:
|
|
|
365
105
|
}
|
|
366
106
|
}
|
|
367
107
|
|
|
368
|
-
// Use canonical JSON (sorted keys) to match client's signature
|
|
369
108
|
const paramsStr = canonicalJSON(params || {});
|
|
370
|
-
// Use delimiters to prevent collision: timestamp=12,method="34" vs timestamp=1,method="234"
|
|
371
|
-
// Include nonce to make each request unique (prevents signature reuse in same millisecond)
|
|
372
109
|
return `${timestamp}:${nonce}:${method}:${paramsStr}`;
|
|
373
110
|
}
|
|
374
111
|
|
|
375
|
-
// =====
|
|
112
|
+
// ===== Ed25519 Public Key Identity =====
|
|
113
|
+
|
|
114
|
+
const ED25519_PUBLIC_KEY_LENGTH = 32; // 32 bytes = 64 hex chars
|
|
115
|
+
const ED25519_SIGNATURE_LENGTH = 64; // 64 bytes
|
|
376
116
|
|
|
377
117
|
/**
|
|
378
|
-
* Validates
|
|
379
|
-
*
|
|
118
|
+
* Validates an Ed25519 public key format.
|
|
119
|
+
* @param publicKey 64-character lowercase hex string (32 bytes)
|
|
380
120
|
*/
|
|
381
|
-
export function
|
|
382
|
-
if (typeof
|
|
383
|
-
return { valid: false, error: '
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (username.length < USERNAME_MIN_LENGTH) {
|
|
387
|
-
return { valid: false, error: `Username must be at least ${USERNAME_MIN_LENGTH} characters` };
|
|
121
|
+
export function validatePublicKey(publicKey: string): { valid: boolean; error?: string } {
|
|
122
|
+
if (typeof publicKey !== 'string') {
|
|
123
|
+
return { valid: false, error: 'Public key must be a string' };
|
|
388
124
|
}
|
|
389
125
|
|
|
390
|
-
if (
|
|
391
|
-
return { valid: false, error: `
|
|
126
|
+
if (publicKey.length !== ED25519_PUBLIC_KEY_LENGTH * 2) {
|
|
127
|
+
return { valid: false, error: `Public key must be ${ED25519_PUBLIC_KEY_LENGTH * 2} hex characters (${ED25519_PUBLIC_KEY_LENGTH} bytes)` };
|
|
392
128
|
}
|
|
393
129
|
|
|
394
|
-
|
|
395
|
-
|
|
130
|
+
for (let i = 0; i < publicKey.length; i++) {
|
|
131
|
+
const c = publicKey[i];
|
|
132
|
+
if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) {
|
|
133
|
+
return { valid: false, error: `Invalid hex character at position ${i}: '${c}' (use lowercase hex)` };
|
|
134
|
+
}
|
|
396
135
|
}
|
|
397
136
|
|
|
398
137
|
return { valid: true };
|
|
399
138
|
}
|
|
400
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Verify an Ed25519 signature.
|
|
142
|
+
* @param publicKey Signer's public key (64-char hex)
|
|
143
|
+
* @param message Message that was signed
|
|
144
|
+
* @param signature Signature to verify (base64 encoded)
|
|
145
|
+
*/
|
|
146
|
+
export async function verifyEd25519Signature(
|
|
147
|
+
publicKey: string,
|
|
148
|
+
message: string,
|
|
149
|
+
signature: string
|
|
150
|
+
): Promise<boolean> {
|
|
151
|
+
try {
|
|
152
|
+
const pkValidation = validatePublicKey(publicKey);
|
|
153
|
+
if (!pkValidation.valid) {
|
|
154
|
+
console.error('Ed25519 verification error: invalid public key:', pkValidation.error);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const publicKeyBytes = hexToBytes(publicKey);
|
|
159
|
+
const encoder = new TextEncoder();
|
|
160
|
+
const messageBytes = encoder.encode(message);
|
|
161
|
+
const signatureBytes = Buffer.from(signature, 'base64');
|
|
162
|
+
|
|
163
|
+
if (signatureBytes.length !== ED25519_SIGNATURE_LENGTH) {
|
|
164
|
+
console.error(`Ed25519 verification error: signature length ${signatureBytes.length}, expected ${ED25519_SIGNATURE_LENGTH}`);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return await ed.verifyAsync(signatureBytes, messageBytes, publicKeyBytes);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('Ed25519 verification error:', error);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
401
175
|
// ===== Tag Validation =====
|
|
402
176
|
|
|
403
|
-
// Tag validation constants
|
|
404
177
|
const TAG_MIN_LENGTH = 1;
|
|
405
178
|
const TAG_MAX_LENGTH = 64;
|
|
406
179
|
const TAG_REGEX = /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/;
|
|
407
180
|
|
|
408
181
|
/**
|
|
409
|
-
* Validates a single tag format
|
|
410
|
-
* Rules: 1-64 chars, lowercase alphanumeric with optional dots/dashes
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
* Valid examples: "chat", "video-call", "com.example.service", "v2"
|
|
414
|
-
* Invalid examples: "", "UPPERCASE", "-starts-dash", "ends-dash-"
|
|
182
|
+
* Validates a single tag format.
|
|
183
|
+
* Rules: 1-64 chars, lowercase alphanumeric with optional dots/dashes,
|
|
184
|
+
* must start and end with alphanumeric character.
|
|
415
185
|
*/
|
|
416
186
|
export function validateTag(tag: string): { valid: boolean; error?: string } {
|
|
417
187
|
if (typeof tag !== 'string') {
|
|
@@ -426,7 +196,6 @@ export function validateTag(tag: string): { valid: boolean; error?: string } {
|
|
|
426
196
|
return { valid: false, error: `Tag must be at most ${TAG_MAX_LENGTH} characters` };
|
|
427
197
|
}
|
|
428
198
|
|
|
429
|
-
// Single character tags just need to be alphanumeric
|
|
430
199
|
if (tag.length === 1) {
|
|
431
200
|
if (!/^[a-z0-9]$/.test(tag)) {
|
|
432
201
|
return { valid: false, error: 'Tag must be lowercase alphanumeric' };
|
|
@@ -434,7 +203,6 @@ export function validateTag(tag: string): { valid: boolean; error?: string } {
|
|
|
434
203
|
return { valid: true };
|
|
435
204
|
}
|
|
436
205
|
|
|
437
|
-
// Multi-character tags must match the pattern
|
|
438
206
|
if (!TAG_REGEX.test(tag)) {
|
|
439
207
|
return { valid: false, error: 'Tag must be lowercase alphanumeric with optional dots/dashes, and start/end with alphanumeric' };
|
|
440
208
|
}
|
|
@@ -443,7 +211,7 @@ export function validateTag(tag: string): { valid: boolean; error?: string } {
|
|
|
443
211
|
}
|
|
444
212
|
|
|
445
213
|
/**
|
|
446
|
-
* Validates an array of tags
|
|
214
|
+
* Validates an array of tags.
|
|
447
215
|
* @param tags Array of tags to validate
|
|
448
216
|
* @param maxTags Maximum number of tags allowed (default: 20)
|
|
449
217
|
*/
|
|
@@ -460,7 +228,6 @@ export function validateTags(tags: string[], maxTags: number = 20): { valid: boo
|
|
|
460
228
|
return { valid: false, error: `Maximum ${maxTags} tags allowed` };
|
|
461
229
|
}
|
|
462
230
|
|
|
463
|
-
// Validate each tag
|
|
464
231
|
for (let i = 0; i < tags.length; i++) {
|
|
465
232
|
const result = validateTag(tags[i]);
|
|
466
233
|
if (!result.valid) {
|
|
@@ -468,7 +235,6 @@ export function validateTags(tags: string[], maxTags: number = 20): { valid: boo
|
|
|
468
235
|
}
|
|
469
236
|
}
|
|
470
237
|
|
|
471
|
-
// Check for duplicates
|
|
472
238
|
const uniqueTags = new Set(tags);
|
|
473
239
|
if (uniqueTags.size !== tags.length) {
|
|
474
240
|
return { valid: false, error: 'Duplicate tags are not allowed' };
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,6 @@ async function main() {
|
|
|
21
21
|
|
|
22
22
|
const storage: Storage = await createStorage({
|
|
23
23
|
type: config.storageType,
|
|
24
|
-
masterEncryptionKey: config.masterEncryptionKey,
|
|
25
24
|
sqlitePath: config.storagePath,
|
|
26
25
|
connectionString: config.databaseUrl,
|
|
27
26
|
poolSize: config.dbPoolSize,
|
|
@@ -32,9 +31,9 @@ async function main() {
|
|
|
32
31
|
const cleanupTimer = setInterval(async () => {
|
|
33
32
|
try {
|
|
34
33
|
const result = await runCleanup(storage, Date.now());
|
|
35
|
-
const total = result.offers + result.
|
|
34
|
+
const total = result.offers + result.rateLimits + result.nonces;
|
|
36
35
|
if (total > 0) {
|
|
37
|
-
console.log(`Cleanup: ${result.offers} offers, ${result.
|
|
36
|
+
console.log(`Cleanup: ${result.offers} offers, ${result.rateLimits} rate limits, ${result.nonces} nonces`);
|
|
38
37
|
}
|
|
39
38
|
} catch (err) {
|
|
40
39
|
console.error('Cleanup error:', err);
|