diff options
author | Calvin Morrison <calvin@pobox.com> | 2025-09-03 21:15:36 -0400 |
---|---|---|
committer | Calvin Morrison <calvin@pobox.com> | 2025-09-03 21:15:36 -0400 |
commit | 49fa5aa2a127bdf8924d02bf77e5086b39c7a447 (patch) | |
tree | 61d86a7705dacc9fddccc29fa79d075d83ab8059 /JMAP_IMPROVEMENTS.md |
Diffstat (limited to 'JMAP_IMPROVEMENTS.md')
-rw-r--r-- | JMAP_IMPROVEMENTS.md | 313 |
1 files changed, 313 insertions, 0 deletions
diff --git a/JMAP_IMPROVEMENTS.md b/JMAP_IMPROVEMENTS.md new file mode 100644 index 0000000..7360ea3 --- /dev/null +++ b/JMAP_IMPROVEMENTS.md @@ -0,0 +1,313 @@ +# JMAP Standards Compliance Improvements + +**Date:** August 15, 2025 +**Based on:** JMAP Crash Course and RFC 8620 + +## Summary of Improvements Made + +After reviewing the JMAP crash course, I've implemented several key improvements to make our JCHAT server more standards-compliant with RFC 8620. + +## Core Improvements + +### 1. Enhanced Request/Response Structure Validation + +**Previous:** +- Basic validation of `using` and `methodCalls` +- Limited error handling + +**Improved:** +```erlang +% Now validates complete JMAP request structure +validate_jmap_request(#{<<"using">> := Using, <<"methodCalls">> := MethodCalls}) -> + case validate_using_array(Using) of + ok -> validate_method_calls_array(MethodCalls); + Error -> Error + end. + +% Ensures method call IDs are unique within request +validate_unique_call_ids(MethodCalls) -> + CallIds = [CallId || [_, _, CallId] <- MethodCalls], + case length(CallIds) =:= length(lists:usort(CallIds)) of + true -> ok; + false -> {error, "Method call IDs must be unique"} + end. +``` + +### 2. Proper Content-Type Handling + +**Previous:** +- Loose content type checking + +**Improved:** +```erlang +validate_content_type(Req) -> + case cowboy_req:header(<<"content-type">>, Req) of + <<"application/json", _/binary>> -> ok; + _ -> {error, invalid_content_type} + end. +``` + +**Now returns proper content-type:** +``` +Content-Type: application/json; charset=utf-8 +``` + +### 3. Comprehensive Error Response Format + +**Previous:** +- Basic error types + +**Improved:** +- All standard JMAP error types from RFC 8620: +```erlang +format_error(invalid_arguments) -> #{type => <<"invalidArguments">>}; +format_error(account_not_found) -> #{type => <<"accountNotFound">>}; +format_error(forbidden) -> #{type => <<"forbidden">>}; +format_error(invalid_result_reference) -> #{type => <<"invalidResultReference">>}; +format_error(anchor_not_found) -> #{type => <<"anchorNotFound">>}; +format_error(unsupported_sort) -> #{type => <<"unsupportedSort">>}; +format_error(unsupported_filter) -> #{type => <<"unsupportedFilter">>}; +format_error(cannot_calculate_changes) -> #{type => <<"cannotCalculateChanges">>}; +format_error(too_large) -> #{type => <<"tooLarge">>}; +format_error(rate_limited) -> #{type => <<"rateLimited">>}; +format_error(state_mismatch) -> #{type => <<"stateMismatch">>}; +% ... and more +``` + +### 4. Authentication Framework + +**Previous:** +- Hardcoded account ID + +**Improved:** +```erlang +% Extract Bearer token from Authorization header +extract_auth_token(<<"Bearer ", Token/binary>>) -> {ok, Token}; + +% Session endpoint now handles authentication +authenticate_session_request(<<"Bearer ", _Token/binary>>) -> + {ok, <<"demo@example.com">>}; % In production, validate token + +% Account ID determination from auth context +determine_account_id(_AuthHeader) -> + <<"default">>. % In production, extract from validated JWT token +``` + +### 5. Method Call Validation + +**Added comprehensive method call structure validation:** +```erlang +validate_method_call([Method, Args, CallId]) + when is_binary(Method), is_map(Args), is_binary(CallId) -> + case validate_method_name(Method) of + true -> ok; + false -> {error, invalid_method_name} + end. + +validate_method_name(Method) -> + case binary:split(Method, <<"/">>) of + [Type, Operation] when byte_size(Type) > 0, byte_size(Operation) > 0 -> + validate_method_chars(Method); + _ -> false + end. +``` + +### 6. Session Object Improvements + +**Previous:** +- Static session state +- Fixed username + +**Improved:** +```erlang +build_session_object(Username) -> + #{ + <<"capabilities">> => #{...}, + <<"accounts">> => #{ + <<"default">> => #{ + <<"name">> => Username, % Dynamic username + <<"isPersonal">> => true, + <<"isReadOnly">> => false, + <<"accountCapabilities">> => #{...} + } + }, + <<"username">> => Username, + <<"state">> => jchat_utils:generate_id() % Unique session state + }. +``` + +## Standards Compliance Checklist + +### ✅ Now Compliant + +1. **Request Structure Validation** + - ✅ Validates `using` array is non-empty and contains only strings + - ✅ Validates `methodCalls` array structure + - ✅ Ensures method call IDs are unique within request + - ✅ Proper method name format validation (`Type/operation`) + +2. **Content-Type Handling** + - ✅ Validates `application/json` content type + - ✅ Returns `application/json; charset=utf-8` in responses + +3. **Error Handling** + - ✅ All standard JMAP error types implemented + - ✅ Proper error response structure + - ✅ URN-based error type identifiers + +4. **Session Endpoint** + - ✅ Authentication framework in place + - ✅ Dynamic session state generation + - ✅ Proper CORS handling + - ✅ Method validation (GET/OPTIONS only) + +5. **HTTP Method Handling** + - ✅ Proper OPTIONS preflight support + - ✅ Method not allowed responses + - ✅ Authorization header extraction + +### ⚠️ Still Needs Work + +1. **Real Authentication** + - Currently returns demo session regardless of auth + - Need JWT token validation implementation + - Need user lookup from token + +2. **Account Management** + - Still using hardcoded "default" account + - Need proper account ID extraction from auth context + - Need multi-account support + +3. **State Management** + - Session state is generated but not tracked + - Need persistent state tracking per account/object type + - Need state validation in method calls + +4. **Binary Data** + - Upload/download endpoints defined but not implemented + - Need blob storage system + - Need proper multipart handling + +## Example: Standards-Compliant Request Flow + +### 1. Client Session Request +```http +GET /jmap/session HTTP/1.1 +Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... +``` + +### 2. Server Session Response +```json +{ + "capabilities": { + "urn:ietf:params:jmap:core": { + "maxSizeUpload": 50000000, + "maxConcurrentRequests": 4, + "maxCallsInRequest": 16 + }, + "urn:ietf:params:jmap:chat": { + "maxMessageLength": 10000, + "maxParticipantsPerConversation": 100 + } + }, + "accounts": { + "default": { + "name": "user@example.com", + "isPersonal": true, + "isReadOnly": false, + "accountCapabilities": { + "urn:ietf:params:jmap:core": {}, + "urn:ietf:params:jmap:chat": {} + } + } + }, + "primaryAccounts": { + "urn:ietf:params:jmap:chat": "default" + }, + "username": "user@example.com", + "apiUrl": "http://localhost:8080/jmap/api", + "state": "b3e91c7a-8f2d-4c1e-9a5b-7d3e2f1c8b9a" +} +``` + +### 3. Client API Request +```http +POST /jmap/api HTTP/1.1 +Content-Type: application/json; charset=utf-8 +Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... + +{ + "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:chat"], + "methodCalls": [ + ["Conversation/query", {"accountId": "default"}, "c1"], + ["Message/query", {"accountId": "default", "filter": {"conversationId": "#c1"}}, "c2"] + ] +} +``` + +### 4. Server API Response +```json +{ + "methodResponses": [ + ["Conversation/query", {"accountId": "default", "ids": ["conv1", "conv2"]}, "c1"], + ["Message/query", {"accountId": "default", "ids": ["msg1", "msg2", "msg3"]}, "c2"] + ], + "sessionState": "b3e91c7a-8f2d-4c1e-9a5b-7d3e2f1c8b9a" +} +``` + +## Testing the Improvements + +### Manual Testing +```bash +# Test session endpoint with proper headers +curl -X GET http://localhost:8080/jmap/session \ + -H "Authorization: Bearer test-token" + +# Test API endpoint with proper content type +curl -X POST http://localhost:8080/jmap/api \ + -H "Content-Type: application/json; charset=utf-8" \ + -H "Authorization: Bearer test-token" \ + -d '{ + "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:chat"], + "methodCalls": [["Core/echo", {"test": "data"}, "c1"]] + }' + +# Test error handling with invalid content type +curl -X POST http://localhost:8080/jmap/api \ + -H "Content-Type: text/plain" \ + -d '{"invalid": "request"}' +``` + +### Expected Error Response +```json +{ + "type": "urn:ietf:params:jmap:error:notJSON", + "status": 400, + "detail": "Content-Type must be application/json" +} +``` + +## Next Steps for Full Compliance + +1. **Implement Real Authentication** + - JWT token validation + - User database lookup + - Session management + +2. **Add Binary Data Support** + - Upload endpoint implementation + - Download endpoint implementation + - Blob storage in Mnesia + +3. **Enhanced State Management** + - Per-account state tracking + - State validation in method calls + - Proper change detection + +4. **Method Enhancements** + - Better property filtering + - Pagination support + - Advanced query features + +These improvements move us significantly closer to full JMAP RFC 8620 compliance while maintaining our clean architecture and extensibility. |