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>
267 lines
7.3 KiB
Markdown
267 lines
7.3 KiB
Markdown
# 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**:
|
|
```json
|
|
{
|
|
"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):
|
|
```html
|
|
<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**:
|
|
1. Keep the migration file as-is
|
|
2. Add comment: "Tables created for future V2 internal provider support"
|
|
3. Don't use these tables in V1 (external verification only)
|
|
4. 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**:
|
|
```python
|
|
# 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
|
|
|
|
1. **Remove Internal Provider Code**:
|
|
- Delete `/auth/authorize` endpoint
|
|
- Delete `/auth/token` endpoint
|
|
- Remove token issuance logic
|
|
- Remove authorization code generation
|
|
|
|
2. **Implement External Verification**:
|
|
```python
|
|
# 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
|
|
```
|
|
|
|
3. **Add Configuration**:
|
|
```ini
|
|
# Required
|
|
ADMIN_ME=https://user.example.com
|
|
|
|
# Optional (with defaults)
|
|
MICROPUB_TOKEN_CACHE_ENABLED=true
|
|
MICROPUB_TOKEN_CACHE_TTL=300
|
|
```
|
|
|
|
4. **Update Error Handling**:
|
|
```python
|
|
try:
|
|
response = httpx.get(endpoint, timeout=5.0)
|
|
except httpx.TimeoutError:
|
|
return error(503, "Authorization server is unreachable")
|
|
```
|
|
|
|
---
|
|
|
|
## Code Examples
|
|
|
|
### Token Verification
|
|
```python
|
|
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
|
|
```python
|
|
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
|
|
```python
|
|
@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
|
|
1. Configure your domain with IndieAuth links
|
|
2. Set ADMIN_ME in StarPunk config
|
|
3. Use Quill (https://quill.p3k.io) to test posting
|
|
4. Verify token caching works (check logs)
|
|
5. Test with auth server down (block network)
|
|
|
|
### Automated Tests
|
|
```python
|
|
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
|
|
|
|
1. **Add to your website's HTML**:
|
|
```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">
|
|
```
|
|
|
|
2. **Configure StarPunk**:
|
|
```ini
|
|
ADMIN_ME=https://your-website.com
|
|
```
|
|
|
|
3. **Test with a Micropub client**:
|
|
- Visit https://quill.p3k.io
|
|
- Enter your website URL
|
|
- Authorize and post!
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
All architectural questions have been answered:
|
|
|
|
1. **Token Format**: Follow IndieAuth spec exactly
|
|
2. **HTML Headers**: Users configure their own domains
|
|
3. **Migration**: Keep tables for future use
|
|
4. **Errors**: Clear messages about connectivity
|
|
5. **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. |