Adds an OAuth 2.1 public client registration flow so MCP clients can
self-register via a user-facing consent page instead of requiring manual
setup in Settings.
Server:
- DB migration adds `is_public` and `created_via` columns to oauth_clients
- New GET /api/oauth/register/validate — validates DCR params, returns
requested scopes; unauthenticated callers get loginRequired flag
- New POST /api/oauth/register — creates a public client, saves consent,
and redirects with client_id (cookie auth required)
- `authenticateClient` / `refreshTokens` skip secret check for public
clients (PKCE provides the security guarantee)
- `createOAuthClient` accepts options for isPublic/createdVia; public
clients store an opaque secret hash instead of a usable secret
- `rotateOAuthClientSecret` blocked on public clients
- `isValidRedirectUri` extracted as a shared helper
- Discovery metadata now advertises registration_endpoint and auth method
`none`; token/revoke endpoints no longer require client_secret for
public clients
Client:
- New OAuthRegisterPage (/oauth/register) — loading → optional
login-required gate → scope selection → done states
- New ScopeGroupPicker component — collapsible groups, indeterminate
checkboxes, select-all per group or globally
- oauthApi.register.{validate,submit} added to api/client.ts
- apiClient exported so it can be reused outside api/client.ts
- IntegrationsTab tests fixed for new collapsible section structure
- collab_notes fallback changed from undefined to [] in MCP trip tools
Navigation tools:
- list_trips and get_trip_summary are now always registered for any
OAuth session regardless of granted scopes — they are required for
trip ID discovery before any scoped tool can be used
- get_trip_summary filters optional sections (budget, packing, collab,
reservations) by the client's OAuth scopes when called without trips:read
Deprecation notice:
- Inject static token deprecation warning into the first tool result
(list_trips or get_trip_summary) via a per-session closure so Claude
is forced to surface it — the instructions field alone is only
background context and is not proactively shown to the user
UI:
- OAuth client creation modal: add hint explaining the always-available
tools, remove the "must select at least one scope" submit guard
- OAuth consent screen: add "Always included" section showing list_trips
and get_trip_summary; handles zero-scope clients gracefully (empty
permissions section is hidden)
The polyfill was injected as an inline script at build time, causing a hard
CSP block under script-src 'self'. All browsers that support ES modules also
support modulepreload natively, so the polyfill is unnecessary.
Introduce trips:share as a dedicated OAuth scope for managing public
share links, decoupled from trips:read and trips:write. Share link
tools (get/create/delete_share_link) now gate on canShareTrips()
instead of the generic read/write scopes. Scope added to both client
and server definitions with full test coverage.
Redesign the consent screen from a narrow single-column card
(max-w-sm) to a two-panel layout (max-w-2xl): app identity and
action buttons on the left, scrollable scope list on the right.
Responsive — stacks vertically on mobile.
vitest@4 requires vite@^6, causing two conflicting esbuild versions in
the lockfile and EBADPLATFORM errors during Docker npm ci. Pin to vitest
3.x which supports vite@5 and resolves a single esbuild@0.21.5.
OAuth 2.1 authentication for MCP:
- Add OAuth 2.1 authorization server with PKCE support (routes/oauth.ts)
- Add OAuth service for client CRUD, auth-code flow, and token management (services/oauthService.ts)
- Add typed scope definitions and enforcement helpers (mcp/scopes.ts)
- Add OAuth consent UI page (OAuthAuthorizePage.tsx)
- Add client-side scope labels and descriptions (api/oauthScopes.ts)
- Integrate OAuth token auth into MCP handler alongside existing static tokens
- All OAuth endpoints gated on `mcp` addon
Addon gating across MCP tools, resources, and prompts:
- Add typed ADDON_IDS constant (server/src/addons.ts) replacing all string literals
- Gate budget tools and resources (trip-budget, per-person, settlement) on `budget` addon
- Gate packing tools and resources (trip-packing, trip-packing-bags, trip-todos) on `packing` addon
- Gate todos tools on `packing` addon (mirrors web UI Lists tab behavior)
- Expand atlas gate to cover full tool body (bucket-list + country tools no longer leak)
- Expand collab gate to cover full tool body (collab notes no longer leak)
- Gate packing-list and budget-overview MCP prompts on their respective addons
- Gate get_trip_summary sections per addon; blank packing/budget/collab_notes/todos when disabled
- Remove trip-files resource and files field from get_trip_summary
- Replace all isAddonEnabled('literal') calls with ADDON_IDS constants
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add reordering support for budget categories and line items within
categories. Changes persist via new DB table (budget_category_order)
and existing sort_order column. Live sync via WebSocket budget:reordered
event. Use Map instead of plain objects for category grouping to
preserve insertion order with numeric category names.
The "Unplanned" filter button in PlacesSidebar only filtered the place
list but not the map. Propagate the filter state to TripPlannerPage so
mapPlaces excludes planned places when the filter is active.
Handle European number formats (e.g. 1.150,32) by detecting the last
separator as decimal and stripping thousand separators. Applied to
budget inline edit cells, add item row, and reservation price field.
Fixes#498
Adds a dedicated download button (blob-based, works on iOS WebApp)
to file cards, file preview modal, and image lightbox. Previously
only "open in tab" was available which doesn't work for non-browser
file types like .gpx on iOS.
Fixes#462
- Notifications: map raw avatar filename to /uploads/avatars/ URL in
getNotifications, createNotification broadcasts, and respond handler
- Admin listUsers: include avatar field in SELECT and map to avatar_url
- Admin page: render actual avatar image instead of initial letter only
- Budget loadItemMembers: map avatar to avatar_url (fixed in prior commit)
Fixes#507
Adds a collapse/expand toggle to the day detail panel header.
Collapsed state persists across day switches. Clicking the header
or the chevron button toggles between compact header-only view
and the full detail panel.
Closes#457
Node 22 URL.createObjectURL strictly requires a native node:buffer Blob
and throws ERR_INVALID_ARG_TYPE when given a jsdom Blob (caught by
fetchImageAsBlob, returning ''). Node 24 relaxed this check, masking the
failure locally.
Tests 007, 011: replace MSW/Response-based fetch mocks with direct
vi.spyOn(fetch) mocks returning node:buffer Blobs via a duck-typed
response object. The real URL.createObjectURL now handles the correct
Blob type and returns a genuine blob: URL on all Node versions.
Test 012: URL.createObjectURL identity varies across Node versions
making it impossible to spy on reliably. Replace createObjectURLSpy
assertion with a completedFetches counter in the fetch mock, which
proves the same semantic guarantee (6 requests ran, 7th was cleared).
setup.ts: restore the original conditional guard so the vi.fn fallback
only applies when URL.createObjectURL is completely absent, not
overwriting a working real implementation.
In jsdom, source modules resolve bare 'URL' identifiers through
window.URL (the jsdom window object), not through globalThis.URL (Node's
URL class). On GitHub Actions these are distinct objects, so all prior
attempts (Object.defineProperty, direct assignment, vi.stubGlobal) were
patching the wrong object and failing silently.
Changes:
- setup.ts: Object.defineProperty targets window.URL so the vi.fn mock
is visible to authUrl.ts at call time
- authUrl.test.ts: drop vi.stubGlobal approach; add vi.clearAllMocks()
to reset accumulated call counts on the setup.ts vi.fn between tests;
fix vi.spyOn target to window.URL in test 012
Direct property assignment and Object.defineProperty both fail
silently on CI when jsdom marks URL.createObjectURL as non-writable
and non-configurable. vi.stubGlobal('URL', ...) replaces globalThis.URL
entirely — which always succeeds — while extending the real URL class
so all URL parsing behaviour is preserved. vi.unstubAllGlobals() is
called at the start of beforeEach to reset cleanly between tests.
Two root causes:
1. authUrl.test.ts (007, 011, 012): Object.defineProperty in setup.ts
fails silently on CI when jsdom's URL.createObjectURL is
non-configurable. vi.restoreAllMocks() in beforeEach then restores
the property to jsdom's native implementation (returns '').
Fix: assign URL.createObjectURL = vi.fn(() => 'blob:mock') directly
in authUrl.test.ts's beforeEach, after restoreAllMocks(), so every
test in the file gets a fresh, reliable mock. Remove the now-
unnecessary mockClear() from test 012.
2. client.test.ts (013): MSW patches the global Response constructor and
calls blob.stream() on the body — a method not implemented by jsdom's
Blob. Fix: replace new Response(blob) with a plain-object duck-type
({ ok: true, blob: () => Promise.resolve(blob) }) to bypass the
patched constructor entirely.
Three interrelated issues caused 4 tests to pass locally but fail on CI:
1. setup.ts only applied the URL.createObjectURL stub when it was
undefined, but jsdom already defines it (returning ''). Changed to
always override with configurable:true so the predictable 'blob:mock'
value is set in every environment.
2. FE-API-013 used Object.defineProperty (non-configurable in jsdom) and
MSW to handle a native fetch call. Replaced with vi.spyOn for both
URL.createObjectURL/revokeObjectURL and a direct fetch mock, which is
more reliable across environments.
3. FE-COMP-AUTHURL-012's vi.spyOn(URL, 'createObjectURL') returned the
same vi.fn() instance set in setup.ts, accumulating calls from all
prior tests in the file (1+8+7+6=22 instead of 6). Added mockClear()
immediately after the spy setup to reset the count.
Add npm overrides to force esbuild@^0.28.0, resolving the conflict
between vite@5.x (which installs 0.21.5) and vitest@4.x's internal
vite@8.x (which requires ^0.27.0 || ^0.28.0). Without this,
npm ci fails on a clean install.
Three distinct bugs caused infinite OIDC redirect loops:
1. After logout, navigating to /login with no signal to suppress the
auto-redirect caused the login page to immediately re-trigger the
OIDC flow. Fixed by passing `{ state: { noRedirect: true } }` via
React Router's navigation state (not URL params, which were fragile
due to async cleanup timing) from all logout call sites.
2. On the OIDC callback page (/login?oidc_code=...), App.tsx's
mount-level loadUser() fired concurrently with the LoginPage's
exchange fetch. The App-level call had no cookie yet and got a 401,
which (if it resolved after the successful exchange loadUser()) would
overwrite isAuthenticated back to false. Fixed by skipping loadUser()
in App.tsx when the initial path is /login.
3. React 18 StrictMode double-invokes useEffect. The first run called
window.history.replaceState to clean the oidc_code from the URL
before starting the async exchange, so the second run saw no
oidc_code and fell through to the getAppConfig auto-redirect, firing
window.location.href = '/api/auth/oidc/login' before the exchange
could complete. Fixed by adding a useRef guard to prevent
double-execution and moving replaceState into the fetch callbacks so
the URL is only cleaned after the exchange resolves.
Also adds login.oidcLoggedOut translation key in all 14 languages to
show "You have been logged out" instead of the generic OIDC-only
message when landing on /login after an intentional logout.
Closes#491
- Exclude place-assigned reservations from timeline to prevent duplicate display
- Use selected day's date instead of today when entering time without date
- Pass day_id when updating reservations, not only when creating
- Clear reservation_time fields when switching booking type to hotel (#459)
- Parse date-only reservation_end_time correctly on edit (#455)
- Show end date on booking cards for date-only values (#455)
- Add auth token to file download links in bookings (#454)
- Account for timezone offsets in flight time validation (#456)
Bidirectional substring matching in isVisitedFeature caused unrelated
regions to be highlighted as visited (e.g. selecting Nordrhein-Westfalen
also marked Nord France due to "nord" being a substring match).
Replace the fuzzy loop with an additional exact check against the Natural
Earth name_en property to cover English-vs-native name mismatches.
Also fix Nominatim field priority to prefer state over county so
reverse-geocoded places resolve to the correct admin-1 level.
Adds integration tests ATLAS-009 through ATLAS-011 covering mark/unmark
region endpoints and user isolation.
Fixes#446
Timed places now auto-sort chronologically when a time is set.
Untimed places can be freely dragged between timed items.
Transports are inserted by time with per-day position override.
Fixes regression from multi-day spanning PR that removed timed/untimed split.
Reordering places on one day of a multi-day reservation no longer
affects the order on other days. Transport positions are now stored
per-day in a new reservation_day_positions table instead of a single
global day_plan_position on the reservation.
If the budget category stored in reservation metadata was deleted, the
form would re-submit it on next save, resurrecting the deleted category.
Now validates against live budget items on form init and falls back to
auto-generation when the stored category is gone.
Closes#442
If a user's last visited tab belongs to an addon that gets disabled while
they are away, re-opening the trip now resets the active tab to 'plan'
instead of rendering the inaccessible addon page.
Closes#441
The navigateTo function was clearing lightboxInfo without re-fetching it,
causing the EXIF sidebar to disappear and nav button placement to break.
Mirrors the fetch logic already present in the thumbnail click handler.
Fixes#439