mirror of
https://github.com/xtr-dev/rondevu-server.git
synced 2025-12-12 03:43:23 +00:00
feat: Add semver-compatible service discovery with privacy
## Breaking Changes
### Removed Endpoints
- Removed GET /users/:username/services (service listing)
- Services are now completely hidden - cannot be enumerated
### Updated Endpoints
- GET /users/:username/services/:fqn now supports semver matching
- Requesting chat@1.0.0 will match chat@1.2.3, chat@1.5.0, etc.
- Will NOT match chat@2.0.0 (different major version)
## New Features
### Semantic Versioning Support
- Compatible version matching following semver rules (^1.0.0)
- Major version must match exactly
- For major version 0, minor must also match (0.x.y is unstable)
- Available version must be >= requested version
- Prerelease versions require exact match
### Privacy Improvements
- All services are now hidden by default
- No way to enumerate or list services for a username
- Must know exact service name to discover
## Implementation
### Server (src/)
- crypto.ts: Added parseVersion(), isVersionCompatible(), parseServiceFqn()
- storage/types.ts: Added findServicesByName() interface method
- storage/sqlite.ts: Implemented findServicesByName() with LIKE query
- storage/d1.ts: Implemented findServicesByName() with LIKE query
- app.ts: Updated GET /:username/services/:fqn with semver matching
### Semver Matching Logic
- Parse requested version: chat@1.0.0 → {name: "chat", version: "1.0.0"}
- Find all services with matching name: chat@*
- Filter to compatible versions using semver rules
- Return first match (most recently created)
## Examples
Request: chat@1.0.0
Matches: chat@1.0.0, chat@1.2.3, chat@1.9.5
Does NOT match: chat@0.9.0, chat@2.0.0, chat@1.0.0-beta
🤖 Generated with Claude Code
This commit is contained in:
@@ -544,6 +544,20 @@ export class D1Storage implements Storage {
|
||||
return result ? (result as any).uuid : null;
|
||||
}
|
||||
|
||||
async findServicesByName(username: string, serviceName: string): Promise<Service[]> {
|
||||
const result = await this.db.prepare(`
|
||||
SELECT * FROM services
|
||||
WHERE username = ? AND service_fqn LIKE ? AND expires_at > ?
|
||||
ORDER BY created_at DESC
|
||||
`).bind(username, `${serviceName}@%`, Date.now()).all();
|
||||
|
||||
if (!result.results) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.results.map(row => this.rowToService(row as any));
|
||||
}
|
||||
|
||||
async deleteService(serviceId: string, username: string): Promise<boolean> {
|
||||
const result = await this.db.prepare(`
|
||||
DELETE FROM services
|
||||
|
||||
@@ -572,6 +572,18 @@ export class SQLiteStorage implements Storage {
|
||||
return row ? row.uuid : null;
|
||||
}
|
||||
|
||||
async findServicesByName(username: string, serviceName: string): Promise<Service[]> {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM services
|
||||
WHERE username = ? AND service_fqn LIKE ? AND expires_at > ?
|
||||
ORDER BY created_at DESC
|
||||
`);
|
||||
|
||||
const rows = stmt.all(username, `${serviceName}@%`, Date.now()) as any[];
|
||||
|
||||
return rows.map(row => this.rowToService(row));
|
||||
}
|
||||
|
||||
async deleteService(serviceId: string, username: string): Promise<boolean> {
|
||||
const stmt = this.db.prepare(`
|
||||
DELETE FROM services
|
||||
|
||||
@@ -299,6 +299,14 @@ export interface Storage {
|
||||
*/
|
||||
queryService(username: string, serviceFqn: string): Promise<string | null>;
|
||||
|
||||
/**
|
||||
* Finds all services by username and service name (without version)
|
||||
* @param username Username
|
||||
* @param serviceName Service name (e.g., 'com.example.chat')
|
||||
* @returns Array of services with matching service name
|
||||
*/
|
||||
findServicesByName(username: string, serviceName: string): Promise<Service[]>;
|
||||
|
||||
/**
|
||||
* Deletes a service (with ownership verification)
|
||||
* @param serviceId Service ID
|
||||
|
||||
Reference in New Issue
Block a user