Validation: Add comprehensive offer validation
- Validate each offer is an object
- Validate each offer has sdp property
- Validate sdp is a string
- Validate sdp is not empty/whitespace
Impact: Prevents runtime errors from malformed offers
Improves error messages with specific index information
Username claiming is now handled automatically in verifyAuth() when a username
doesn't exist. The separate claimUsername RPC method is no longer needed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The createService storage method expects offers in the request,
but publishService wasn't passing them. This caused undefined
error when d1.ts tried to call request.offers.map().
Now correctly passes offers to createService which handles
creating both the service and all offers atomically.
Auto-claim was incorrectly using validateUsernameClaim() which
expects 'claim:{username}:{timestamp}' message format. This failed
when users tried to auto-claim via publishService or getService.
Now auto-claim only:
- Validates username format
- Verifies signature against the actual message
- Claims the username
This allows implicit username claiming on first authenticated request.
The function expects 4 separate parameters, not an object.
This was causing 'Username must be a string' errors because
the entire object was being passed as the username parameter.
The message validation was missing a continue statement, causing
the handler to continue executing even after pushing an error response.
This led to undefined errors when trying to map over undefined values.
Modified verifyAuth() to automatically claim usernames on first use.
When a username is not claimed and a publicKey is provided in the
RPC request, the server will validate and auto-claim it.
Changes:
- Added publicKey parameter to verifyAuth() function
- Added publicKey field to RpcRequest interface
- Updated RpcHandler type to include publicKey parameter
- Modified all method handlers to pass publicKey to verifyAuth()
- Updated handleRpc() to extract publicKey from requests
🤖 Generated with Claude Code
https://claude.com/claude-code
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
BREAKING CHANGES:
- Replaced REST API with RPC interface
- Single POST /rpc endpoint for all operations
- Removed auth middleware (per-method auth instead)
- Support for batch operations
- Message format changed for all methods
Changes:
- Created src/rpc.ts with all method handlers
- Simplified src/app.ts to only handle /rpc endpoint
- Removed src/middleware/auth.ts
- Updated README.md with complete RPC documentation
This migration aligns the D1 database schema with the unified Ed25519
authentication system that replaced the dual peerId/secret system.
Changes:
- Renames peer_id to username in offers table
- Renames answerer_peer_id to answerer_username in offers table
- Renames peer_id to username in ice_candidates table
- Adds service_fqn column to offers table
- Updates all indexes and foreign keys
- DELETE /services/:fqn uses request body for auth, not query parameters
- Updated anonymous users description to reflect server capabilities
(not client auto-claiming behavior which was removed)
- Remove outdated UUID-based endpoint documentation
- Document actual service:version@username FQN format
- Add /offers/poll combined polling endpoint
- Update all endpoint paths to match actual implementation
- Document ICE candidate role filtering
- Add migration notes from v0.3.x
- Add role and peerId to ICE candidate responses for matching
- Offerers can now see their own candidates (for debugging/sync)
- Answerers can poll same endpoint to get offerer candidates
- Each candidate tagged with role ('offerer' or 'answerer') and peerId
- Enables proper bidirectional ICE candidate exchange
- Change UNIQUE constraint to composite key on separate columns
- Move upsert logic into D1Storage.createService() for atomic operation
- Delete existing service and its offers before inserting new one
- Remove redundant delete logic from app.ts endpoint
- Fixes 'UNIQUE constraint failed: services.service_fqn' error when republishing
- Add GET /offers/poll endpoint for efficient batch polling
- Returns both answered offers and ICE candidates in single request
- Supports timestamp-based filtering with 'since' parameter
- Reduces HTTP overhead from 2N requests to 1 request
- Filters ICE candidates by role (answerer candidates for offerer)
Added GET /offers/answered endpoint that returns all answered offers
for the authenticated peer with optional 'since' timestamp filtering.
This allows offerers to efficiently poll for all incoming connections
in a single request instead of polling each offer individually.
The createOffers function was not inserting the service_id column even
though it was passed in the CreateOfferRequest. This caused all offers
to have NULL service_id, making getOffersForService return empty results.
Fixed:
- Added service_id to INSERT statement in createOffers
- Added serviceId to created offer objects
- Added serviceId to rowToOffer mapping
This resolves the 'No available offers' error when trying to connect
to a published service.
Changed offers table to use service_id (nullable) instead of service_fqn.
This matches the actual D1 storage implementation in d1.ts which expects:
- service_id TEXT (optional link to service)
- NOT service_fqn (that's only in the services table)
Resolves 'NOT NULL constraint failed: offers.service_fqn' error.
- Update wrangler.toml with new D1 database ID
- Add fresh_schema.sql for clean database initialization
- Applied schema to fresh D1 database
- Server redeployed with correct database binding
This resolves the 'table services has no column named service_name' error
by ensuring the database has the correct v0.4.1+ schema.
The validateServicePublish function was incorrectly parsing the signature
message when serviceFqn contained colons (e.g., 'chat:2.0.0@user').
Old logic: Split by ':' and expected exactly 4 parts
Problem: serviceFqn 'chat:2.0.0@user' contains a colon, so we get 5 parts
Fixed:
- Allow parts.length >= 4
- Extract timestamp from the last part
- Reconstruct serviceFqn from all middle parts (parts[2] to parts[length-2])
This fixes the '403 Invalid signature for username' error that was
preventing service publication.
- Add try/catch in claimUsername to handle UNIQUE constraint
- Return meaningful error: 'This public key has already claimed a different username'
- Enable observability logs for better debugging
BREAKING CHANGE: Replace offer-based endpoints with service-based signaling
- Add POST /services/:uuid/answer
- Add GET /services/:uuid/answer
- Add POST /services/:uuid/ice-candidates
- Add GET /services/:uuid/ice-candidates
- Remove all /offers/* endpoints (POST /offers, GET /offers/mine, etc.)
- Server auto-detects peer's offer when offerId is omitted
- Update README with new service-based API documentation
- Bump version to 0.4.0
This change simplifies the API by focusing on services rather than individual offers.
WebRTC signaling (answer/ICE) now operates at the service level, with automatic
offer detection when needed.
## 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
## Breaking Changes
### Server
- Services can now have multiple offers instead of single offer
- POST /users/:username/services accepts `offers` array instead of `sdp`
- GET /users/:username/services/:fqn returns `offers` array in response
- GET /services/:uuid returns `offers` array in response
- Database schema: removed `offer_id` from services table, added `service_id` to offers table
- Added `batchCreateServices()` and `getOffersForService()` methods
### Client
- `PublishServiceOptions` interface: `offers` array instead of `sdp` string
- `Service` interface: `offers` array instead of `offerId` and `sdp`
- `ServiceRequest` interface: `offers` array instead of `sdp`
- RondevuSignaler.setOffer() sends offers array to server
- Updated to extract offerId from first offer in service response
## New Features
- Support for multiple simultaneous offers per service (connection pooling)
- Batch service creation endpoint for reduced server load
- Proper one-to-many relationship between services and offers
## Implementation Details
### Server Changes (src/storage/)
- sqlite.ts: Added service_id column to offers, removed offer_id from services
- d1.ts: Updated to match new interface
- types.ts: Updated interfaces for Service, Offer, CreateServiceRequest
- app.ts: Updated all service endpoints to handle offers array
### Client Changes (src/)
- api.ts: Added OfferRequest and ServiceOffer interfaces
- rondevu-service.ts: Updated PublishServiceOptions to use offers array
- rondevu-signaler.ts: Updated to send/receive offers array
## Migration Notes
- No backwards compatibility - this is a breaking change
- Services published with old API will not work with new server
- Clients must update to new API to work with updated server
🤖 Generated with Claude Code
Modified /services/:uuid endpoint to return an available (unanswered)
offer from the service's offer pool instead of always returning the
initial offer. This fixes reconnection failures where clients would
try to answer already-consumed offers.
Changes:
- Query all offers from the service's peer ID
- Return first unanswered offer
- Return 503 if no offers available
Fixes: "Offer already answered" errors on reconnection attempts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When a service is republished (e.g., for TTL refresh), the old service
is now deleted before creating a new one, preventing UNIQUE constraint
errors on (username, service_fqn).
Changes:
- Query for existing service before creation
- Delete existing service if found
- Create new service with same username/serviceFqn
This enables the client's TTL auto-refresh feature to work correctly.
- Delete unused bloom.ts module (leftover from topic-based discovery)
- Remove maxTopicsPerOffer configuration (no longer used)
- Remove unused info field from Offer types
- Simplify generateOfferHash() to only hash SDP (remove topics param)
- Update outdated comments referencing deprecated features
- Remove backward compatibility topics field from answer responses
This completes the migration to V2 service-based architecture by
removing all remnants of the V1 topic-based system.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Delete existing service before creating new one
- Prevents UNIQUE constraint error on (username, service_fqn)
- Enables seamless service republishing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Change 'answererPeerId' to 'answererId'
- Change 'answerSdp' to 'sdp'
- Add 'topics' field (empty array) for client compatibility
This ensures the server response matches the expected format
in the client's AnsweredOffer interface.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The error handler was referencing variables (username, serviceFqn, offers)
that were declared inside the try block. If an error occurred before these
were defined, the error handler itself would fail, resulting in non-JSON
responses that caused "JSON.parse: unexpected character" errors on the client.
Fixed by:
- Declaring variables at function scope
- Initializing offers as empty array
- Using destructuring assignment for username/serviceFqn
This ensures the error handler can always access these variables safely,
even if an early error occurs, and will always return proper JSON responses.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The service publishing endpoint was using validateUsernameClaim which
expects the message format "claim:{username}:{timestamp}", but clients
send "publish:{username}:{serviceFqn}:{timestamp}".
Added validateServicePublish function to properly validate service
publishing signatures with the correct message format.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Switch from sync verify() to async verifyAsync() to work with
hashes.sha512Async which uses WebCrypto API.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- d1.ts: Use global crypto.randomUUID() instead of importing from 'crypto'
- sqlite.ts: Use 'node:crypto' import for Node.js compatibility
This fixes the Cloudflare Workers deployment error:
"The package 'crypto' wasn't found on the file system but is built into node"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
@noble/ed25519 v3.0.0 requires explicit SHA-512 hash function setup
before using any cryptographic operations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add POST /services endpoint for publishing services with username verification
- Add DELETE /services/:serviceId endpoint for unpublishing services
- Add GET /services/:serviceFqn endpoint for service discovery
- Add POST /usernames/claim endpoint with Ed25519 signature verification
- Add POST /usernames/renew endpoint for extending username TTL
- Add GET /usernames/:username endpoint for checking username availability
- Add username expiry tracking and cleanup (365-day default TTL)
- Add service-to-offer relationship tracking
- Add signature verification for username operations
- Update storage schema for usernames and services tables
- Add comprehensive README documentation for V2 APIs
- Update version to 0.8.0
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>