Bug: getService() was accessing parsed.service (undefined) instead of
parsed.serviceName, causing D1_TYPE_ERROR in service discovery.
This affected both random discovery (line 263) and paginated discovery
(line 218) when clients requested services without specifying username.
The parseServiceFqn() function returns { serviceName, version, username },
not { service, version, username }.
Fixes: D1_TYPE_ERROR: Type 'undefined' not supported for value 'undefined'
when discovering services like 'test-chat:1.0.0' (without @username)
- Document fix for storage method calls
- Explain replacement of non-existent storage.getServicesByName()
- Note compatibility with Storage interface specification
Fixes customer-reported error: "storage.getServicesByName is not a function"
Replace non-existent getServicesByName() with:
- discoverServices() for paginated mode
- getRandomService() for random mode
Fixes "storage.getServicesByName is not a function" error.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update username validation regex to allow periods (.) in addition to alphanumeric characters and dashes. Usernames must still start and end with alphanumeric characters.
Use Buffer for base64 encoding/decoding to ensure compatibility with Node.js clients using NodeCryptoAdapter. The previous btoa/atob implementation caused signature verification failures when clients used Buffer-based encoding.
Fixes: Invalid signature errors when using NodeCryptoAdapter
Break down 138-line method with three helper functions:
1. filterCompatibleServices(): Eliminates duplicate filtering logic
- Used in both paginated and random discovery modes
- Centralizes version compatibility checking
2. findAvailableOffer(): Encapsulates offer lookup logic
- Used across all three modes
- Ensures consistent offer selection
3. buildServiceResponse(): Standardizes response formatting
- Single source of truth for response structure
- Used in all return paths
Benefits:
- Eliminates 30+ lines of duplicate code
- Three modes now clearly separated and documented
- Easier to maintain and test each mode independently
- Consistent response formatting across all modes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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.