# Micropub Endpoint Design for StarPunk V1 ## Executive Summary This document defines the architecture and implementation plan for adding Micropub support to StarPunk, the **critical blocker for V1 release**. The Micropub endpoint will enable external clients (like Indigenous, Quill, or Micropublish.net) to create posts on StarPunk installations. **Current State:** - ✅ IndieAuth authentication working (authorization endpoint with PKCE) - ✅ Note CRUD operations via `notes.py` - ✅ Markdown file storage with YAML frontmatter - ✅ SQLite metadata database - ❌ **No Micropub endpoint** (V1 blocker) - ❌ **No token endpoint** (required for Micropub auth) **Target State for V1:** - Micropub server implementation (create-only for V1) - IndieAuth token endpoint for issuing access tokens - Support for creating posts via form-encoded and JSON - Minimal V1 implementation (no media endpoint, no update/delete) ## Architecture Overview ### System Components ``` ┌─────────────────────────────────────────────────────────────┐ │ Micropub Client │ │ (Indigenous, Quill, etc.) │ └──────────────────────┬──────────────────────────────────────┘ │ │ HTTP Requests with Bearer Token ▼ ┌─────────────────────────────────────────────────────────────┐ │ StarPunk Server │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ Micropub Endpoint │ │ │ │ /micropub (Blueprint) │ │ │ ├────────────────────────────────────────────────────┤ │ │ │ • Token Validation (Bearer auth) │ │ │ │ • Request Parsing (form/JSON) │ │ │ │ • Action Routing (create/update/delete/query) │ │ │ │ • Response Formatting │ │ │ └──────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ Token Management │ │ │ │ /auth/token (endpoint) │ │ │ ├────────────────────────────────────────────────────┤ │ │ │ • Authorization code exchange │ │ │ │ • Access token generation │ │ │ │ • Scope validation │ │ │ │ • Token storage in database │ │ │ └──────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ Existing Notes CRUD │ │ │ │ starpunk/notes.py │ │ │ ├────────────────────────────────────────────────────┤ │ │ │ • create_note() │ │ │ │ • update_note() │ │ │ │ • delete_note() │ │ │ │ • get_note() │ │ │ └──────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ Storage Layer │ │ │ ├──────────────────────┬──────────────────────────────┤ │ │ │ SQLite Database │ Markdown Files │ │ │ │ • notes metadata │ /data/notes/YYYY/MM/ │ │ │ │ • tokens │ • YAML frontmatter │ │ │ │ • sessions │ • Markdown content │ │ │ └──────────────────────┴──────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ## Detailed Component Design ### 1. Token Endpoint (`/auth/token`) The token endpoint is required for IndieAuth authorization code flow, converting authorization codes into access tokens for API access. #### Endpoint Specification ``` POST /auth/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code& code={authorization_code}& client_id={client_url}& redirect_uri={redirect_url}& me={user_profile_url}& code_verifier={pkce_verifier} # Optional (if PKCE was used) ``` #### Response Format Success (200 OK): ```json { "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...", "token_type": "Bearer", "scope": "create update delete", "me": "https://example.com/" } ``` Error (400 Bad Request): ```json { "error": "invalid_grant", "error_description": "The authorization code is invalid or expired" } ``` #### Implementation Details ```python # New file: starpunk/tokens.py def generate_access_token() -> str: """Generate cryptographically secure access token""" return secrets.token_urlsafe(32) def create_token(me: str, client_id: str, scope: str) -> str: """ Create and store access token in database Args: me: User's identity URL client_id: Client application URL scope: Space-separated list of scopes Returns: Access token string """ token = generate_access_token() token_hash = hashlib.sha256(token.encode()).hexdigest() db = get_db() db.execute(""" INSERT INTO tokens (token_hash, me, client_id, scope, created_at, expires_at) VALUES (?, ?, ?, ?, datetime('now'), datetime('now', '+90 days')) """, (token_hash, me, client_id, scope)) db.commit() return token def verify_token(token: str) -> Optional[dict]: """ Verify access token and return token info Returns dict with: me, client_id, scope, or None if invalid """ token_hash = hashlib.sha256(token.encode()).hexdigest() db = get_db() row = db.execute(""" SELECT me, client_id, scope FROM tokens WHERE token_hash = ? AND expires_at > datetime('now') """, (token_hash,)).fetchone() if row: return dict(row) return None ``` #### Database Schema Update ```sql -- SECURITY FIX: Migrate to hashed token storage -- Note: This will invalidate all existing tokens -- Step 1: Create new secure tokens table CREATE TABLE tokens_secure ( id INTEGER PRIMARY KEY AUTOINCREMENT, token_hash TEXT UNIQUE NOT NULL, -- SHA256 hash of token me TEXT NOT NULL, client_id TEXT, scope TEXT DEFAULT 'create', -- Default scope for V1 created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL, last_used_at TIMESTAMP, revoked_at TIMESTAMP ); -- Step 2: Drop old insecure table (invalidates existing tokens) DROP TABLE IF EXISTS tokens; -- Step 3: Rename to final name ALTER TABLE tokens_secure RENAME TO tokens; -- Step 4: Create indexes CREATE INDEX idx_tokens_hash ON tokens(token_hash); CREATE INDEX idx_tokens_me ON tokens(me); CREATE INDEX idx_tokens_expires ON tokens(expires_at); -- Step 5: Create authorization_codes table CREATE TABLE authorization_codes ( id INTEGER PRIMARY KEY AUTOINCREMENT, code_hash TEXT UNIQUE NOT NULL, -- SHA256 hash for security me TEXT NOT NULL, client_id TEXT NOT NULL, redirect_uri TEXT NOT NULL, scope TEXT, state TEXT, code_challenge TEXT, -- Optional PKCE code_challenge_method TEXT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL, used_at TIMESTAMP -- Prevent replay attacks ); CREATE INDEX idx_auth_codes_hash ON authorization_codes(code_hash); CREATE INDEX idx_auth_codes_expires ON authorization_codes(expires_at); ``` ### 2. Micropub Endpoint (`/micropub`) The main Micropub endpoint handles all post operations. #### Route Structure ```python # New file: starpunk/routes/micropub.py from flask import Blueprint, request, jsonify, current_app from starpunk.tokens import verify_token from starpunk.micropub import ( handle_create, handle_update, handle_delete, handle_query, MicropubError ) bp = Blueprint("micropub", __name__) @bp.route("/micropub", methods=["GET", "POST"]) def micropub_endpoint(): """Main Micropub endpoint""" # Extract token from Authorization header or form token = extract_bearer_token(request) if not token: return error_response("unauthorized", "No access token provided"), 401 # Verify token token_info = verify_token(token) if not token_info: return error_response("unauthorized", "Invalid access token"), 401 # Handle query endpoints (GET requests) if request.method == "GET": return handle_query(request.args, token_info) # Handle action endpoints (POST requests) content_type = request.headers.get("Content-Type", "") if "application/json" in content_type: data = request.get_json() action = data.get("action", "create") else: # Form-encoded or multipart data = request.form.to_dict(flat=False) action = data.get("action", ["create"])[0] try: if action == "create": return handle_create(data, token_info) elif action == "update": # V1: Update not supported return error_response("invalid_request", "Update action not supported in V1"), 400 elif action == "delete": # V1: Delete not supported return error_response("invalid_request", "Delete action not supported in V1"), 400 else: return error_response("invalid_request", f"Unknown action: {action}"), 400 except MicropubError as e: return error_response("invalid_request", str(e)), 400 ``` #### Content Creation Handler ```python # New file: starpunk/micropub.py def handle_create(data: dict, token_info: dict) -> tuple: """ Handle post creation Args: data: Micropub request data (form or JSON) token_info: Authenticated user info Returns: Response tuple (body, status, headers) """ # Check scope if "create" not in token_info.get("scope", ""): return error_response("insufficient_scope", "Token lacks create scope"), 403 # Extract properties properties = normalize_properties(data) # Map Micropub properties to StarPunk note format title = extract_title(properties) content = extract_content(properties) tags = properties.get("category", []) # Create note using existing CRUD from starpunk.notes import create_note try: note = create_note( title=title, content=content, tags=tags, author=token_info["me"], published=True # Micropub posts are published by default ) # Build permalink URL permalink = f"{current_app.config['SITE_URL']}/notes/{note.slug}" # Return 201 Created with Location header return "", 201, {"Location": permalink} except Exception as e: current_app.logger.error(f"Failed to create note via Micropub: {e}") return error_response("server_error", "Failed to create post"), 500 def normalize_properties(data: dict) -> dict: """ Normalize Micropub properties from both form and JSON formats Handles: - Form: property[]=value arrays - JSON: {"properties": {"property": ["value"]}} """ if "properties" in data: # JSON format return data["properties"] # Form format - convert to properties dict properties = {} for key, value in data.items(): if key.startswith("mp-") or key in ["action", "url", "access_token"]: continue # Skip reserved properties # Handle array notation: property[] -> property clean_key = key.rstrip("[]") # Ensure value is always a list if not isinstance(value, list): value = [value] properties[clean_key] = value return properties def extract_content(properties: dict) -> str: """Extract content from Micropub properties""" content = properties.get('content', [''])[0] if not content: raise MicropubError("Content is required") return content def extract_title(properties: dict) -> str: """Extract title from Micropub properties""" # Use 'name' property if provided title = properties.get('name', [''])[0] # If no name, extract from content (first line, max 50 chars) if not title: content = properties.get('content', [''])[0] if content: first_line = content.split('\n')[0] title = first_line[:50] + ('...' if len(first_line) > 50 else '') return title ``` #### V1 Limitations In V1, only the `create` action is supported. Update and delete operations will return an error response and are planned for a post-V1 release. ### 3. Query Endpoints Micropub defines several query endpoints for client discovery: ```python def handle_query(args: dict, token_info: dict) -> tuple: """Handle Micropub query endpoints""" q = args.get("q") if q == "config": # Return server configuration config = { "media-endpoint": None, # No media endpoint in V1 "syndicate-to": [], # No syndication targets in V1 "post-types": [ { "type": "note", "name": "Note", "properties": ["content"] } ] } return jsonify(config), 200 elif q == "source": # Return source of a specific post url = args.get("url") if not url: return error_response("invalid_request", "No URL provided"), 400 slug = extract_slug_from_url(url) from starpunk.notes import get_note note = get_note(slug) if not note: return error_response("invalid_request", "Post not found"), 400 # Convert note to Micropub format mf2 = { "type": ["h-entry"], "properties": { "content": [note.content], "name": [note.title] if note.title else [], "category": note.tags if note.tags else [], "published": [note.created_at.isoformat()], "url": [f"{current_app.config['SITE_URL']}/notes/{note.slug}"] } } return jsonify(mf2), 200 elif q == "syndicate-to": # Return syndication targets (none for V1) return jsonify({"syndicate-to": []}), 200 else: return error_response("invalid_request", f"Unknown query: {q}"), 400 ``` ### 4. Authorization Flow Integration The complete authorization flow for Micropub: ``` ┌──────────┐ │ Client │ └─────┬────┘ │ │ 1. GET /auth/authorization? │ response_type=code& │ client_id=https://client.example& │ redirect_uri=https://client.example/callback& │ state=1234567890& │ scope=create& # V1: create only │ me=https://user.example& │ code_challenge=XXXXX& # Optional PKCE │ code_challenge_method=S256 ▼ ┌──────────────┐ │ StarPunk │ │ (Authorize) │ └─────┬────────┘ │ │ 2. Check admin session │ If not logged in: redirect to /auth/login │ If logged in: show authorization form │ │ 3. User approves, generate authorization code │ ▼ ┌──────────┐ │ Client │ └─────┬────┘ │ │ 4. POST /auth/token │ grant_type=authorization_code& │ code=XXXXX& │ client_id=https://client.example& │ redirect_uri=https://client.example/callback& │ me=https://user.example& │ code_verifier=XXXXX # If PKCE was used ▼ ┌──────────────┐ │ StarPunk │ │ (Token) │ └─────┬────────┘ │ │ 5. Return access token │ { │ "access_token": "XXXXX", │ "token_type": "Bearer", │ "scope": "create", # V1: create only │ "me": "https://user.example" │ } ▼ ┌──────────┐ │ Client │ └─────┬────┘ │ │ 6. POST /micropub │ Authorization: Bearer XXXXX │ Content-Type: application/x-www-form-urlencoded │ │ h=entry&content=Hello+world ▼ ┌──────────────┐ │ StarPunk │ │ (Micropub) │ └──────────────┘ ``` ## Implementation Plan ### Phase 1: Token Management (2-3 days) 1. **Database Migration** - Update tokens table schema - Add indexes for performance - Create migration script 2. **Token Module** (`starpunk/tokens.py`) - Token generation functions - Token storage with hashing - Token verification - Scope validation helpers 3. **Token Endpoint** (`/auth/token`) - Authorization code validation - PKCE verification - Token generation - Response formatting 4. **Testing** - Unit tests for token functions - Integration tests for token endpoint - Security tests (token expiry, revocation) ### Phase 2: Micropub Core (3-4 days) 1. **Micropub Module** (`starpunk/micropub.py`) - Property normalization - Content extraction - Note format conversion - Error handling 2. **Micropub Routes** (`starpunk/routes/micropub.py`) - Main endpoint handler - Bearer token extraction - Content-type handling - Action routing 3. **Create Handler** - Form-encoded parsing - JSON parsing - Note creation via CRUD - Location header generation 4. **Testing** - Create post via form-encoded - Create post via JSON - Invalid token handling - Scope validation ### Phase 3: Query Endpoints & Validation (1-2 days) 1. **Query Endpoints** - Config endpoint - Source endpoint - Syndicate-to endpoint (empty for V1) 2. **Property Validation** - Content extraction and validation - Title generation from content - Tag/category mapping 3. **Testing** - Query endpoint responses - Property mapping edge cases - Error response formatting ### Phase 4: Integration & Polish (1-2 days) 1. **Authorization Endpoint Updates** - Add scope parameter handling - Store requested scopes with auth state - Pass scopes to token creation 2. **Discovery Headers** - Add `Link: ; rel="micropub"` header - Add `Link: ; rel="authorization_endpoint"` header - Add `Link: ; rel="token_endpoint"` header 3. **Error Handling** - Consistent error responses - Proper HTTP status codes - Detailed error descriptions 4. **Documentation** - API documentation - Client configuration guide - Testing with real clients ## V1 Minimal Feature Set ### In Scope for V1 ✅ **Required for Micropub Compliance:** - Bearer token authentication - Create posts (form-encoded) - Create posts (JSON) - Return 201 Created with Location header - Configuration query endpoint - Source query endpoint - Authorization endpoint (`/auth/authorization`) - Token endpoint (`/auth/token`) ✅ **Required for Functionality:** - Authorization code generation and storage - Token generation with SHA256 hashing - Scope enforcement (create only for V1) - Integration with existing notes CRUD - Property mapping (content, title, tags) - Optional PKCE support ### Out of Scope for V1 (Post-V1 Roadmap) ❌ **Deferred to Post-V1:** - Update operations (action=update) - Delete operations (action=delete) - Media endpoint (file uploads) - Photo/video properties - Syndication targets - Complex post types (articles, replies, etc.) - Batch operations - Websub notifications - Token introspection endpoint - Token revocation endpoint - Multiple scopes beyond "create" ## Security Considerations ### Token Security 1. **Storage** - Tokens stored as SHA256 hashes - Original token never logged - Secure random generation (32 bytes) 2. **Validation** - Check token exists - Check not expired (90 days) - Check not revoked - Verify scope for operation 3. **Transport** - Require HTTPS in production - Support both header and form parameter - Never include in URLs ### Scope Enforcement ```python # V1 only supports "create" scope SUPPORTED_SCOPES = ["create"] DEFAULT_SCOPE = "create" def check_scope(required: str, granted: str) -> bool: """Check if granted scopes include required scope""" if not granted: # IndieAuth spec: no scope means no access return False granted_scopes = set(granted.split()) return required in granted_scopes def validate_requested_scope(scope: str) -> str: """Validate and filter requested scopes to supported ones""" if not scope: return "" # Empty scope allowed during authorization requested = set(scope.split()) supported = set(SUPPORTED_SCOPES) valid_scopes = requested & supported return " ".join(valid_scopes) if valid_scopes else "" ``` ### Input Validation 1. **Content Validation** - Sanitize HTML content - Validate URLs - Limit content size - Check required properties 2. **URL Validation** - Verify URL belongs to this site - Extract valid slugs - Prevent directory traversal 3. **Rate Limiting** - Limit requests per token - Limit failed authentication attempts - Implement exponential backoff ## Testing Strategy ### Unit Tests ```python # tests/test_micropub.py def test_normalize_properties_form(): """Test form-encoded property normalization""" data = { "content": ["Hello world"], "category[]": ["tag1", "tag2"], "mp-slug": ["my-post"] } properties = normalize_properties(data) assert properties["content"] == ["Hello world"] assert properties["category"] == ["tag1", "tag2"] assert "mp-slug" not in properties def test_normalize_properties_json(): """Test JSON property normalization""" data = { "type": ["h-entry"], "properties": { "content": ["Hello world"], "category": ["tag1", "tag2"] } } properties = normalize_properties(data) assert properties["content"] == ["Hello world"] assert properties["category"] == ["tag1", "tag2"] def test_bearer_token_extraction(): """Test token extraction from header and form""" # Test header request = Mock(headers={"Authorization": "Bearer abc123"}) token = extract_bearer_token(request) assert token == "abc123" # Test form parameter request = Mock(headers={}, form={"access_token": "xyz789"}) token = extract_bearer_token(request) assert token == "xyz789" ``` ### Integration Tests ```python # tests/test_micropub_integration.py def test_create_post_form_encoded(client, auth_token): """Test creating post with form-encoded request""" response = client.post("/micropub", headers={"Authorization": f"Bearer {auth_token}"}, data={ "h": "entry", "content": "Test post", "category[]": ["test", "micropub"] } ) assert response.status_code == 201 assert "Location" in response.headers assert "/notes/" in response.headers["Location"] def test_create_post_json(client, auth_token): """Test creating post with JSON request""" response = client.post("/micropub", headers={ "Authorization": f"Bearer {auth_token}", "Content-Type": "application/json" }, json={ "type": ["h-entry"], "properties": { "content": ["Test post"], "category": ["test", "micropub"] } } ) assert response.status_code == 201 assert "Location" in response.headers def test_unauthorized_request(client): """Test request without token""" response = client.post("/micropub", data={"h": "entry", "content": "Test"} ) assert response.status_code == 401 assert response.json["error"] == "unauthorized" def test_insufficient_scope(client, read_only_token): """Test request with insufficient scope""" response = client.post("/micropub", headers={"Authorization": f"Bearer {read_only_token}"}, data={"h": "entry", "content": "Test"} ) assert response.status_code == 403 assert response.json["error"] == "insufficient_scope" ``` ### Client Testing Test with real Micropub clients: 1. **Indigenous** (iOS/Android) - Configure with StarPunk URLs - Test post creation - Test photo uploads (when media endpoint added) 2. **Quill** (Web) - Test at https://quill.p3k.io - Enter StarPunk domain - Complete authentication - Create test posts 3. **Micropublish.net** (Web) - Test at https://micropublish.net - Full-featured client - Test all operations ## Migration Path ### Database Migrations ```sql -- Migration: Add Micropub support -- Version: 0.10.0 -- 1. Update tokens table for better security ALTER TABLE tokens RENAME TO tokens_old; CREATE TABLE tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, token_hash TEXT UNIQUE NOT NULL, me TEXT NOT NULL, client_id TEXT, scope TEXT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP, last_used_at TIMESTAMP, revoked_at TIMESTAMP ); -- Migrate existing tokens (if any) -- Note: Existing tokens will be invalid as we don't have original values DROP TABLE tokens_old; -- 2. Add authorization codes table for token exchange CREATE TABLE IF NOT EXISTS authorization_codes ( code TEXT PRIMARY KEY, me TEXT NOT NULL, client_id TEXT NOT NULL, redirect_uri TEXT NOT NULL, scope TEXT, code_challenge TEXT, code_challenge_method TEXT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL, used_at TIMESTAMP ); CREATE INDEX idx_auth_codes_expires ON authorization_codes(expires_at); ``` ### Configuration Updates ```python # config.py additions # Micropub settings MICROPUB_ENDPOINT = "/micropub" TOKEN_ENDPOINT = "/auth/token" TOKEN_EXPIRY_DAYS = 90 AUTHORIZATION_CODE_EXPIRY_MINUTES = 10 # Supported scopes (V1 only supports create) SUPPORTED_SCOPES = ["create"] DEFAULT_SCOPE = "create" ``` ## Effort Estimates ### Total Effort: 6-8 days (Reduced for V1 scope) | Phase | Task | Effort | Priority | |-------|------|--------|----------| | 1 | Token Management | 2-3 days | Critical | | 2 | Micropub Core (Create Only) | 2-3 days | Critical | | 3 | Query Endpoints & Validation | 1-2 days | High | | 4 | Integration & Polish | 1-2 days | High | ### Resource Requirements - **Developer**: 1 person full-time for 2 weeks - **Testing**: Access to Micropub clients - **Documentation**: 1 day included in estimates ### Risk Factors 1. **Token Security** (High Impact, Low Probability) - Mitigation: Follow security best practices, hash tokens 2. **Client Compatibility** (Medium Impact, Medium Probability) - Mitigation: Test with multiple clients early 3. **Scope Creep** (Medium Impact, High Probability) - Mitigation: Strict V1 feature set, defer nice-to-haves ## Success Criteria ### Functional Requirements ✅ Micropub endpoint responds to POST requests ✅ Bearer token authentication works ✅ Posts can be created via form-encoded requests ✅ Posts can be created via JSON requests ✅ Location header returned on creation ✅ Query endpoints return valid responses ✅ Token endpoint exchanges codes for tokens ✅ Scopes are enforced correctly ### Performance Requirements ✅ Post creation < 500ms ✅ Token validation < 50ms ✅ Query responses < 200ms ### Security Requirements ✅ Tokens stored as hashes ✅ Expired tokens rejected ✅ Invalid tokens return 401 ✅ Insufficient scope returns 403 ✅ HTTPS required in production ### Compatibility Requirements ✅ Works with Indigenous app ✅ Works with Quill web client ✅ Works with Micropublish.net ✅ Passes Micropub Rocks validator ## Conclusion The Micropub implementation is straightforward given StarPunk's existing architecture: 1. **Existing CRUD operations** in `notes.py` handle the storage 2. **Existing authentication** provides the foundation for tokens 3. **Clear specification** from W3C defines exact requirements 4. **Minimal V1 scope** focuses on core functionality The main work is: - Building the token endpoint for IndieAuth - Creating the Micropub request handler - Mapping between Micropub and StarPunk formats - Proper error handling and responses With 8-10 days of focused development, StarPunk will have a fully functional Micropub endpoint, unblocking the V1 release. ## Appendix: Example Requests ### Creating a Note (Form-Encoded) ```http POST /micropub HTTP/1.1 Host: starpunk.example.com Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc... Content-Type: application/x-www-form-urlencoded h=entry& content=Just+had+coffee+at+the+new+place+downtown.+Really+good!& category[]=coffee& category[]=portland ``` ### Creating a Note (JSON) ```http POST /micropub HTTP/1.1 Host: starpunk.example.com Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc... Content-Type: application/json { "type": ["h-entry"], "properties": { "content": ["Just had coffee at the new place downtown. Really good!"], "category": ["coffee", "portland"] } } ``` ### V1 Limitation Examples #### Attempting Update (Returns Error) ```http POST /micropub HTTP/1.1 Host: starpunk.example.com Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc... Content-Type: application/json { "action": "update", "url": "https://starpunk.example.com/notes/2024/11/coffee-downtown", "replace": { "content": ["Updated content"] } } ``` Response (400 Bad Request): ```json { "error": "invalid_request", "error_description": "Update action not supported in V1" } ``` #### Attempting Delete (Returns Error) ```http POST /micropub HTTP/1.1 Host: starpunk.example.com Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc... Content-Type: application/x-www-form-urlencoded action=delete& url=https://starpunk.example.com/notes/2024/11/test-post ``` Response (400 Bad Request): ```json { "error": "invalid_request", "error_description": "Delete action not supported in V1" } ``` ### Querying Configuration ```http GET /micropub?q=config HTTP/1.1 Host: starpunk.example.com Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc... ``` Response: ```json { "media-endpoint": null, "syndicate-to": [], "post-types": [ { "type": "note", "name": "Note", "properties": ["content"] } ] } ```