Commit Graph
100 Commits
Author SHA1 Message Date
Chris CoutinhoandClaude Opus 4.6 da104c59ac fix: handle categories, recurrence_rule, attendees, and reminder_minutes in update_event
_merge_ical_properties() only handled a subset of event fields, silently
dropping categories, recurrence_rule, attendees, and reminder_minutes
during updates. These fields were fully supported by _create_ical_event()
and accepted by the MCP tool, but never applied.

Closes #544

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 23:15:37 +01:00
Chris CoutinhoandClaude Opus 4.6 66e32d4705 fix: expand recurring events in date-range queries
PR #539 fixed date-range filtering so events outside the queried range
are excluded. However, recurring events still returned the master event
with its original DTSTART instead of expanded occurrences.

Add <C:expand> element to CalDAV REPORT requests (RFC 4791 §9.6.5) when
both date bounds are provided, so the server returns one VEVENT per
occurrence with the correct DTSTART. Refactor VEVENT parsing into a
shared helper and add _parse_all_ical_events() to handle multi-VEVENT
responses from expanded results.

Closes #538

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 12:43:40 +01:00
Chris CoutinhoandClaude Opus 4.6 b79ac29a9d fix: use CalDAV time-range filter for calendar date range queries
get_calendar_events() accepted start/end datetime parameters but called
calendar.events() which fetches all events, silently discarding the
date filters. This caused nc_calendar_list_events and
nc_calendar_get_upcoming_events to return the entire calendar history.

Add _search_events_by_date() helper that builds a CalDAV REPORT query
with a <time-range> filter (RFC 4791 §9.9) for server-side filtering.
Falls back to calendar.events() when no dates are given.

Closes #538

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 17:35:33 +01:00
Chris CoutinhoandClaude Opus 4.5 a7581a1d1b fix(helm): add backward compatibility for legacy persistence configs
- Add helper functions to detect and use legacy persistence configs
- Legacy auth.multiUserBasic.persistence.* and qdrant.localPersistence.*
  configs continue to work but show deprecation warnings in NOTES.txt
- New dataStorage.enabled takes precedence when explicitly set
- PVC size/accessMode/storageClass values from legacy configs are honored

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 07:45:30 +01:00
Chris Coutinho 8e0d64f7d3 Merge branch 'master' of github.com:cbcoutinho/nextcloud-mcp-server 2026-01-30 19:26:34 +00:00
Chris Coutinho c97ffe8e47 docs(astrolabe): Add initial blog post 2026-01-30 19:17:23 +00:00
Chris Coutinho 9ec00d4de5 chore: Update screenshot names 2026-01-30 19:14:10 +00:00
Chris CoutinhoandClaude Opus 4.5 c7882adb24 docs: add authentication flows reference by deployment mode
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>
2026-01-28 08:38:29 +01:00
Chris CoutinhoandClaude Opus 4.5 9491d698e8 fix(astrolabe): add pagination and psalm fixes for token refresh
- 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>
2026-01-28 08:13:22 +01:00
Chris CoutinhoandClaude Opus 4.5 5b71ac3251 fix(astrolabe): add locking to prevent token refresh race condition
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>
2026-01-27 22:23:42 +01:00
Chris CoutinhoandClaude Opus 4.5 815a09be34 test(astrolabe): add unit tests for background token refresh
- Fix McpTokenStorageTest: add IDBConnection mock for new constructor parameter
- Add doctrine/dbal dev dependency for IQueryBuilder mock support
- Add tests for getAllUsersWithTokens() database query method
- Create RefreshUserTokensTest with comprehensive coverage:
  - Job interval configuration (15 min)
  - Token refresh threshold logic (50% lifetime)
  - issued_at tracking for accurate lifetime calculation
  - Fallback to default lifetime when issued_at missing
  - Token rotation handling
  - Error handling and logging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 12:23:06 +01:00
