Create unified documentation covering authentication flows across all five
deployment modes. Documents three communication patterns (MCP Client → MCP
Server → Nextcloud, background sync, Astrolabe → MCP Server) with ASCII
sequence diagrams and implementation references.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add pagination to getAllUsersWithTokens() with limit/offset params
- Update RefreshUserTokens to process users in batches of 100
- Add lock TTL documentation to withTokenLock() docstring
- Fix psalm type errors in getAccessToken() method
- Add unit tests for pagination and batched processing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds distributed locking using Nextcloud's ILockingProvider to prevent
race conditions between background job and on-demand token refresh.
Uses double-check locking pattern:
1. Quick check without lock - return immediately if token is valid
2. Acquire exclusive lock if token needs refresh
3. Re-check after lock - another process may have refreshed
4. Refresh only if still needed
5. Graceful degradation on LockedException
Changes:
- McpTokenStorage: add ILockingProvider, withTokenLock() method
- McpTokenStorage: update getAccessToken() with locking pattern
- RefreshUserTokens: wrap refresh in withTokenLock(), catch LockedException
- Add comprehensive unit tests for locking behavior
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes missing issued_at parameter when storing tokens refreshed via
getAccessToken() callback, ensuring accurate token lifetime calculation
for the background refresh job.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Prevents users from having to re-authorize Astrolabe after periods of
inactivity by proactively refreshing OAuth tokens before they expire.
Changes:
- Add RefreshUserTokens background job that runs every 15 minutes
- Add on-demand token refresh in SemanticSearchProvider (Unified Search)
- Store issued_at timestamp for accurate token lifetime calculation
- Add getAllUsersWithTokens() to query users needing refresh
The job dynamically calculates refresh threshold based on actual token
lifetime (50% remaining), working with any IdP (Nextcloud OIDC, Keycloak,
etc.) rather than relying on IdP-specific configuration.
Closes#510
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Split the monolithic management.py (1988 lines) into 4 focused modules:
- management.py: Server status, user sessions, shared helpers (~520 lines)
- passwords.py: App password provisioning for BasicAuth mode (~300 lines)
- webhooks.py: Webhook registration management (~290 lines)
- visualization.py: Search and PDF preview endpoints (~810 lines)
Backward compatibility maintained via __init__.py re-exports.
Updated test imports to use new module paths.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add allowed_bots configuration for renovate-bot-cbcoutinho to enable
Claude Code review on dependency update PRs.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add allowed_bots configuration for renovate-bot-cbcoutinho to enable
Claude Code review on dependency update PRs.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fix Psalm static analysis errors:
- Add return type annotations to refresh callback closures
- Use strict null comparisons instead of truthy/falsy checks
- Cast response body to string for json_decode
- Add type annotation for decoded JSON data
- Update psalm-baseline.xml to remove fixed issues
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace the client-side PDF.js viewer with server-side rendering using PyMuPDF.
This avoids CSP worker restrictions and ES private field access issues that
affected Chromium browsers.
Changes:
- Add /api/v1/pdf-preview endpoint to MCP server (management.py)
- Add pdf-preview route and controller action in Astrolabe PHP backend
- Refactor PDFViewer.vue to display server-rendered PNG images
- Remove pdfjs-dist dependency and client-side PDF loading code
- Use @nextcloud/axios for CSRF token handling in PDFViewer
The server downloads the PDF via WebDAV, renders the requested page with
PyMuPDF at the specified scale, and returns a base64-encoded PNG image.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update psalm-baseline.xml to match renamed OauthController.php (lowercase 'a')
- Move AlertCircle import to top of PDFViewer.vue to satisfy ESLint import/first rule
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When viewing PDF chunks in semantic search, the PDF viewer failed with
"can't access private field" errors. This was caused by:
1. CSP blocks web workers (worker-src 'none'), forcing fake worker mode
2. Vite transforms ES private fields in the bundle, but the worker file
is untransformed, causing incompatible private field implementations
3. Vue's ref() wraps PDFDocumentProxy in a Proxy, which can't access
ES private fields
Fixed by:
- Loading pdfjs-dist externally via script tag (avoids Vite transform)
- Creating pdfjs-loader.mjs that imports pdf.mjs and sets window.pdfjsLib
- Using Util::addScript() for CSP-compliant script loading with nonces
- Using shallowRef() instead of ref() for pdfDoc to avoid Proxy wrapper
- Setting workerSrc at runtime using OC.linkTo() for correct app path
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace generic "Network error" with specific error messages:
- Show backend error message when available from HTTP response
- Display "Authorization required. Please complete Step 1 in
Settings → Astrolabe." for 401 Unauthorized errors
- Show "Search service unavailable" for 503 errors
- Keep generic network error only for actual connection failures
This helps users understand when they need to complete OAuth
authorization vs when there's an actual network problem.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Rename OAuthController.php to OauthController.php for consistency
- Fix Personal.php to check specifically for app password presence
using getBackgroundSyncPassword() instead of hasBackgroundSyncAccess()
for hybrid auth mode
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace Close button click with Escape key in app password dialog
(h2 element was intercepting pointer events)
- Make test_users_setup fixture idempotent by checking user existence
before creation and only tracking created users for cleanup
- Fix search results detection by removing wait for .app-content-wrapper
CSS class that doesn't exist in Astrolabe's Vue app
- Add progress logging during results polling
- Increase polling timeout to 30 seconds for search results
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add dbquery.py for MariaDB and sqlitequery.py for SQLite databases
in MCP service containers. Both scripts wrap docker compose exec to
simplify database inspection during development.
- dbquery.py: Query Nextcloud MariaDB with vertical/JSON output
- sqlitequery.py: Query MCP service SQLite DBs with service aliases
(mcp, oauth, keycloak, basic) and column/JSON output modes
- Document both scripts in CLAUDE.md Database Inspection section
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>