mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
feat(mcp): granular OAuth scopes and per-client rate limiting
- Split `media:read` into `geo:read` and `weather:read` scopes - Add dedicated `atlas:read/write` scopes (previously under `places`) - Add dedicated `todos:read/write` scopes (previously under `collab`) - Rate limiting now keyed by userId+clientId instead of userId alone - Bind MCP sessions to the OAuth client that created them - Log MCP tool calls to audit log with clientId - Invalidate all MCP sessions on addon state change - Reduce session sweep interval from 10min to 1min - Update all translations with new scope labels
This commit is contained in:
@@ -7,38 +7,50 @@ export interface ScopeInfo {
|
||||
group: string
|
||||
}
|
||||
|
||||
export const SCOPE_GROUPS: Record<string, ScopeInfo> = {
|
||||
'trips:read': { label: 'View trips & itineraries', description: 'Read trips, days, day notes, and members', group: 'Trips' },
|
||||
'trips:write': { label: 'Edit trips & itineraries', description: 'Create and update trips, days, notes, and manage members', group: 'Trips' },
|
||||
'trips:delete': { label: 'Delete trips', description: 'Permanently delete entire trips — this action is irreversible', group: 'Trips' },
|
||||
'trips:share': { label: 'Manage share links', description: 'Create, update, and revoke public share links for trips', group: 'Trips' },
|
||||
'places:read': { label: 'View places & map data', description: 'Read places, day assignments, tags, categories, and visited countries', group: 'Places' },
|
||||
'places:write': { label: 'Manage places', description: 'Create, update, and delete places, assignments, tags, and atlas entries', group: 'Places' },
|
||||
'packing:read': { label: 'View packing lists', description: 'Read packing items, bags, and category assignees', group: 'Packing' },
|
||||
'packing:write': { label: 'Manage packing lists', description: 'Add, update, delete, toggle, and reorder packing items and bags', group: 'Packing' },
|
||||
'budget:read': { label: 'View budget', description: 'Read budget items and expense breakdown', group: 'Budget' },
|
||||
'budget:write': { label: 'Manage budget', description: 'Create, update, and delete budget items', group: 'Budget' },
|
||||
'reservations:read': { label: 'View reservations', description: 'Read reservations and accommodation details', group: 'Reservations' },
|
||||
'reservations:write': { label: 'Manage reservations', description: 'Create, update, delete, and reorder reservations', group: 'Reservations' },
|
||||
'collab:read': { label: 'View collaboration', description: 'Read collab notes, polls, messages, and to-do items', group: 'Collaboration' },
|
||||
'collab:write': { label: 'Manage collaboration', description: 'Create, update, and delete collab notes, todos, polls, and messages', group: 'Collaboration' },
|
||||
'notifications:read': { label: 'View notifications', description: 'Read in-app notifications and unread counts', group: 'Notifications' },
|
||||
'notifications:write': { label: 'Manage notifications', description: 'Mark notifications as read and respond to them', group: 'Notifications' },
|
||||
'vacay:read': { label: 'View vacation plans', description: 'Read vacation planning data, entries, and stats', group: 'Vacation' },
|
||||
'vacay:write': { label: 'Manage vacation plans', description: 'Create and manage vacation entries, holidays, and team plans', group: 'Vacation' },
|
||||
'media:read': { label: 'Maps & weather data', description: 'Search locations, resolve map URLs, and fetch weather forecasts', group: 'Media' },
|
||||
export interface ScopeKeys {
|
||||
labelKey: string
|
||||
descriptionKey: string
|
||||
groupKey: string
|
||||
}
|
||||
|
||||
export const SCOPE_GROUPS: Record<string, ScopeKeys> = {
|
||||
'trips:read': { labelKey: 'oauth.scope.trips:read.label', descriptionKey: 'oauth.scope.trips:read.description', groupKey: 'oauth.scope.group.trips' },
|
||||
'trips:write': { labelKey: 'oauth.scope.trips:write.label', descriptionKey: 'oauth.scope.trips:write.description', groupKey: 'oauth.scope.group.trips' },
|
||||
'trips:delete': { labelKey: 'oauth.scope.trips:delete.label', descriptionKey: 'oauth.scope.trips:delete.description', groupKey: 'oauth.scope.group.trips' },
|
||||
'trips:share': { labelKey: 'oauth.scope.trips:share.label', descriptionKey: 'oauth.scope.trips:share.description', groupKey: 'oauth.scope.group.trips' },
|
||||
'places:read': { labelKey: 'oauth.scope.places:read.label', descriptionKey: 'oauth.scope.places:read.description', groupKey: 'oauth.scope.group.places' },
|
||||
'places:write': { labelKey: 'oauth.scope.places:write.label', descriptionKey: 'oauth.scope.places:write.description', groupKey: 'oauth.scope.group.places' },
|
||||
'atlas:read': { labelKey: 'oauth.scope.atlas:read.label', descriptionKey: 'oauth.scope.atlas:read.description', groupKey: 'oauth.scope.group.atlas' },
|
||||
'atlas:write': { labelKey: 'oauth.scope.atlas:write.label', descriptionKey: 'oauth.scope.atlas:write.description', groupKey: 'oauth.scope.group.atlas' },
|
||||
'packing:read': { labelKey: 'oauth.scope.packing:read.label', descriptionKey: 'oauth.scope.packing:read.description', groupKey: 'oauth.scope.group.packing' },
|
||||
'packing:write': { labelKey: 'oauth.scope.packing:write.label', descriptionKey: 'oauth.scope.packing:write.description', groupKey: 'oauth.scope.group.packing' },
|
||||
'todos:read': { labelKey: 'oauth.scope.todos:read.label', descriptionKey: 'oauth.scope.todos:read.description', groupKey: 'oauth.scope.group.todos' },
|
||||
'todos:write': { labelKey: 'oauth.scope.todos:write.label', descriptionKey: 'oauth.scope.todos:write.description', groupKey: 'oauth.scope.group.todos' },
|
||||
'budget:read': { labelKey: 'oauth.scope.budget:read.label', descriptionKey: 'oauth.scope.budget:read.description', groupKey: 'oauth.scope.group.budget' },
|
||||
'budget:write': { labelKey: 'oauth.scope.budget:write.label', descriptionKey: 'oauth.scope.budget:write.description', groupKey: 'oauth.scope.group.budget' },
|
||||
'reservations:read': { labelKey: 'oauth.scope.reservations:read.label', descriptionKey: 'oauth.scope.reservations:read.description', groupKey: 'oauth.scope.group.reservations' },
|
||||
'reservations:write': { labelKey: 'oauth.scope.reservations:write.label', descriptionKey: 'oauth.scope.reservations:write.description', groupKey: 'oauth.scope.group.reservations' },
|
||||
'collab:read': { labelKey: 'oauth.scope.collab:read.label', descriptionKey: 'oauth.scope.collab:read.description', groupKey: 'oauth.scope.group.collab' },
|
||||
'collab:write': { labelKey: 'oauth.scope.collab:write.label', descriptionKey: 'oauth.scope.collab:write.description', groupKey: 'oauth.scope.group.collab' },
|
||||
'notifications:read': { labelKey: 'oauth.scope.notifications:read.label', descriptionKey: 'oauth.scope.notifications:read.description', groupKey: 'oauth.scope.group.notifications' },
|
||||
'notifications:write': { labelKey: 'oauth.scope.notifications:write.label', descriptionKey: 'oauth.scope.notifications:write.description', groupKey: 'oauth.scope.group.notifications' },
|
||||
'vacay:read': { labelKey: 'oauth.scope.vacay:read.label', descriptionKey: 'oauth.scope.vacay:read.description', groupKey: 'oauth.scope.group.vacay' },
|
||||
'vacay:write': { labelKey: 'oauth.scope.vacay:write.label', descriptionKey: 'oauth.scope.vacay:write.description', groupKey: 'oauth.scope.group.vacay' },
|
||||
'geo:read': { labelKey: 'oauth.scope.geo:read.label', descriptionKey: 'oauth.scope.geo:read.description', groupKey: 'oauth.scope.group.geo' },
|
||||
'weather:read': { labelKey: 'oauth.scope.weather:read.label', descriptionKey: 'oauth.scope.weather:read.description', groupKey: 'oauth.scope.group.weather' },
|
||||
}
|
||||
|
||||
export const ALL_SCOPES = Object.keys(SCOPE_GROUPS)
|
||||
|
||||
// Group all scopes for the client registration form
|
||||
export const SCOPE_GROUP_NAMES = [...new Set(Object.values(SCOPE_GROUPS).map(s => s.group))]
|
||||
export const SCOPE_GROUP_NAMES = [...new Set(Object.values(SCOPE_GROUPS).map(s => s.groupKey))]
|
||||
|
||||
export function getScopesByGroup(): Record<string, Array<{ scope: string } & ScopeInfo>> {
|
||||
export function getScopesByGroup(t: (key: string) => string): Record<string, Array<{ scope: string } & ScopeInfo>> {
|
||||
const groups: Record<string, Array<{ scope: string } & ScopeInfo>> = {}
|
||||
for (const [scope, info] of Object.entries(SCOPE_GROUPS)) {
|
||||
if (!groups[info.group]) groups[info.group] = []
|
||||
groups[info.group].push({ scope, ...info })
|
||||
for (const [scope, keys] of Object.entries(SCOPE_GROUPS)) {
|
||||
const group = t(keys.groupKey)
|
||||
if (!groups[group]) groups[group] = []
|
||||
groups[group].push({ scope, label: t(keys.labelKey), description: t(keys.descriptionKey), group })
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user