mirror of
https://github.com/xtr-dev/rondevu-server.git
synced 2025-12-13 04:13:25 +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:
@@ -228,6 +228,60 @@ export function validateServiceFqn(fqn: string): { valid: boolean; error?: strin
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse semantic version string into components
|
||||
*/
|
||||
export function parseVersion(version: string): { major: number; minor: number; patch: number; prerelease?: string } | null {
|
||||
const match = version.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-z0-9.-]+)?$/);
|
||||
if (!match) return null;
|
||||
|
||||
return {
|
||||
major: parseInt(match[1], 10),
|
||||
minor: parseInt(match[2], 10),
|
||||
patch: parseInt(match[3], 10),
|
||||
prerelease: match[4]?.substring(1), // Remove leading dash
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two versions are compatible (same major version)
|
||||
* Following semver rules: ^1.0.0 matches 1.x.x but not 2.x.x
|
||||
*/
|
||||
export function isVersionCompatible(requested: string, available: string): boolean {
|
||||
const req = parseVersion(requested);
|
||||
const avail = parseVersion(available);
|
||||
|
||||
if (!req || !avail) return false;
|
||||
|
||||
// Major version must match
|
||||
if (req.major !== avail.major) return false;
|
||||
|
||||
// If major is 0, minor must also match (0.x.y is unstable)
|
||||
if (req.major === 0 && req.minor !== avail.minor) return false;
|
||||
|
||||
// Available version must be >= requested version
|
||||
if (avail.minor < req.minor) return false;
|
||||
if (avail.minor === req.minor && avail.patch < req.patch) return false;
|
||||
|
||||
// Prerelease versions are only compatible with exact matches
|
||||
if (req.prerelease && req.prerelease !== avail.prerelease) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse service FQN into service name and version
|
||||
*/
|
||||
export function parseServiceFqn(fqn: string): { serviceName: string; version: string } | null {
|
||||
const parts = fqn.split('@');
|
||||
if (parts.length !== 2) return null;
|
||||
|
||||
return {
|
||||
serviceName: parts[0],
|
||||
version: parts[1],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates timestamp is within acceptable range (prevents replay attacks)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user