@xtr-dev/rondevu-server 0.1.5 → 0.2.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 +217 -69
- package/dist/index.js +1067 -368
- package/dist/index.js.map +4 -4
- package/package.json +3 -2
- package/src/app.ts +339 -271
- package/src/crypto.ts +164 -0
- package/src/storage/d1.ts +295 -119
- package/src/storage/sqlite.ts +309 -107
- package/src/storage/types.ts +159 -29
package/src/crypto.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Crypto utilities for stateless peer authentication
|
|
3
3
|
* Uses Web Crypto API for compatibility with both Node.js and Cloudflare Workers
|
|
4
|
+
* Uses @noble/ed25519 for Ed25519 signature verification
|
|
4
5
|
*/
|
|
5
6
|
|
|
7
|
+
import * as ed25519 from '@noble/ed25519';
|
|
8
|
+
|
|
6
9
|
const ALGORITHM = 'AES-GCM';
|
|
7
10
|
const IV_LENGTH = 12; // 96 bits for GCM
|
|
8
11
|
const KEY_LENGTH = 32; // 256 bits
|
|
9
12
|
|
|
13
|
+
// Username validation
|
|
14
|
+
const USERNAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
15
|
+
const USERNAME_MIN_LENGTH = 3;
|
|
16
|
+
const USERNAME_MAX_LENGTH = 32;
|
|
17
|
+
|
|
18
|
+
// Timestamp validation (5 minutes tolerance)
|
|
19
|
+
const TIMESTAMP_TOLERANCE_MS = 5 * 60 * 1000;
|
|
20
|
+
|
|
10
21
|
/**
|
|
11
22
|
* Generates a random peer ID (16 bytes = 32 hex chars)
|
|
12
23
|
*/
|
|
@@ -147,3 +158,156 @@ export async function validateCredentials(peerId: string, encryptedSecret: strin
|
|
|
147
158
|
return false;
|
|
148
159
|
}
|
|
149
160
|
}
|
|
161
|
+
|
|
162
|
+
// ===== Username and Ed25519 Signature Utilities =====
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Validates username format
|
|
166
|
+
* Rules: 3-32 chars, lowercase alphanumeric + dash, must start/end with alphanumeric
|
|
167
|
+
*/
|
|
168
|
+
export function validateUsername(username: string): { valid: boolean; error?: string } {
|
|
169
|
+
if (typeof username !== 'string') {
|
|
170
|
+
return { valid: false, error: 'Username must be a string' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (username.length < USERNAME_MIN_LENGTH) {
|
|
174
|
+
return { valid: false, error: `Username must be at least ${USERNAME_MIN_LENGTH} characters` };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (username.length > USERNAME_MAX_LENGTH) {
|
|
178
|
+
return { valid: false, error: `Username must be at most ${USERNAME_MAX_LENGTH} characters` };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!USERNAME_REGEX.test(username)) {
|
|
182
|
+
return { valid: false, error: 'Username must be lowercase alphanumeric with optional dashes, and start/end with alphanumeric' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { valid: true };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Validates service FQN format (service-name@version)
|
|
190
|
+
* Service name: reverse domain notation (com.example.service)
|
|
191
|
+
* Version: semantic versioning (1.0.0, 2.1.3-beta, etc.)
|
|
192
|
+
*/
|
|
193
|
+
export function validateServiceFqn(fqn: string): { valid: boolean; error?: string } {
|
|
194
|
+
if (typeof fqn !== 'string') {
|
|
195
|
+
return { valid: false, error: 'Service FQN must be a string' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Split into service name and version
|
|
199
|
+
const parts = fqn.split('@');
|
|
200
|
+
if (parts.length !== 2) {
|
|
201
|
+
return { valid: false, error: 'Service FQN must be in format: service-name@version' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const [serviceName, version] = parts;
|
|
205
|
+
|
|
206
|
+
// Validate service name (reverse domain notation)
|
|
207
|
+
const serviceNameRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
|
|
208
|
+
if (!serviceNameRegex.test(serviceName)) {
|
|
209
|
+
return { valid: false, error: 'Service name must be reverse domain notation (e.g., com.example.service)' };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (serviceName.length < 3 || serviceName.length > 128) {
|
|
213
|
+
return { valid: false, error: 'Service name must be 3-128 characters' };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Validate version (semantic versioning)
|
|
217
|
+
const versionRegex = /^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9.-]+)?$/;
|
|
218
|
+
if (!versionRegex.test(version)) {
|
|
219
|
+
return { valid: false, error: 'Version must be semantic versioning (e.g., 1.0.0, 2.1.3-beta)' };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { valid: true };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Validates timestamp is within acceptable range (prevents replay attacks)
|
|
227
|
+
*/
|
|
228
|
+
export function validateTimestamp(timestamp: number): { valid: boolean; error?: string } {
|
|
229
|
+
if (typeof timestamp !== 'number' || !Number.isFinite(timestamp)) {
|
|
230
|
+
return { valid: false, error: 'Timestamp must be a finite number' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
const diff = Math.abs(now - timestamp);
|
|
235
|
+
|
|
236
|
+
if (diff > TIMESTAMP_TOLERANCE_MS) {
|
|
237
|
+
return { valid: false, error: `Timestamp too old or too far in future (tolerance: ${TIMESTAMP_TOLERANCE_MS / 1000}s)` };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { valid: true };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Verifies Ed25519 signature
|
|
245
|
+
* @param publicKey Base64-encoded Ed25519 public key (32 bytes)
|
|
246
|
+
* @param signature Base64-encoded Ed25519 signature (64 bytes)
|
|
247
|
+
* @param message Message that was signed (UTF-8 string)
|
|
248
|
+
* @returns true if signature is valid, false otherwise
|
|
249
|
+
*/
|
|
250
|
+
export async function verifyEd25519Signature(
|
|
251
|
+
publicKey: string,
|
|
252
|
+
signature: string,
|
|
253
|
+
message: string
|
|
254
|
+
): Promise<boolean> {
|
|
255
|
+
try {
|
|
256
|
+
// Decode base64 to bytes
|
|
257
|
+
const publicKeyBytes = base64ToBytes(publicKey);
|
|
258
|
+
const signatureBytes = base64ToBytes(signature);
|
|
259
|
+
|
|
260
|
+
// Encode message as UTF-8
|
|
261
|
+
const encoder = new TextEncoder();
|
|
262
|
+
const messageBytes = encoder.encode(message);
|
|
263
|
+
|
|
264
|
+
// Verify signature using @noble/ed25519
|
|
265
|
+
const isValid = await ed25519.verify(signatureBytes, messageBytes, publicKeyBytes);
|
|
266
|
+
return isValid;
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.error('Ed25519 signature verification failed:', err);
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Validates a username claim request
|
|
275
|
+
* Verifies format, timestamp, and signature
|
|
276
|
+
*/
|
|
277
|
+
export async function validateUsernameClaim(
|
|
278
|
+
username: string,
|
|
279
|
+
publicKey: string,
|
|
280
|
+
signature: string,
|
|
281
|
+
message: string
|
|
282
|
+
): Promise<{ valid: boolean; error?: string }> {
|
|
283
|
+
// Validate username format
|
|
284
|
+
const usernameCheck = validateUsername(username);
|
|
285
|
+
if (!usernameCheck.valid) {
|
|
286
|
+
return usernameCheck;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Parse message format: "claim:{username}:{timestamp}"
|
|
290
|
+
const parts = message.split(':');
|
|
291
|
+
if (parts.length !== 3 || parts[0] !== 'claim' || parts[1] !== username) {
|
|
292
|
+
return { valid: false, error: 'Invalid message format (expected: claim:{username}:{timestamp})' };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const timestamp = parseInt(parts[2], 10);
|
|
296
|
+
if (isNaN(timestamp)) {
|
|
297
|
+
return { valid: false, error: 'Invalid timestamp in message' };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Validate timestamp
|
|
301
|
+
const timestampCheck = validateTimestamp(timestamp);
|
|
302
|
+
if (!timestampCheck.valid) {
|
|
303
|
+
return timestampCheck;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Verify signature
|
|
307
|
+
const signatureValid = await verifyEd25519Signature(publicKey, signature, message);
|
|
308
|
+
if (!signatureValid) {
|
|
309
|
+
return { valid: false, error: 'Invalid signature' };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { valid: true };
|
|
313
|
+
}
|