Chris CoutinhoandClaude Opus 4.5 c46f9eb212 fix(astrolabe): add issued_at to on-demand token refresh
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>
2026-01-27 11:25:43 +01:00
Chris CoutinhoandClaude Opus 4.5 28219e00e7 feat(astrolabe): add background token refresh job
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>
2026-01-27 10:34:39 +01:00
Chris CoutinhoandClaude Opus 4.5 2e7774654b refactor(api): split management.py into domain-focused modules
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>
2026-01-26 21:28:18 +01:00
Chris Coutinho 61ce873411 chore: Address reviewer comments and add error handling to PDF chunk viz preview endpoints 2026-01-26 21:16:31 +01:00
Chris Coutinho 8507e480d6 Merge remote-tracking branch 'origin/master' into feat/pymupdf-pdf-rendering 2026-01-26 20:29:34 +01:00
Chris CoutinhoandClaude Opus 4.5 905d18baf7 ci(claude): allow renovate bot to trigger code reviews
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>
2026-01-26 20:29:17 +01:00
Chris CoutinhoandClaude Opus 4.5 b5e5d86790 ci(claude): allow renovate bot to trigger code reviews
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>
2026-01-26 20:27:17 +01:00
Chris CoutinhoandClaude Opus 4.5 c35e94b0bc test(api): add unit tests for PDF preview management endpoint
Add comprehensive unit tests for the /api/v1/pdf-preview endpoint:
- Parameter validation (file_path, page, scale)
- OAuth token authentication
- PDF rendering with PyMuPDF
- Error handling (file not found, invalid page, corrupted PDF)
- Edge cases (URL-encoded paths, boundary values, missing config)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 20:24:33 +01:00
Chris CoutinhoandClaude Opus 4.5 c09ebe99cc fix(astrolabe): resolve Psalm type errors in PDF preview code
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>
2026-01-26 20:19:23 +01:00
Chris CoutinhoandClaude Opus 4.5 d5544a7731 refactor(astrolabe): replace client-side PDF.js with server-side PyMuPDF rendering
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>
2026-01-26 20:04:57 +01:00
Chris CoutinhoandClaude Opus 4.5 bc62f2a066 fix(astrolabe): fix Psalm baseline and ESLint import order
- 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>
2026-01-25 21:41:48 +01:00
Chris CoutinhoandClaude Opus 4.5 38adb96be4 fix(astrolabe): load pdfjs-dist externally to fix PDF viewer
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>
2026-01-25 21:08:44 +01:00
Chris CoutinhoandClaude Opus 4.5 c76dd21eeb fix(astrolabe): improve error messages for authorization issues
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>
2026-01-24 16:21:57 +01:00
Chris CoutinhoandClaude Opus 4.5 c5bf4cda8a fix(astrolabe): rename OAuthController and fix app password check
- 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>
2026-01-24 13:04:23 +01:00
Chris CoutinhoandClaude Opus 4.5 0b6a6b0842 fix(tests): improve Astrolabe integration test reliability
- 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>
2026-01-24 13:03:35 +01:00
Chris CoutinhoandClaude Opus 4.5 7b75304c9f feat(scripts): add database query helpers for development
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>
2026-01-24 09:26:30 +01:00
Chris Coutinho 9004e14022 Merge remote-tracking branch 'origin/master' into renovate/plotly.js-dist-min-3.x 2026-01-21 18:22:05 +01:00
Chris CoutinhoandClaude Opus 4.5 e7a3dd698a fix(astrolabe): update Plotly title attributes for v3 compatibility
Plotly.js v3 removed string format for title attributes (plotly/plotly.js#7212).
All titles must now use object format: { text: "..." }

Changes:
- Main layout title: string → { text: "..." }
- Scene axis titles (xaxis, yaxis, zaxis): string → { text: "..." }
- Colorbar title: string → { text: "..." }

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 14:04:49 +01:00
Chris CoutinhoandClaude Opus 4.5 f0ade4ad28 refactor(astrolabe): add PHP property types to fix Psalm errors
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>
2026-01-17 21:24:56 +01:00
Chris CoutinhoandClaude Opus 4.5 737f10f190 fix(astrolabe): improve token refresh error handling and validation
- Extract magic number to TOKEN_EXPIRY_BUFFER_SECONDS constant
- Add URL validation for astrolabe_internal_url with fallback
- Warn when internal URL uses external port mapping (misconfiguration)
- Differentiate HTTP error handling by status code:
  - Network errors (LocalServerException): warning level
  - Auth errors (401/403): error level (token invalid)
  - Server errors (500+): warning level (transient)
- Reduce log level for IdP selection messages to debug
- Add integration tests for credential storage, isolation, and revoke/reprovision

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 19:43:15 +01:00
Chris Coutinho 813e9a60cb chore: Run npm install 2026-01-17 15:03:33 +01:00
Chris CoutinhoandClaude Opus 4.5 5c25b87cbe chore(astrolabe): remove duplicate .github workflows
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>
2026-01-17 14:26:20 +01:00
Chris CoutinhoandClaude Opus 4.5 e48c5fa9a2 fix(astrolabe): delete stale tokens when refresh fails
- 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>
2026-01-17 14:21:53 +01:00
Chris CoutinhoandClaude Opus 4.5 303efeddf7 refactor(astrolabe): upgrade to @nextcloud/vue 9.3.3 API
- 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>
2026-01-17 13:09:27 +01:00
Chris CoutinhoandClaude Opus 4.5 fef13a6d3d test(astrolabe): add comprehensive unit tests for token refresh and storage
Add unit tests addressing reviewer feedback on test coverage gaps:

IdpTokenRefresher::refreshAccessToken() tests:
- Token refresh with internal Nextcloud OIDC
- Token refresh with external IdP (Keycloak)
- Error handling: missing client_secret, missing MCP URL
- Error handling: invalid responses, HTTP exceptions
- Token rotation validation (missing refresh_token in response)

McpTokenStorage tests (multi-user basic auth):
- OAuth token storage, retrieval, deletion
- Token expiration checks with 60-second buffer
- getAccessToken with automatic refresh callback
- App password storage for background sync
- hasBackgroundSyncAccess() for both OAuth and app passwords
- Background sync type detection and timestamp tracking

Test coverage: 41 tests, 76 assertions (up from 5 tests)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 11:00:45 +01:00
Chris CoutinhoandClaude Opus 4.5 c4973290a6 fix(astrolabe): resolve CI failures for code quality checks
- 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>
2026-01-17 10:56:36 +01:00
Chris CoutinhoandClaude Opus 4.5 c018268681 docs(astrolabe): add config docs and unit tests for internal URL
Address PR #487 reviewer feedback:

- Add documentation for `astrolabe_internal_url` config option
- Add unit tests for `IdpTokenRefresher::getNextcloudBaseUrl()`
- Fix CI workflow paths (astroglobe -> astrolabe)
- Add PHPUnit job to CI workflow for PHP 8.1, 8.2, 8.3
- Remove obsolete ApiTest that tested non-existent method

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 22:24:43 +01:00
Chris CoutinhoandClaude Opus 4.5 79cfb65590 fix(astrolabe): use internal URL for OAuth token refresh
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>
2026-01-16 19:42:54 +01:00
Chris CoutinhoandClaude Opus 4.5 1cc460b0d8 fix(astrolabe): Address reviewer feedback for hybrid mode
Addresses code review feedback:

Personal.php:
- Consolidate template variables to use camelCase consistently
- Remove duplicate snake_case variables (auth_mode, supports_app_passwords)
- Add oauthUrl to standard OAuth mode parameters (fixes fallback issue)
- Add requesttoken for CSRF protection

personal.php (template):
- Use null coalescing for safe variable access
- Reuse computed $isHybridMode variable instead of duplicate check
- Remove complex fallback URL logic (oauthUrl now always provided)

IdpTokenRefresher.php:
- Use Nextcloud's overwrite.cli.url config when available
- Fall back to http://localhost for container deployments
- Better supports non-containerized environments

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 10:44:52 +01:00
Chris CoutinhoandClaude Opus 4.5 104a2ec9e3 test: Add unit tests for status endpoint OIDC config
Add unit tests for /api/v1/status endpoint focusing on OIDC config:
- Test hybrid mode (multi_user_basic + enable_offline_access) returns OIDC
- Test pure multi_user_basic mode without offline_access omits OIDC
- Test OAuth mode returns OIDC config
- Test single-user BasicAuth mode omits OIDC config
- Test partial OIDC config (only discovery_url or only issuer)

Also updates docs/authentication.md with Astrolabe hybrid mode setup:
- Two-step credential setup (OAuth + app password)
- Technical details for each credential type
- Request direction table explaining why two credentials needed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 10:43:59 +01:00
Chris CoutinhoandClaude Opus 4.5 e87ae56041 fix(astrolabe): Fix NcSelect options and CSS loading
- 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>
2026-01-15 17:21:22 +01:00
Chris CoutinhoandClaude Opus 4.5 c95459234b fix(astrolabe): fix OAuth flow and settings UI for hybrid mode
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>
2026-01-15 16:14:00 +01:00
Chris CoutinhoandClaude Opus 4.5 f16f852b23 fix(api): return OIDC config in hybrid mode for Astrolabe OAuth flow
The /api/v1/status endpoint now returns OIDC configuration (discovery_url,
issuer) when running in hybrid mode (multi_user_basic + offline_access),
not just in pure OAuth mode.

This allows Astrolabe to discover the IdP and complete the OAuth flow
for obtaining tokens to call MCP server management APIs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 16:13:50 +01:00
Chris CoutinhoandClaude Opus 4.5 006a3d95d6 fix(astrolabe): address review feedback for Vue 3 bindings
- 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>
2026-01-15 12:16:08 +01:00
Chris CoutinhoandClaude Opus 4.5 cb4e8acd9f fix(astrolabe): update Vue component bindings for Vue 3 compatibility
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>
2026-01-15 12:06:20 +01:00
Chris Coutinho 929c40709a fix(ci): bump helm chart version when MCP appVersion changes
When the MCP server version is bumped, the Helm chart's appVersion is
updated but the chart version was not. This caused users to not see
new chart releases when the app version changed.

Now the Helm chart version is bumped (PATCH) whenever:
1. There are helm-scoped commits (existing behavior)
2. OR when the MCP server version is bumped (new behavior)

This ensures Helm users always get notified of new releases containing
updated app versions.
2026-01-15 09:59:09 +01:00
Chris Coutinho c192bd2ec9 chore: update docker-compose.yml env var names for semantic search 2026-01-15 09:48:38 +01:00
Chris CoutinhoandClaude Opus 4.5 c6295b48a5 fix(astrolabe): define appName and appVersion for @nextcloud/vue
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>
2026-01-15 08:59:31 +01:00
Chris Coutinho cf0781d2fe bump: version 0.56.2 → 0.57.0 2026-01-15 07:32:46 +01:00
Chris CoutinhoandClaude Opus 4.5 01ad2b3d21 refactor: Use get_settings() for vector sync enabled check
Replace direct os.getenv() calls with get_settings().vector_sync_enabled
to ensure consistent behavior with both VECTOR_SYNC_ENABLED (deprecated)
and ENABLE_SEMANTIC_SEARCH environment variables.

Also add webhook management documentation guide.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 20:30:51 +01:00
Chris CoutinhoandClaude Opus 4.5 e4cddef343 fix: Add missing annotations for deck remove/unassign operations
- Add destructiveHint=True to deck_remove_label_from_card and
  deck_unassign_user_from_card (ADR-017 compliance)
- Set idempotentHint=True since remove operations produce same end state
- Update test_annotations.py to exclude nc_webdav_create_directory from
  non-idempotent check (MKCOL is idempotent by design - returns 405 if exists)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 20:02:20 +01:00
Chris CoutinhoandClaude Opus 4.5 f15baefe7e feat: Add rate limiting and extract helpers for app password endpoints
Security improvements:
- Add in-memory rate limiter for app password provisioning (5 attempts/hour/user)
- Returns 429 Too Many Requests with Retry-After header when limit exceeded
- Rate limiting is per-user to prevent cross-user DoS

Code quality improvements:
- Extract _extract_basic_auth() helper to reduce duplication across 3 endpoints
- Move base64, re imports to module level
- Add APP_PASSWORD_PATTERN constant for regex validation
- Add NEXTCLOUD_VALIDATION_TIMEOUT constant (10s)

Test coverage:
- Add test_provision_app_password_rate_limiting
- Add test_rate_limiting_is_per_user
- Add autouse fixture to clear rate limit state between tests
- Total: 15 tests for management API endpoints

Addresses reviewer feedback on PR #473.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 14:02:00 +01:00
Chris CoutinhoandClaude Opus 4.5 6affad1c8b refactor: Extract storage helper and improve PHP error handling
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>
2026-01-13 21:50:34 +01:00
Chris CoutinhoandClaude Opus 4.5 370c3ff444 test: Add comprehensive tests for app password storage and provisioning
- Add 12 unit tests for RefreshTokenStorage app password methods
  - Basic CRUD operations (store, get, delete)
  - Encryption verification (passwords encrypted at rest)
  - Error handling (missing encryption key, wrong key)
  - Multi-user independence

- Add 13 unit tests for Management API endpoints
  - POST /api/v1/users/{user_id}/app-password provisioning
  - GET /api/v1/users/{user_id}/app-password status
  - DELETE /api/v1/users/{user_id}/app-password deletion
  - Auth validation (BasicAuth, username matching)
  - Nextcloud credential validation

- Rewrite 10 integration tests for new architecture
  - Remove AstrolabeClient/OAuth dependency
  - Use local RefreshTokenStorage for app passwords
  - Test BasicAuth and OAuth mode separation
  - Test NotProvisionedError scenarios

Addresses reviewer feedback on PR #473 requiring test coverage.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 21:44:23 +01:00
Chris CoutinhoandClaude Opus 4.5 e486e92f91 fix(auth): Store app passwords locally for multi-user BasicAuth background sync
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>
2026-01-13 15:44:11 +01:00
Chris CoutinhoandClaude Opus 4.5 a26a470af6 fix(deck): Always preserve fields in update_card for partial updates
The Deck PUT API is a full replacement, not a partial update.
Previously, title and description were conditionally sent, causing:
- 400 errors when title not provided (it's required)
- Description being cleared when not explicitly set

Now all required fields (title, type, owner) and description are
always included in the payload using current card values when not
explicitly provided. This matches the existing pattern for type/owner.

Also simplified owner extraction since DeckCard.validate_owner
already ensures it's always a string.

Fixes #452

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 23:30:01 -06:00
Chris CoutinhoandClaude Opus 4.5 71ace47197 test: Define expected partial update behavior for DeckClient.update_card
Refactor tests to assert what SHOULD happen (partial updates preserve
unchanged fields) rather than documenting current buggy behavior.

Tests will fail until fix is implemented in client or upstream.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 22:28:23 -06:00
Chris CoutinhoandClaude Opus 4.5 30d3d9f0cf test: Add integration tests documenting DeckClient.update_card bugs
Tests document current behavior of update_card method:
- Updating without title fails (400) - title required but conditionally sent
- Updating with title clears description - PUT is full replacement

Related: #452

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 11:52:57 -06:00
Chris CoutinhoandClaude Opus 4.5 dd23191987 fix(astrolabe): Fix CSS loading for Nextcloud apps
Two issues prevented CSS from loading correctly:

1. Entry point naming mismatch: Vite output `main.css` but Nextcloud's
   `Util::addStyle('astrolabe', 'astrolabe-main')` expected `astrolabe-main.css`

2. CSS code splitting: Vite extracted @nextcloud/vue component styles
   into separate chunks (e.g., NcUserBubble-*.css) that Nextcloud doesn't
   load automatically. Without these styles, the UI rendered incorrectly.

Changes:
- Rename entry point from `main` to `astrolabe-main`
- Add `cssCodeSplit: false` to bundle all CSS into the entry point
- Update assetFileNames to output consistent `astrolabe-main.css`

This increases CSS bundle from 11KB to 286KB but ensures all component
styles are available when the page loads.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 11:37:43 -06:00
Chris CoutinhoandClaude Opus 4.5 48a4182ef9 fix(astrolabe): Fix revoke access button HTTP method mismatch
The "Revoke Access" button in Astrolabe personal settings was failing
with "Unable to connect to server" error in multi-user basic auth mode.

Root cause: The JavaScript sends a POST request but the route was
configured to accept DELETE. Changed the route to:
- Use POST method (matching the JavaScript fetch call)
- Use /api/v1/background-sync/credentials/revoke path (avoiding
  conflict with storeAppPassword which uses POST on the base URL)

Added integration test that verifies the complete revoke flow:
enable background sync → click revoke → verify credentials deleted.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 22:49:53 -06:00
Chris Coutinho 13dd709fc2 bump: version 0.56.1 → 0.56.2 2025-12-29 12:18:18 -06:00
Chris CoutinhoandClaude Opus 4.5 663e66af81 fix(oauth): Enable browser OAuth routes for Management API in hybrid mode
The /oauth/login route was returning 404 in multi-user BasicAuth mode with
offline access enabled. This was because browser OAuth routes were gated
by `oauth_enabled` (only True for MCP OAuth modes), not by
`oauth_provisioning_available` which correctly includes hybrid mode.

The Management API (admin UI, webhook management) requires OAuth
authentication regardless of how MCP tools authenticate. These are
independent security concerns:
- MCP Tools: BasicAuth (waiting for upstream Nextcloud OAuth patches)
- Management API: OAuth (for admin UI, webhook management, vector sync)

Changes:
- Gate browser OAuth routes by oauth_provisioning_available instead of
  oauth_enabled
- Add follow_redirects=True to OIDC discovery HTTP clients

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 12:14:26 -06:00
Chris Coutinho 9c17bbfe9c bump: version 0.56.0 → 0.56.1 2025-12-26 10:33:20 -06:00
Chris Coutinho 056414752e fix(mcp): Move all imports to the top of modules 2025-12-26 10:05:27 -06:00
Chris CoutinhoandClaude Opus 4.5 9a6a253858 fix(tests): Add singleton reset fixture to prevent anyio.WouldBlock errors
Add module-scoped autouse fixture `reset_all_singletons` in
tests/integration/conftest.py that resets all global singletons
between test modules:

- _qdrant_client (vector/qdrant_client.py)
- _embedding_service, _bm25_service (embedding/service.py)
- _provider (providers/registry.py)
- _vector_sync_state with memory streams (app.py)
- _tracer (observability/tracing.py)
- _registry (auth/client_registry.py)
- _token_exchange_service (auth/token_exchange.py)

This fixes anyio.WouldBlock errors that occurred when running the
full integration test suite together. The errors were caused by
stale singleton state holding references to dead event loops or
closed memory streams from previous test modules.

Results:
- Before: 22 passed, 26 errors (WouldBlock), 12 failed
- After: 48 passed, 25 skipped, 1 failed (unrelated timeout)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 09:12:21 -06:00
Chris Coutinho 0a23e484e9 docs(auth): Update docstrings of management api auth handling 2025-12-26 09:05:04 -06:00
Chris CoutinhoandClaude Opus 4.5 779d474aaa fix(tests): Fix integration test failures in qdrant, sampling, and rag tests
- test_qdrant_collection_creation.py:
  - Add get_vector_params() helper to handle named vectors format
  - Collections use {"dense": VectorParams(...)} instead of direct VectorParams
  - Fix otel_service_name setting in test_collection_name_generation

- test_sampling.py:
  - Fix MCP response parsing: use json.loads(result.content[0].text)
    instead of result.structuredContent (which is None)
  - Add require_vector_sync_tools() helper for graceful skipping
  - Add helper call to all 5 test functions

- test_rag.py:
  - Add require_vector_sync_tools() helper for graceful skipping
  - Fix MCP response parsing (same as sampling tests)
  - Prevents 600s timeout when VECTOR_SYNC_ENABLED is not set

Tests now pass/skip cleanly when run independently. The anyio.WouldBlock
errors in full test suite runs are fixture isolation issues, not code bugs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-25 09:59:44 -06:00
Chris CoutinhoandClaude Opus 4.5 894bf5f916 refactor(auth): Decouple BasicAuth and OAuth authentication strategies
Completely separates multi-user BasicAuth mode from OAuth mode with no
fallback between them. These are now mutually exclusive authentication
strategies based on deployment configuration.

Changes:
- Create separate functions: get_user_client_basic_auth() and
  get_user_client_oauth() with clear separation of concerns
- Update get_user_client() to dispatch based on use_basic_auth parameter
- Pass use_basic_auth through all background sync tasks
- Update app.py to determine auth mode at startup
- Rewrite integration tests to verify no OAuth fallback in BasicAuth mode
- Fix test assertions for response field names and duplicate title handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-25 08:27:15 -06:00
Chris CoutinhoandClaude Opus 4.5 804480836e fix(auth): Skip issuer validation for management API tokens
Fixes NC PHP app (Astrolabe) OAuth integration by making token validation
more lenient for management API access.

Problem:
- Astrolabe calls Nextcloud OIDC token endpoint via internal URL (http://localhost)
- Tokens are issued with iss: http://localhost (internal)
- MCP server expects iss: http://localhost:8080 (external)
- Token validation failed with "Invalid issuer"

Solution:
- Add skip_issuer_check parameter to _verify_jwt_signature()
- verify_token_for_management_api() now skips both audience and issuer checks
- Security maintained: signature still verified, authorization checked by API

Also includes related fixes from previous session:
- Update test selectors for Vue 3 UI ("Enable Semantic Search")
- Fix OIDC discovery URL transformation in OAuthController.php
- Add overwrite.cli.url to setup hook for proper external URLs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 17:25:48 -06:00
Chris Coutinho 5e2ef5f35b chore: lint 2025-12-24 09:52:45 -06:00
Chris CoutinhoandClaude Opus 4.5 a51376fd5a fix: Use settings.enable_offline_access for env var consolidation
Migrate all direct ENABLE_OFFLINE_ACCESS environment variable checks to
use settings.enable_offline_access, which handles both the new
ENABLE_BACKGROUND_OPERATIONS and deprecated ENABLE_OFFLINE_ACCESS vars.

Also fixes JWT issuer validation in Docker by using NEXTCLOUD_PUBLIC_ISSUER_URL
when set, resolving 401 errors caused by internal/external URL mismatch.

Changes:
- app.py: Use settings for offline access checks in setup_oauth_config,
  register_oauth_client, and tool registration
- oauth_tools.py: Use settings in provision_nextcloud_access and check_logged_in
- management.py: Use settings in get_user_session
- scope_authorization.py: Use settings in require_scopes decorator
- Remove unused os imports after migration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 09:10:01 -06:00
Chris Coutinho 10a0969138 fix: Add required config.py attributes 2025-12-23 11:57:30 -07:00
Chris Coutinho 5e76ddc60d feat: Remove URL rewriting in favor of proper nextcloud config
Remove URL rewriting logic from MCP server that was converting
      public URLs to internal Docker URLs. This was a workaround for
      Nextcloud's overwritehost setting forcing URLs to localhost:8080.

      Changes:
      - Remove OIDC endpoint rewriting in app.py (setup_oauth_config)
      - Remove OIDC_JWKS_URI override support (no longer needed)
      - Remove URL rewriting in browser_oauth_routes.py
      - Remove URL rewriting in token_broker.py
      - Update Helm chart values and README
      - Add hybrid auth setup unit tests
      - Update Astrolabe admin UI for Vue 3

      The proper fix is in the previous commit which removes the
      overwritehost setting from Nextcloud, allowing it to respect
      the Host header from incoming requests.
2025-12-23 11:34:57 -07:00
Chris CoutinhoandClaude Opus 4.5 9ea1902e2b fix(docker): remove overwritehost to fix container-to-container DCR
Remove the overwritehost and overwrite.cli.url settings that were forcing
Nextcloud to generate URLs with localhost:8080 regardless of the incoming
request's Host header.

This was breaking Dynamic Client Registration (DCR) from the mcp-oauth
container, which needs to reach Nextcloud at http://app:80 but was getting
discovery documents with http://localhost:8080 URLs that are unreachable
from inside the Docker network.

Now Nextcloud respects the Host header:
- Browser requests to localhost:8080 → returns localhost:8080 URLs
- Container requests to app:80 → returns app:80 URLs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 11:28:47 -07:00
Chris CoutinhoandClaude Sonnet 4.5 dd42849d70 feat(helm): migrate to new environment variable naming convention
Replace deprecated environment variables with new consolidated names:
- VECTOR_SYNC_ENABLED → ENABLE_SEMANTIC_SEARCH
- ENABLE_OFFLINE_ACCESS → ENABLE_BACKGROUND_OPERATIONS

Update values.yaml structure:
- Rename 'vectorSync' section to 'semanticSearch'
- Update descriptions to emphasize BM25 hybrid search

Benefits:
- Aligns with application-level config consolidation
- Clearer naming: "semantic search" vs "vector sync"
- Maintains backward compatibility via application deprecation handling
- Automatic enablement of background ops when semantic search enabled in multi-user modes

Updated files:
- values.yaml: Renamed vectorSync → semanticSearch
- deployment.yaml: New env var names with deprecation comments
- NOTES.txt: Updated deployment notes
- README.md: Updated documentation and examples

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-23 09:12:07 -07:00
Chris Coutinho 4248b67b2e feat: Migrate to vue 3 2025-12-23 05:46:49 +01:00
Chris Coutinho d7c99fcc69 feat(astrolabe): upgrade to Vue 3 and @nextcloud/vue 9
- Merge renovate/major-vue-monorepo: Vue 2.7.16 → 3.5.26
- Merge renovate/nextcloud-vue-9.x: @nextcloud/vue 8.29.2 → 9.3.1
- Update component imports to new @nextcloud/vue v9 paths
- Replace .sync modifiers with v-model:prop (Vue 3 syntax)
- Replace beforeDestroy with beforeUnmount lifecycle hook
- Remove Vue.() usage (automatic reactivity in Vue 3)
- Update main.js to use createApp() instead of Vue.extend()
- Add @vitejs/plugin-vue and configure Vite for Vue 3
- All builds passing, ready for admin UX improvements
2025-12-23 00:47:21 +01:00
Chris Coutinho 47095fabcd Merge remote-tracking branch 'origin/renovate/nextcloud-vue-9.x' into feat/improve-admin-ux-vue3
# Conflicts:
#	third_party/astrolabe/package-lock.json
2025-12-23 00:38:50 +01:00
Chris Coutinho 85b7b935b3 Merge remote-tracking branch 'origin/renovate/major-vue-monorepo' into feat/improve-admin-ux-vue3 2025-12-23 00:36:51 +01:00
Chris CoutinhoandClaude Sonnet 4.5 8ba3ae73ab fix(helm): set OIDC client env vars when using existingSecret
The deployment template only checked for clientId being set in
values.yaml, so when using existingSecret without setting clientId,
the NEXTCLOUD_OIDC_CLIENT_ID and NEXTCLOUD_OIDC_CLIENT_SECRET env
vars were never created.

This broke existingSecret for OIDC-based auth - the server would
always fall back to DCR even when pre-registered credentials were
provided via secret.

Fix: Check for EITHER clientId OR existingSecret being set before
creating the OIDC client credential env vars.

Affects both OIDC-based auth modes:
- auth.oauth.existingSecret (OAuth mode)
- auth.multiUserBasic.existingSecret (multi-user BasicAuth with offline access)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-22 22:21:23 +01:00
Chris CoutinhoandClaude Sonnet 4.5 5b9e76ddb4 fix(helm): trigger chart release workflow on helm chart tags
The helm-release workflow was only triggering on v* tags (MCP server
releases), not on nextcloud-mcp-server-* tags (helm chart releases).

This caused chart releases to be skipped because:
1. Helm chart version bump creates tag nextcloud-mcp-server-X.Y.Z
2. Workflow never runs for this tag (pattern didn't match)
3. Next v* tag triggers workflow at wrong commit (Chart.yaml not updated)
4. chart-releaser skips because version already exists

Fix: Add nextcloud-mcp-server-* to workflow trigger pattern so chart
releases execute at the correct commit where Chart.yaml has the new version.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-22 21:48:26 +01:00
Chris Coutinho ea96a58678 fix(helm): address PR #447 reviewer feedback
Critical fix:
- deployment.yaml: Only reference OAuth credentials when clientId is set
- Fixes pod failure when using existingSecret without static OAuth creds
- Aligns deployment behavior with secret template logic

Previously, the deployment referenced OAuth credentials when either
clientId OR existingSecret was set. However, the secret template only
includes OAuth credentials when clientId is explicitly provided. This
caused pod failures when users provided an existingSecret for offline
access without static OAuth credentials (intending to use DCR).

The fix ensures OAuth env vars are only referenced when clientId is set,
matching the OAuth mode pattern and allowing DCR to work correctly with
existingSecret configurations.

Minor improvements:
- values.yaml: Clarify OAuth credentials are optional (uses DCR if not provided)

Testing verified all scenarios:
 Pass-through only (no offline access): No secrets/PVCs/OAuth vars
 Offline + DCR (no clientId): Secret with encryption key only, no OAuth vars
 Offline + static OAuth: Secret with all keys, OAuth vars present
 existingSecret without clientId: No auto secret, no OAuth vars (FIXED)

Resolves reviewer feedback from PR #447
2025-12-22 21:34:40 +01:00
Chris Coutinho 9b5c6779e9 fix(helm): include MCP server version bumps in changelog pattern
Updates helm chart commitizen config to recognize MCP server version
bump commits (which update appVersion in Chart.yaml) as valid triggers
for helm chart version bumps.

Problem:
- When MCP server version bumps, it updates Chart.yaml appVersion
- Helm chart commitizen only matched "(helm)" scoped commits
- Result: appVersion updated but chart version not bumped

Solution:
- Extended changelog_pattern to include "bump: version X → Y" commits
- Now helm chart version will bump when either:
  1. Commits with (helm) scope are made, OR
  2. MCP server version bumps (updating appVersion)

This ensures chart version stays in sync with appVersion updates.
2025-12-22 21:17:14 +01:00
Chris Coutinho 04140d671e feat(helm): add support for multi-user BasicAuth mode
Adds multi-user-basic authentication mode to the helm chart alongside
existing basic (single-user) and oauth modes.

Multi-user BasicAuth mode enables:
- Pass-through authentication (credentials in request headers)
- Optional background operations using app passwords via Astrolabe
- Optional OAuth client credentials (uses DCR if not provided)
- Token encryption and persistent storage for background sync

Changes:
- values.yaml: Add auth.multiUserBasic configuration section
- deployment.yaml: Add ENABLE_MULTI_USER_BASIC_AUTH and related env vars
- secret.yaml: Add secret template for token encryption key and OAuth credentials
- pvc.yaml: Add PVC template for token database persistence
- _helpers.tpl: Add helper functions for secret/PVC names

Tested with:
  helm template --set auth.mode=multi-user-basic \
    --set auth.multiUserBasic.enableOfflineAccess=true \
    --set auth.multiUserBasic.tokenEncryptionKey=... \
    --set vectorSync.enabled=true

Related: Multi-user deployment support (ADR-020)
2025-12-22 21:03:10 +01:00
Chris CoutinhoandClaude Sonnet 4.5 4a5766b84e feat(config): enable DCR for multi-user BasicAuth with offline access
Allows multi-user BasicAuth mode to use Dynamic Client Registration (DCR)
for OAuth credentials when ENABLE_OFFLINE_ACCESS is enabled, making it
consistent with OAuth modes and reducing configuration burden.

**Changes:**

Configuration Validation:
- Relaxed OAuth credential requirements for multi-user BasicAuth
- OAuth credentials now optional when offline access enabled
- Will use DCR as fallback if NEXTCLOUD_OIDC_CLIENT_ID/SECRET not set
- Updated validation to log info instead of error when DCR will be used

Startup Logic (app.py):
- Added DCR workflow for multi-user BasicAuth before uvicorn starts
- Creates oauth_context for management APIs when offline access enabled
- Allows Astrolabe to authenticate management API calls with OAuth
- DCR runs synchronously at same lifecycle point as OAuth modes
- Added traceback import for better error logging
- Fixed type assertions for nextcloud_host
- Fixed undefined variable references in vector sync logging

Management API:
- Improved auth mode detection using proper detect_auth_mode()
- Added auth_mode field to /status endpoint:
  * "basic" - Single-user BasicAuth
  * "multi_user_basic" - Multi-user BasicAuth
  * "oauth" - OAuth modes
  * "smithery" - Smithery stateless
- Added supports_app_passwords indicator for multi-user BasicAuth

Docker Compose:
- Updated mcp-multi-user-basic service configuration:
  * Enabled vector sync (VECTOR_SYNC_ENABLED=true)
  * Added ENABLE_OFFLINE_ACCESS=true for app password support
  * Added NEXTCLOUD_MCP_SERVER_URL for Astrolabe integration
  * Documented optional static OAuth credentials

Testing:
- Updated test_config_validators.py to expect DCR fallback
- Enhanced configure_astrolabe_for_mcp_server fixture with verification
- Added debug logging to test_users_setup fixture

**Workflow:**
1. User configures ENABLE_OFFLINE_ACCESS=true
2. Server checks for static NEXTCLOUD_OIDC_CLIENT_ID/SECRET
3. If not found, performs DCR before uvicorn starts
4. DCR registers client with Nextcloud OIDC provider
5. OAuth credentials used for Astrolabe management API auth
6. Background sync can retrieve user app passwords via Astrolabe

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-22 19:43:24 +01:00
Chris CoutinhoandClaude Sonnet 4.5 65c3f099fa feat(astrolabe): implement app password provisioning for multi-user background sync
Adds complete app password provisioning workflow for multi-user BasicAuth
deployments, allowing users to independently enable background sync by
generating and storing Nextcloud app passwords.

**New Components:**

Backend (PHP):
- CredentialsController: Validates and stores app passwords
  * Validates app password format and authenticity via OCS API
  * Stores encrypted passwords in oc_preferences
  * Provides status and credential management endpoints
- AstrolabeAdminSettings: Admin configuration page for MCP server URL
- AstrolabeAdminSettingsListener: Event listener for admin section
- Updated McpTokenStorage: Added background sync credential methods

Frontend:
- personalSettings.js: Form handling for app password entry
  * AJAX submission with error handling
  * Shows success/error notifications
  * Triggers page reload after successful save
- settings.css: Styling for settings pages
- Updated personal.php template: Two-option UI
  * Option 1: OAuth refresh token (future, not yet available)
  * Option 2: App password (works today, recommended)
  * Shows "Active" badge when provisioned
  * Displays credential type and provisioned timestamp

Routes:
- POST /api/v1/background-sync/credentials - Store app password
- GET /api/v1/background-sync/status - Get provisioning status
- DELETE /api/v1/background-sync/credentials - Revoke credentials
- GET /api/v1/background-sync/credentials/{userId} - Admin only

**Testing:**
- test_astrolabe_settings_buttons.py: Integration test for UI buttons

**Workflow:**
1. User generates app password in Nextcloud Security settings
2. User navigates to Astrolabe personal settings
3. User enters app password in "Option 2: App Password" form
4. Backend validates password via OCS API call
5. Password stored encrypted in oc_preferences
6. Page reloads showing "Active" badge with credential details
7. MCP server can now use stored password for background operations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-22 19:39:13 +01:00
Chris CoutinhoandClaude Sonnet 4.5 b293258210 test(astrolabe): fix app password extraction in multi-user background sync test
Fixes the Playwright-based integration test that verifies multi-user app
password provisioning for background sync in Astrolabe.

**Root Cause:**
The test was failing to extract the generated app password from Nextcloud's
"New app password" dialog due to overly specific CSS selectors that didn't
match the actual DOM structure.

**Changes:**
- Enhanced network response logging to capture HTTP status codes
- Simplified app password extraction logic:
  * Wait for dialog heading using text selector
  * Iterate through ALL text inputs on page
  * Find password by pattern: contains dashes and length > 20
  * Validate extracted password against expected format
- Added format validation with regex before returning password
- Added detailed debug logging for each extraction step
- Improved error messages with screenshot paths

**Testing:**
Test now successfully completes for both alice and bob test users:
- Logs in to Nextcloud
- Generates app password in Security settings
- Extracts password from dialog
- Navigates to Astrolabe settings
- Enters and saves app password
- Verifies "Active" badge appears
- Confirms credentials stored in database

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-22 19:32:06 +01:00
Chris CoutinhoandClaude Sonnet 4.5 1a5bb10cd0 feat(config): consolidate configuration with smart dependency resolution (ADR-021)
Simplifies configuration by consolidating overlapping settings and adding
automatic dependency resolution. This makes semantic search configuration
significantly easier for users while maintaining 100% backward compatibility.

## Key Changes

### Variable Renaming (Backward Compatible)
- `VECTOR_SYNC_ENABLED` → `ENABLE_SEMANTIC_SEARCH` (old name still works)
- `ENABLE_OFFLINE_ACCESS` → `ENABLE_BACKGROUND_OPERATIONS` (old name still works)
- Deprecation warnings logged when old names used
- Old names will be removed in v1.0.0

### Smart Dependency Resolution
- `ENABLE_SEMANTIC_SEARCH` automatically enables background operations in multi-user modes
- No need to set both `ENABLE_OFFLINE_ACCESS` and `VECTOR_SYNC_ENABLED` anymore
- Single-user mode doesn't auto-enable background ops (not needed)

### Explicit Mode Selection (Optional)
- New `MCP_DEPLOYMENT_MODE` environment variable
- Valid values: single_user_basic, multi_user_basic, oauth_single_audience,
  oauth_token_exchange, smithery
- Removes ambiguity about which deployment mode is active
- Falls back to auto-detection if not set (existing behavior)

### Configuration Templates
- Reorganized `env.sample` by deployment mode with clear sections
- Added mode-specific quick-start templates:
  - `env.sample.single-user` - Simplest configuration
  - `env.sample.oauth-multi-user` - Recommended multi-user
  - `env.sample.oauth-advanced` - Token exchange mode

## Implementation Details

### Files Modified
- `nextcloud_mcp_server/config.py` - Smart dependency resolution helpers
- `nextcloud_mcp_server/config_validators.py` - Simplified validation, explicit mode
- `tests/unit/test_config_validators.py` - 19 new tests (60 total, all passing)
- `env.sample` - Reorganized by deployment mode
- `docs/configuration.md` - Complete rewrite with consolidated approach
- `docs/troubleshooting.md` - New consolidation troubleshooting section
- `README.md` - Updated variable references

### New Files
- `docs/ADR-021-configuration-consolidation.md` - Architecture decision record
- `docs/configuration-migration-v2.md` - Comprehensive migration guide
- `env.sample.single-user` - Single-user quick-start template
- `env.sample.oauth-multi-user` - OAuth multi-user quick-start template
- `env.sample.oauth-advanced` - Token exchange quick-start template

## User Impact

### Before (Confusing)
```bash
ENABLE_OFFLINE_ACCESS=true      # Why both?
VECTOR_SYNC_ENABLED=true        # What's the relationship?
```

### After (Simplified)
```bash
MCP_DEPLOYMENT_MODE=oauth_single_audience  # Explicit (optional)
ENABLE_SEMANTIC_SEARCH=true                # Auto-enables background ops!
```

### Benefits
- 📉 2 fewer variables to understand for semantic search
- 📋 Clear intent ("I want semantic search")
- 🎯 Explicit mode declaration available
- 🔄 100% backward compatible
-  All 265 unit tests passing

## Testing
- All 60 config validation tests passing
- 10 new tests for configuration consolidation
- 9 new tests for explicit mode selection
- Full unit test suite: 265 tests passing
- Backward compatibility verified

## Migration
Users can migrate at their own pace. Old variable names continue working
with deprecation warnings. See docs/configuration-migration-v2.md for
detailed migration instructions.

Related: ADR-021

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-21 20:36:36 +01:00
Chris Coutinho 981f102b27 fix(config): address reviewer feedback
- Restore CI test filter (-m unit -m smoke) for faster CI runs
- Replace local path reference with ADR-020 reference in config_validators.py
- Add comprehensive BasicAuthMiddleware unit tests (10 tests covering all edge cases)

Addresses critical CI issue and improves test coverage for multi-user BasicAuth mode.
2025-12-20 21:16:17 +01:00
Chris CoutinhoandClaude Sonnet 4.5 94febf1602 ci(test): build Astrolabe app before running tests
Add build step for Astrolabe app in CI workflow to compile frontend
assets before docker-compose starts.

Changes:
- Install Node.js 20 for Astrolabe build
- Run composer install --no-dev for Astrolabe PHP dependencies
- Run npm ci and npm run build to compile frontend assets

This ensures the Astrolabe app is properly built in CI, similar to
the existing OIDC app build process.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 21:11:28 +01:00
Chris CoutinhoandClaude Sonnet 4.5 286a3eb20f feat(auth): add multi-user BasicAuth pass-through mode
Implement multi-user BasicAuth pass-through mode (ADR-020) where each
request includes BasicAuth credentials that are forwarded to Nextcloud
APIs without persistent storage.

Changes:
- Add _get_client_from_basic_auth() in context.py to extract credentials
  from Authorization header (set by BasicAuthMiddleware)
- Add AstrolabeClient for app password provisioning via Astrolabe API
- Update oauth_sync.py with dual credential support (app passwords first,
  then refresh tokens as fallback)
- Simplify oauth_tools.py provisioning logic
- Add integration tests for app password provisioning and multi-user BasicAuth

Features:
- Stateless multi-user mode: credentials passed per-request
- Optional background sync via app passwords (stored in Astrolabe)
- Falls back to refresh tokens if app password not available
- Test coverage for provisioning flow and pass-through mode

Related: ADR-019 (Multi-user BasicAuth), ADR-020 (Deployment Modes)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 20:55:31 +01:00
Chris Coutinho 19b209f412 test: Enable all tests 2025-12-20 20:53:02 +01:00
Chris CoutinhoandClaude Sonnet 4.5 cd7ba5685a feat(astrolabe): add dynamic MCP server configuration for testing
Replace static post-installation configuration with dynamic test-time
configuration to support testing multiple MCP server deployments.

Changes:
- Remove static MCP server URL and OAuth client setup from post-installation
- Add configure_astrolabe_for_mcp_server fixture (session-scoped)
- Fixture dynamically configures:
  * Nextcloud system config (mcp_server_url, mcp_server_public_url)
  * OAuth client creation via occ oidc:create
  * Client credential storage (astrolabe_client_id, astrolabe_client_secret)
- Update existing OAuth tests to use dynamic configuration
- Add test_astrolabe_multi_server_integration.py with parametrized tests

Benefits:
- Test Astrolabe with mcp-oauth, mcp-keycloak, mcp-multi-user-basic
- Each test configures for its specific MCP server
- No static configuration conflicts between deployments
- Cleaner post-installation (37 lines, down from 85)

Test Results:
- test_astrolabe_configuration_for_different_servers: PASSED (mcp-oauth, mcp-keycloak)
- test_astrolabe_reconfiguration: PASSED

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 20:49:53 +01:00
Chris CoutinhoandClaude Sonnet 4.5 4507359760 refactor(config): centralize configuration validation and simplify startup
Implement centralized configuration validation (ADR-020) to simplify
deployment mode detection and improve error messages.

Changes:
- Create ADR-020 documenting 5 deployment modes with required/optional config
- Add config_validators.py with validate_configuration() and mode detection
- Simplify app.py startup with single validation point at get_app()
- Remove duplicate is_oauth_mode() function (43 lines)
- Fix DeploymentMode mapping (only SELF_HOSTED and SMITHERY_STATELESS exist)
- Add comprehensive unit tests (41 tests covering all modes and edge cases)
- Add enable_multi_user_basic_auth to Settings and BasicAuthMiddleware

Docker Compose:
- Remove conflicting ENABLE_MULTI_USER_BASIC_AUTH from mcp-oauth service
- Add dedicated mcp-multi-user-basic service on port 8003

Test Results:
- 237/237 integration tests PASSED
- All deployment modes verified: single-user BasicAuth, multi-user BasicAuth,
  OAuth single-audience, OAuth token exchange (Keycloak), Smithery stateless

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 20:49:28 +01:00
Chris CoutinhoandClaude Sonnet 4.5 8682fa4f88 ci: Handle NO_COMMITS_TO_BUMP gracefully in bump scripts
When commitizen finds no eligible commits to bump, it exits with
code 1 and outputs [NO_COMMITS_TO_BUMP]. This was causing the
GitHub Actions workflow to fail even though this is an expected
scenario.

Updated all three bump scripts (bump-mcp.sh, bump-helm.sh,
bump-astrolabe.sh) to:
- Detect the [NO_COMMITS_TO_BUMP] message
- Exit with code 0 (success) instead of code 1
- Output an informational message instead of an error

This allows the bump-version workflow to complete successfully
when no version bumps are needed, matching the workflow's existing
logic that handles empty BUMPED_COMPONENTS.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 13:17:50 +01:00
Chris Coutinho 53b84200d4 ci: Gracefully exit on no commit bump 2025-12-20 13:11:11 +01:00
Chris Coutinho 18298177f7 fix(astrolabe): screenshots in info.xml 2025-12-20 01:04:20 +01:00
Chris Coutinho 651b73545d fix(astrolabe): screenshots in info.xml 2025-12-20 00:58:40 +01:00
Chris Coutinho d4d1a332fb fix(astrolabe): Update screenshots 2025-12-20 00:52:21 +01:00