MCP clients like Claude Code were unable to use tools because tokens
obtained directly from Nextcloud had the wrong audience claim. The MCP
server now acts as its own OAuth Authorization Server, proxying auth
to Nextcloud with its own client_id so tokens have the correct audience.
New endpoints: /.well-known/oauth-authorization-server, /oauth/token,
/oauth/register. Modified /oauth/authorize from pass-through to
intermediary pattern. PRM now points authorization_servers to the MCP
server instead of Nextcloud.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add cross-product matrix (3 versions x 4 auth modes = 12 CI jobs)
- Parameterize Nextcloud image in docker-compose.yml via NEXTCLOUD_IMAGE env var
- Pin NC 31.0.8, 32.0.6, 33.0.0 with SHA digests in workflow
- Add Renovate customManagers to auto-update NC images in workflow
- Fix Astrolabe install hook to prefer volume mount over app store
- Bump Astrolabe submodule to support NC 33 (max-version 31→33)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Astrolabe has been extracted to its own repository at
github.com/cbcoutinho/astrolabe for independent releases.
Changes:
- Replace third_party/astrolabe/ directory with git submodule
- Remove astrolabe-ci.yml and appstore-build-publish.yml workflows
- Remove scripts/bump-astrolabe.sh
- Remove Astrolabe sections from bump-version.yml workflow
- Remove Astrolabe build steps from test.yml CI workflow
- Remove astrolabe volume mount from docker-compose.yml
- Simplify astrolabe install hook to always use app store
- Update CONTRIBUTING.md to reflect two-component monorepo
Co-Authored-By: Claude Opus 4.6 <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>
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>
Add explicit property type declarations to IdpTokenRefresher,
CredentialsController, OAuthController, and McpServerClient classes.
This improves type safety and allows Psalm to properly infer types,
eliminating MissingPropertyType and many MixedMethodCall errors.
Also adds IClient import where needed and validates getSystemValue
returns to ensure string types before use.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
GitHub workflows should be defined only in the root .github directory,
not in the subproject directory.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Delete stored token when refresh callback fails or returns null
- Delete stored token when expired with no refresh callback available
- Fix test namespaces (Service → OCA\Astrolabe\Tests\Unit\Service)
- Update tests to verify token deletion on refresh failure
Prevents repeated refresh attempts with stale tokens that will never
succeed, improving error handling and reducing unnecessary API calls.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace NcCheckboxRadioSwitch :checked with :model-value
- Replace NcCheckboxRadioSwitch @update:checked with @update:model-value
- Replace NcButton type="primary|secondary|tertiary" with variant prop
- Bump @nextcloud/vue minimum version to ^9.3.3
These changes address deprecated APIs removed in @nextcloud/vue v9.0.0:
- :checked/:update:checked was replaced by v-model/modelValue pattern
- type prop for button variants was replaced by variant prop
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix PHP CS Fixer issues (single quotes, indentation)
- Add typed property declarations to ApiController
- Add Psalm baseline to suppress 517 pre-existing errors
- Fix workflow name references (astroglobe → astrolabe)
The CI workflow was previously watching a non-existent path and never
ran. After fixing the path trigger, these pre-existing code quality
issues were discovered. The Psalm baseline allows CI to pass while
tracking technical debt for incremental resolution.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The IdpTokenRefresher was incorrectly using overwrite.cli.url (the
external URL like http://localhost:8080) for internal token refresh
requests. This URL is not accessible from inside Docker containers
since port 8080 is only mapped on the host machine.
Changed getNextcloudBaseUrl() to:
- Always use http://localhost (internal port 80) by default
- Added optional astrolabe_internal_url config for custom setups
- Removed overwrite.cli.url usage (intended for external URLs only)
This fixes 401 errors in Astrolabe semantic search when OAuth tokens
need to be refreshed in containerized deployments.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use :input-label prop for NcSelect field labels instead of :label
(the :label prop sets the option label property key, not the visible label)
- Fix CSS loading in admin.php and personal.php templates to use
astrolabe-main (the bundled CSS file)
- Update minimum Nextcloud version to 31 (required for Vue 3)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
In hybrid mode (multi_user_basic + offline_access), users need BOTH:
- OAuth token for Astrolabe→MCP API calls
- App password for MCP→Nextcloud background sync
Changes:
- Personal.php: Pass correct oauthUrl pointing to Astrolabe's OAuth
controller instead of MCP server's browser OAuth. Check both OAuth
token AND app password status in hybrid mode.
- personal.php template: Show two-step workflow UI requiring both
credentials before showing "Active" status. Each step shows
completion badges.
- IdpTokenRefresher.php: Use http://localhost for internal token
refresh requests (consistent with OAuthController). External URLs
like localhost:8080 don't work from inside the container.
Fixes 401 errors when searching in Astrolabe with hybrid deployment.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Change limit initialization from string '20' to number 20 in App.vue
- Update AdminSettings.vue NcTextField to use v-model instead of legacy
:value/@update:value bindings
- Update AdminSettings.vue NcSelect components to use :model-value with
computed getters and @update:model-value for proper object-to-id
conversion (same pattern as App.vue)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The astrolabe app was using Vue 2 style bindings that don't work with
@nextcloud/vue 9.x and Vue 3:
- NcTextField: Changed from :value/@update:value to v-model
- NcSelect: Changed from v-model (with computed prop) to
:model-value/@update:model-value
The legacy :value and @update:value props were being ignored because
@nextcloud/vue 9.x components use modelValue/update:modelValue internally.
This caused the search button to remain disabled and the algorithm
dropdown to be unresponsive.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The @nextcloud/vue library (v9.x) requires appName and appVersion to be
defined as global constants at build time. Without these, the library
logs an error: "The '@nextcloud/vue' library was used without setting /
replacing the 'appName'."
This fix reads the app ID and version from appinfo/info.xml and injects
them via Vite's define option, matching how @nextcloud/webpack-vue-config
handles this for webpack-based apps.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Management API:
- Extract _get_app_password_storage() helper function
- Reduces code duplication across 3 endpoints
- Adds TYPE_CHECKING import for type hints
PHP CredentialsController:
- Add partial_success field to distinguish full vs partial success
- Add local_storage and mcp_sync boolean fields for clarity
- Rename 'warning' to 'mcp_error' for consistency
- Improves UI feedback when MCP server sync fails
Response structure now clearly indicates:
- Full success: partial_success=false, local_storage=true, mcp_sync=true
- Partial success: partial_success=true, local_storage=true, mcp_sync=false
- Full failure: success=false (unchanged)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, the multi-user BasicAuth mode attempted to retrieve app passwords
via OAuth client_credentials grant, which Nextcloud OIDC doesn't support.
This fix implements local storage for app passwords:
- Add app_passwords table via Alembic migration (002)
- Add store/get/delete methods to RefreshTokenStorage
- Add management API endpoints for app password provisioning:
- POST /api/v1/users/{user_id}/app-password
- GET /api/v1/users/{user_id}/app-password
- DELETE /api/v1/users/{user_id}/app-password
- Update oauth_sync.py to read from local storage
- Update Astrolabe to send app passwords to MCP server after validation
- Add app-hook to configure mcp_server_url in Nextcloud
The flow is now:
1. User creates app password in Nextcloud Security settings
2. User enters it in Astrolabe Personal Settings
3. Astrolabe validates against Nextcloud, then sends to MCP server
4. MCP server stores encrypted app password locally
5. Background sync uses locally stored password
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>