Commit Graph
100 Commits
Author SHA1 Message Date
Chris Coutinho 148573e28b Merge remote-tracking branch 'origin/master' into feat/docker-compose-profiles-login-flow 2026-03-01 17:26:05 +01:00
Chris CoutinhoandClaude Opus 4.6 db1e0606ad fix: address PR #589 review feedback (round 2)
Consolidate three independent RefreshTokenStorage lazy singletons into a
single lock-protected get_shared_storage() function, eliminating race
conditions on concurrent first-access. Remove blanket try/except in
_get_stored_scopes so storage errors propagate as proper MCP errors
instead of silently triggering "please provision" messages. Handle
declined/cancelled elicitation results in Login Flow tools by cleaning up
sessions and returning clear status. Add update_app_password_scopes() to
avoid unnecessary decrypt/re-encrypt when only scopes change. Add
unprovisioned-user early exit and no-op detection to nc_auth_update_scopes.
Remove four dead config fields and misleading NEXTCLOUD_PASSWORD deprecation
warning. Add periodic login flow session cleanup task. Generate separate
Fernet keys per service. Add board cleanup in deck integration test. Gate
CI unit tests on linting and skip Astrolabe build for single-user profile.
Fix test markers from oauth to multi_user_basic for astrolabe integration
tests. Update login_flow.py docstrings to document outbound HTTP calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 16:35:31 +01:00
Chris CoutinhoandClaude Opus 4.6 33cf0fee9b fix(ci): remove dev OIDC mount to fix HTTP 500 in single-user/multi-user-basic
The dev OIDC app was mounted for all profiles but composer install only
ran for Playwright profiles, causing a missing vendor/autoload.php fatal
error that broke all /apps/* and /ocs/* routes. The hook script already
has app store fallback logic, so removing the dev mount is sufficient.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:28:01 +01:00
Chris CoutinhoandClaude Opus 4.6 b2fd4da9fe fix(ci): fix health check timeout and per-profile MCP server URL routing
The Nextcloud health check expected HTTP 401 from serverinfo, but NC 32
returns 200 — causing 5-minute timeouts. Switch to /status.php with
"installed":true check (matches Docker healthcheck). Also route the
correct MCP_SERVER_URL per CI matrix profile into the app container so
Astrolabe connects to the right service, and make the init script
gracefully skip when the var is unset.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:30:07 +01:00
Chris CoutinhoandClaude Opus 4.6 16cd2e27cb fix(ci): fix PHP gating, add multi-user-basic matrix entry, upload debug artifacts
PHP setup was gated behind needs-playwright but Astrolabe build needs
composer unconditionally. Add multi-user-basic CI matrix entry with
proper marker filtering. Upload Playwright screenshots and service logs
as artifacts on failure for easier debugging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 11:02:39 +01:00
Chris CoutinhoandClaude Opus 4.6 e28af5453b fix: address PR #589 review feedback for Login Flow v2
- Fix data loss in nc_auth_update_scopes: remove premature
  delete_app_password call; old password stays valid until upsert
  replaces it on successful re-provisioning
- Replace assert with proper error return in nc_auth_check_status
- Add lazy singleton for RefreshTokenStorage in auth_tools,
  scope_authorization, and context to avoid per-call re-initialization
- Centralize _is_login_flow_mode() to get_settings().enable_login_flow
  and remove duplicate definitions and per-call os.getenv reads
- Add dev-only comment to TOKEN_ENCRYPTION_KEY in docker-compose.yml
- Gate OIDC build steps in CI behind matrix.needs-playwright
- Add diagnostic step reporting Playwright skip count in CI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:08:55 +01:00
Chris CoutinhoandClaude Opus 4.6 87ec3c4f5b chore: update third_party submodule pointers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 09:17:29 +01:00
Chris CoutinhoandClaude Opus 4.6 989749530c fix(ci): fix integration test collection and skip Playwright in CI
- Add @pytest.mark.oauth to OAuth-dependent tests in
  test_scope_authorization.py so they're excluded from single-user job
- Add module-level pytestmark to test_introspection_authorization.py
- Fix single-user marker expression to also exclude oauth smoke tests
- Add --ignore paths for multi-user, qdrant, and RAG evaluation tests
- Uncomment GITHUB_ACTIONS skip in oauth_callback_server fixture
- Add GITHUB_ACTIONS skip to login_flow_oauth_token fixture
- Mount third_party/oidc volume in docker-compose.yml app service
- Add OIDC diagnostic step in CI for playwright jobs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 09:17:03 +01:00
Chris CoutinhoandClaude Opus 4.6 2d46959d01 fix(test): fix 17 pre-existing unit test failures and add astrolabe CI build
Unit test fixes:
- test_userinfo_routes: patch nextcloud_httpx_client instead of httpx.AsyncClient
- test_instrument_tool: patch trace_operation in metrics module (where imported)
- test_management_app_password_endpoints: patch nextcloud_httpx_client and
  get_settings at correct import locations
- test_management_status_endpoint: patch detect_auth_mode and get_settings at
  correct import locations (api.management, not config/config_validators)
- test_token_exchange: fix TokenBrokerService constructor args (client_id/
  client_secret instead of encryption_key)

CI:
- Add Node.js setup and astrolabe build step (composer + npm ci + npm run build)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 21:53:06 +01:00
Chris CoutinhoandClaude Opus 4.6 59fdcd123a fix(ci): keep third_party mount, always build submodules in CI
The third_party volume mount is required for astrolabe/notes/oidc
development. Always checkout submodules and build the OIDC app in
all CI matrix jobs since the app container needs it.

Remove the docker-compose.oidc.yml override (no longer needed).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:51:36 +01:00
Chris CoutinhoandClaude Opus 4.6 b79c54cc6a fix(ci): revert accidental third_party mount, use compose override for OIDC
The third_party:/opt/apps volume was accidentally uncommented in
docker-compose.yml. Without submodules checked out, this empty mount
breaks the Notes app installation hook in CI.

Fix: keep the mount commented in docker-compose.yml and add a separate
docker-compose.oidc.yml override that's only used for OIDC-requiring
profiles (oauth, login-flow) in CI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:48:07 +01:00
Chris CoutinhoandClaude Opus 4.6 fe3fbe95a1 fix(ci): don't block integration matrix on unit-test failures
Unit tests have pre-existing failures unrelated to deployment mode
testing. Run integration matrix after linting only so the matrix
can expand and test each profile independently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:39:45 +01:00
Chris CoutinhoandClaude Opus 4.6 8fe7d81e57 ci: use matrix strategy for deployment mode integration tests
Replace the single integration-test job with a matrix that tests each
deployment mode independently using Docker Compose profiles:

- single-user: smoke + integration tests (port 8000)
- oauth: OAuth flow tests with Playwright (port 8001)
- login-flow: Login Flow v2 tests with Playwright (port 8004)

Unit tests run separately without Docker. OIDC app build and Playwright
install are conditional based on the mode. Service logs are captured on
failure for debugging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:35:47 +01:00
Chris CoutinhoandClaude Opus 4.6 8b5c2395b5 feat: add Docker Compose profiles and Login Flow v2 service
Add selective service startup via Docker Compose profiles so each MCP
deployment mode runs independently. Also add the new mcp-login-flow
service (port 8004) for Login Flow v2 authentication (ADR-022).

Profile assignments:
- single-user: mcp (port 8000)
- multi-user-basic: mcp-multi-user-basic (port 8003)
- oauth: mcp-oauth (port 8001)
- keycloak: keycloak + mcp-keycloak (port 8002)
- login-flow: mcp-login-flow (port 8004)

Infrastructure services (db, redis, app, recipes) always start.

Integration tests cover the full Login Flow v2 provisioning flow:
OAuth → browser login → app password → Nextcloud API access for
notes, calendar, contacts, files, deck, and cookbook operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:33:54 +01:00
Chris CoutinhoandClaude Opus 4.6 18e5baf2a5 fix: address PR #574 fourth review round
- Use lowercase generics (list[...]) in new deck response models
- Add clarifying comment on AddressBook.uri slug semantics
- Fall back calendar_display_name to calendar_name when absent

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 09:36:14 +01:00
Chris CoutinhoandClaude Opus 4.6 f51b27ba19 fix: address PR #574 third review round
- Guard board.labels against None in deck_get_labels and resource
- Add TODO comments for calendar_display_name in single-calendar paths
- Document _raw_contact_to_model scope limitation (maps only what the
  client returns; expanding requires changes to vCard parsing)
- Log debug warning when event has no start_datetime
- Verified Table model is safe with extra fields (Pydantic v2 ignores)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 13:49:55 +01:00
Chris CoutinhoandClaude Opus 4.6 76e6c12b56 fix: address PR #574 second review round
- Enrich single-calendar event dicts with calendar_name before mapping
  to CalendarEventSummary (list_events and upcoming_events paths)
- Extract _raw_contact_to_model() from inline mapping in contacts.py,
  fix custom_fields type annotation to dict[str, Any]
- Add unit tests for _event_dict_to_summary covering categories parsing,
  falsy coercion, and calendar name passthrough
- Replace duplicated test helper with import of production function

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 10:57:09 +01:00
Chris CoutinhoandClaude Opus 4.6 76e305006c fix: address PR #574 review comments
Restore contact email/birthday/nickname data and per-event calendar
source that were silently dropped during response model wrapping.
Remove dead elif branches in OAuth deck tests, add regression tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 09:39:46 +01:00
Chris CoutinhoandClaude Opus 4.6 8887aa241a fix: wrap raw list returns in response models to produce single TextContent block
MCP tools returning raw lists caused FastMCP's _convert_to_content() to create
one TextContent block per element. Most MCP clients only read content[0], so
they saw a single result instead of the full list.

Wrapped 9 tool functions in proper response objects:
- deck: deck_get_boards, deck_get_stacks, deck_get_cards, deck_get_labels
- calendar: nc_calendar_list_events, nc_calendar_get_upcoming_events
- contacts: nc_contacts_list_addressbooks, nc_contacts_list_contacts
- tables: nc_tables_list_tables

Closes #568

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 09:22:16 +01:00
Chris CoutinhoandClaude Opus 4.6 a11ae9c027 refactor: enforce PLC0415 (import-outside-top-level) for source code
Enable ruff PLC0415 rule for all source files (tests excluded via
per-file-ignores). Move 136 inline imports to top-level across 33 files.
8 imports suppressed with noqa for legitimate reasons: circular
dependencies (client/__init__.py, context.py), optional dependency
guards (app.py document processors, auth/userinfo_routes.py), and
post-env-setup imports (smithery_main.py).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 08:04:50 +01:00
Chris CoutinhoandClaude Opus 4.6 81efa6e263 fix: address PR #571 review comments
- Move httpx import to top-level and use anyio task group for concurrent
  validation in cleanup_invalid_app_passwords (storage.py)
- Respect Retry-After header for 429 responses, capped at 300s (oauth_sync.py)
- Soften pre-validation exceptions so transient failures don't crash the
  background sync task (oauth_sync.py)
- Replace f-string SQL with blanket DELETE and add returncode checks (conftest.py)
- Extract clear_stale_test_state() helper to deduplicate cleanup logic
  in astrolabe background sync tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 08:03:55 +01:00
Chris CoutinhoandClaude Opus 4.6 3779ec3e17 fix: resolve stale credentials causing astrolabe background sync test failures
The revoke test failed because it only completed Step 2 (app password) but
not Step 1 (OAuth authorization). In hybrid mode, Astrolabe requires both
steps for $isFullyConfigured=true, which gates the "Revoke Access" button.

Changes:
- Use complete_astrolabe_authorization() in revoke test for full two-step flow
- Add stale state cleanup (app passwords, bruteforce entries, Astrolabe prefs)
  to both enablement and revoke tests
- Add startup cleanup of invalid app passwords in BasicAuth mode
- Pre-validate credentials before entering scanner loop to fail fast
- Handle 401/403/429 in scanner with proper backoff and circuit breaking
- Clean up app passwords in test_users_setup fixture teardown

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:55:58 +01:00
Chris CoutinhoandClaude Opus 4.6 0d259d2dfd docs(ADR-022): concrete Smithery rationale + app password lifecycle
Address reviewer feedback on two fronts:

- Replace vague privacy-only Smithery deprecation rationale with concrete
  justification: free tier sunsetting March 2026 (primary), privacy as
  secondary. Updated in context, migration table, and Alternative 5.

- Add App Password Lifecycle Management section covering stale/revoked
  password detection (401 handling), login flow session cleanup (background
  task), and optional password rotation (APP_PASSWORD_MAX_AGE_DAYS).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:19:13 +01:00
Chris CoutinhoandClaude Opus 4.6 037e88e416 ci: pin GitHub Actions version comments to full semver
Renovate's helpers:pinGitHubActionDigestsToSemver preset reads version
comments to track updates. Major-only comments (e.g. # v6) produce
unhelpful changelog diffs like "v6 → v6". Full semver comments
(e.g. # v6.0.2) let Renovate show meaningful version changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 09:50:12 +01:00
Chris CoutinhoandClaude Opus 4.5 dae2f276ae docs(ADR-022): address reviewer feedback
Changes based on review:

1. Add Nextcloud platform limitation section documenting OAuth/scope
   support by endpoint type (WebDAV supports OAuth, others don't)

2. Update MCP elicitation to show capability negotiation and graceful
   fallback - URL in error message when elicitation not supported

3. Simplify Smithery section - recommend self-hosted for privacy,
   don't detail platform changes

4. Expand re-auth section with scope merging behavior, scenarios table,
   and explicit design choice for tool-based re-auth over auto-elicitation

5. Make rate limiting configurable with environment variables and
   admin guidance by deployment size

6. Clarify OAuth alternative - keep simple now, revisit if Nextcloud
   adds scoped OAuth support

7. Expand verification steps with required tests, add recommended
   Nextcloud configuration, add required README security notice

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-18 09:23:32 +01:00
Chris CoutinhoandClaude Opus 4.5 d94610d0ec docs: add ADR-022 for deployment mode consolidation via Login Flow v2
Proposes consolidating five deployment modes into two:
- Single-User: App password in env vars (trusted environment)
- Multi-User: Login Flow v2 for per-user app password acquisition

Key changes:
- Use Nextcloud Login Flow v2 (NC 16+) for delegated authentication
- Application-level scope enforcement (app passwords have no native scopes)
- MCP elicitation for seamless authorization prompting
- Astrolabe front-end integration for scope management UI
- Clear security posture documentation for administrators

This removes the need for upstream Nextcloud OAuth patches and simplifies
deployment while maintaining security through defense-in-depth.

Related: #521

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-18 09:23:32 +01:00
Chris CoutinhoandClaude Opus 4.6 66ccacdee1 fix: add type: ignore for caldav ssl_verify_cert parameter
caldav types declare ssl_verify_cert as Union[bool, str] but the value
is passed through to httpx which accepts ssl.SSLContext. Add type: ignore
annotation to satisfy the ty type checker in CI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 13:34:05 +01:00
Chris CoutinhoandClaude Opus 4.6 1a4486a388 fix: convert CA bundle path to ssl.SSLContext to avoid httpx deprecation warning
httpx emits a DeprecationWarning when verify=<str> is passed, recommending
ssl.SSLContext instead. This affected both our httpx client factories and
the caldav library passthrough.

Changed get_nextcloud_ssl_verify() to return bool | ssl.SSLContext instead
of bool | str by constructing an SSLContext when NEXTCLOUD_CA_BUNDLE is set.
All downstream consumers (httpx, caldav) natively accept ssl.SSLContext.

Also fixed app password endpoint tests that used overly broad MagicMock
(auto-generated truthy nextcloud_ca_bundle attribute).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 13:27:22 +01:00
Chris CoutinhoandClaude Opus 4.6 1707b2e6e1 feat: add self-signed SSL certificate support for Nextcloud connections
Add NEXTCLOUD_VERIFY_SSL and NEXTCLOUD_CA_BUNDLE env vars to configure
TLS certificate verification for all outbound Nextcloud connections.
Centralizes SSL config via a new HTTP client factory (http.py) used by
all 27 Nextcloud-bound call sites, including API clients, OIDC endpoints,
OAuth flows, and health checks.

Closes #560

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 09:21:21 +01:00
Chris CoutinhoandClaude Opus 4.6 3be229a487 refactor: remove stale astrolabe references from commitizen config
Remove astrolabe tag filtering and commit scope exclusions from
pyproject.toml now that Astrolabe lives in its own repository.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 00:17:38 +01:00
Chris CoutinhoandClaude Opus 4.6 49c5439686 docs: annotate astrolabe integration tests as cross-system interface tests
Add cross-system interface test annotations to the 5 astrolabe test files,
clarifying they test the MCP server's integration with the Astrolabe
Nextcloud app (installed from the app store, source now in a separate repo).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 15:54:08 +01:00
Chris CoutinhoandClaude Opus 4.6 08d37a6597 docs: clean up astrolabe references after extraction
Remove astrolabe-specific docs and sections that belong in the
astrolabe repo. Update remaining references to point to the
astrolabe repo where appropriate.

- Fix .gitmodules SSH → HTTPS URL for astrolabe submodule
- Remove bump-version.yml stale "astrolabe" scope comment
- Delete blog-introducing-astrolabe.md (moved to astrolabe repo)
- Remove "Astrolabe Background Token Refresh" section from auth-flows.md
- Replace "Astrolabe User Setup" section in authentication.md with link
- Remove "Astrolabe Internal URL" section from configuration.md
- Remove "Webhook Presets (via Astrolabe UI)" from webhook guide

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:16:50 +01:00
Chris CoutinhoandClaude Opus 4.6 dfc75a8619 refactor: extract Astrolabe to separate repository
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>
2026-02-11 10:10:29 +01:00
Chris CoutinhoandClaude Opus 4.6 ec8eab99f3 fix: strip whitespace from category names when splitting
Trim whitespace from comma-separated category values in all three
methods: _create_ical_event, _merge_ical_properties, and
_merge_ical_todo_properties. Prevents leading/trailing spaces in
category names from inputs like "work, meeting".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 23:46:28 +01:00
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