Implements tag/category system backend following microformats2 p-category specification. Database changes: - Migration 008: Add tags and note_tags tables - Normalized tag storage (case-insensitive lookup, display name preserved) - Indexes for performance New module: - starpunk/tags.py: Tag management functions - normalize_tag: Normalize tag strings - get_or_create_tag: Get or create tag records - add_tags_to_note: Associate tags with notes (replaces existing) - get_note_tags: Retrieve note tags (alphabetically ordered) - get_tag_by_name: Lookup tag by normalized name - get_notes_by_tag: Get all notes with specific tag - parse_tag_input: Parse comma-separated tag input Model updates: - Note.tags property (lazy-loaded, prefer pre-loading in routes) - Note.to_dict() add include_tags parameter CRUD updates: - create_note() accepts tags parameter - update_note() accepts tags parameter (None = no change, [] = remove all) Micropub integration: - Pass tags to create_note() (tags already extracted by extract_tags()) - Return tags in q=source response Per design doc: docs/design/v1.3.0/microformats-tags-design.md Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
7.3 KiB
IndieAuth Implementation Questions - Answered
Quick Reference
All architectural questions have been answered. This document provides the concrete guidance needed for implementation.
Questions & Answers
✅ Q1: External Token Endpoint Response Format
Answer: Follow the IndieAuth spec exactly (W3C TR).
Expected Response:
{
"me": "https://user.example.net/",
"client_id": "https://app.example.com/",
"scope": "create update delete"
}
Error Responses: HTTP 400, 401, or 403 for invalid tokens.
✅ Q2: HTML Discovery Headers
Answer: These are links users add to THEIR websites, not StarPunk.
User's HTML (on their personal domain):
<link rel="authorization_endpoint" href="https://indielogin.com/auth">
<link rel="token_endpoint" href="https://tokens.indieauth.com/token">
<link rel="micropub" href="https://your-starpunk.example.com/api/micropub">
StarPunk's Role: Discover these endpoints from the user's URL, don't generate them.
✅ Q3: Migration Strategy
Architectural Decision: Keep migration 002, document it as future-use.
Action Items:
- Keep the migration file as-is
- Add comment: "Tables created for future V2 internal provider support"
- Don't use these tables in V1 (external verification only)
- No impact on existing production databases
Rationale: Empty tables cause no harm, avoid migration complexity later.
✅ Q4: Error Handling
Answer: Show clear, informative error messages.
Error Messages:
- Auth server down: "Authorization server is unreachable. Please try again later."
- Invalid token: "Access token is invalid or expired. Please re-authorize."
- Network error: "Cannot connect to authorization server."
HTTP Status Codes:
- 401: No token provided
- 403: Invalid/expired token
- 503: Auth server unreachable
✅ Q5: Cache Revocation Delay
Architectural Decision: Use 5-minute cache with configuration options.
Implementation:
# Default: 5-minute cache
MICROPUB_TOKEN_CACHE_TTL=300
MICROPUB_TOKEN_CACHE_ENABLED=true
# High security: disable cache
MICROPUB_TOKEN_CACHE_ENABLED=false
Security Notes:
- SHA256 hash tokens before caching
- Memory-only cache (not persisted)
- Document 5-minute delay in security guide
- Allow disabling for high-security needs
Implementation Checklist
Immediate Actions
-
Remove Internal Provider Code:
- Delete
/auth/authorizeendpoint - Delete
/auth/tokenendpoint - Remove token issuance logic
- Remove authorization code generation
- Delete
-
Implement External Verification:
# Core verification function def verify_micropub_token(bearer_token, expected_me): # 1. Check cache (if enabled) # 2. Discover token endpoint from expected_me # 3. Verify with external endpoint # 4. Cache result (if enabled) # 5. Return validation result -
Add Configuration:
# Required ADMIN_ME=https://user.example.com # Optional (with defaults) MICROPUB_TOKEN_CACHE_ENABLED=true MICROPUB_TOKEN_CACHE_TTL=300 -
Update Error Handling:
try: response = httpx.get(endpoint, timeout=5.0) except httpx.TimeoutError: return error(503, "Authorization server is unreachable")
Code Examples
Token Verification
def verify_token(bearer_token: str, token_endpoint: str, expected_me: str) -> Optional[dict]:
"""Verify token with external endpoint"""
try:
response = httpx.get(
token_endpoint,
headers={'Authorization': f'Bearer {bearer_token}'},
timeout=5.0
)
if response.status_code == 200:
data = response.json()
if data.get('me') == expected_me and 'create' in data.get('scope', ''):
return data
return None
except httpx.TimeoutError:
raise TokenEndpointError("Authorization server is unreachable")
Endpoint Discovery
def discover_token_endpoint(me_url: str) -> str:
"""Discover token endpoint from user's URL"""
response = httpx.get(me_url)
# 1. Check HTTP Link header
if link := parse_link_header(response.headers.get('Link'), 'token_endpoint'):
return urljoin(me_url, link)
# 2. Check HTML <link> tags
if 'text/html' in response.headers.get('content-type', ''):
if link := parse_html_link(response.text, 'token_endpoint'):
return urljoin(me_url, link)
raise DiscoveryError(f"No token endpoint found at {me_url}")
Micropub Endpoint
@app.route('/api/micropub', methods=['POST'])
def micropub_endpoint():
# Extract token
auth = request.headers.get('Authorization', '')
if not auth.startswith('Bearer '):
return {'error': 'unauthorized'}, 401
token = auth[7:] # Remove "Bearer "
# Verify token
try:
token_info = verify_micropub_token(token, app.config['ADMIN_ME'])
if not token_info:
return {'error': 'forbidden'}, 403
except TokenEndpointError as e:
return {'error': 'temporarily_unavailable', 'error_description': str(e)}, 503
# Process Micropub request
# ... create note ...
return '', 201, {'Location': note_url}
Testing Guide
Manual Testing
- Configure your domain with IndieAuth links
- Set ADMIN_ME in StarPunk config
- Use Quill (https://quill.p3k.io) to test posting
- Verify token caching works (check logs)
- Test with auth server down (block network)
Automated Tests
def test_token_verification():
# Mock external token endpoint
with responses.RequestsMock() as rsps:
rsps.add(responses.GET, 'https://tokens.example.com/token',
json={'me': 'https://user.com', 'scope': 'create'})
result = verify_token('test-token', 'https://tokens.example.com/token', 'https://user.com')
assert result['me'] == 'https://user.com'
def test_auth_server_unreachable():
# Mock timeout
with pytest.raises(TokenEndpointError, match="unreachable"):
verify_token('test-token', 'https://timeout.example.com/token', 'https://user.com')
User Documentation Template
For Users: Setting Up IndieAuth
-
Add to your website's HTML:
<link rel="authorization_endpoint" href="https://indielogin.com/auth"> <link rel="token_endpoint" href="https://tokens.indieauth.com/token"> <link rel="micropub" href="[YOUR-STARPUNK-URL]/api/micropub"> -
Configure StarPunk:
ADMIN_ME=https://your-website.com -
Test with a Micropub client:
- Visit https://quill.p3k.io
- Enter your website URL
- Authorize and post!
Summary
All architectural questions have been answered:
- Token Format: Follow IndieAuth spec exactly
- HTML Headers: Users configure their own domains
- Migration: Keep tables for future use
- Errors: Clear messages about connectivity
- Cache: 5-minute TTL with disable option
The implementation path is clear: remove internal provider code, implement external verification with caching, and provide good error messages. This aligns with StarPunk's philosophy of minimal code and IndieWeb principles.
Ready for Implementation: All questions answered, examples provided, architecture documented.