feat(oauth): add trips:share scope and redesign consent screen

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.
This commit is contained in:
jubnl
2026-04-10 00:54:59 +02:00
parent 41f1dd9ce5
commit 8212f3c023
5 changed files with 97 additions and 46 deletions
+40
View File
@@ -9,6 +9,7 @@ import {
canWrite,
canRead,
canDeleteTrips,
canShareTrips,
ALL_SCOPES,
SCOPE_INFO,
} from '../../../src/mcp/scopes';
@@ -22,6 +23,7 @@ describe('ALL_SCOPES', () => {
expect(ALL_SCOPES).toContain('trips:read');
expect(ALL_SCOPES).toContain('trips:write');
expect(ALL_SCOPES).toContain('trips:delete');
expect(ALL_SCOPES).toContain('trips:share');
expect(ALL_SCOPES).toContain('budget:read');
expect(ALL_SCOPES).toContain('budget:write');
expect(ALL_SCOPES).toContain('packing:read');
@@ -122,6 +124,10 @@ describe('canReadTrips', () => {
expect(canReadTrips(['trips:delete'])).toBe(true);
});
it('returns true when trips:share is present', () => {
expect(canReadTrips(['trips:share'])).toBe(true);
});
it('returns false when only unrelated scopes are present', () => {
expect(canReadTrips(['budget:read', 'packing:write'])).toBe(false);
});
@@ -214,3 +220,37 @@ describe('canDeleteTrips', () => {
expect(canDeleteTrips([])).toBe(false);
});
});
// ---------------------------------------------------------------------------
// canShareTrips
// ---------------------------------------------------------------------------
describe('canShareTrips', () => {
it('returns true when scopes is null (full access)', () => {
expect(canShareTrips(null)).toBe(true);
});
it('returns true when trips:share is present', () => {
expect(canShareTrips(['trips:share'])).toBe(true);
});
it('returns false when only trips:read is present', () => {
expect(canShareTrips(['trips:read'])).toBe(false);
});
it('returns false when only trips:write is present', () => {
expect(canShareTrips(['trips:write'])).toBe(false);
});
it('returns false when only trips:delete is present', () => {
expect(canShareTrips(['trips:delete'])).toBe(false);
});
it('returns false for unrelated scopes', () => {
expect(canShareTrips(['budget:write', 'packing:read'])).toBe(false);
});
it('returns false for empty scopes array', () => {
expect(canShareTrips([])).toBe(false);
});
});