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:
jubnl
2026-04-11 02:06:09 +02:00
parent 4670d4914c
commit 535c06bb3f
39 changed files with 1930 additions and 237 deletions
+9 -2
View File
@@ -24,14 +24,21 @@ describe('ALL_SCOPES', () => {
expect(ALL_SCOPES).toContain('trips:write');
expect(ALL_SCOPES).toContain('trips:delete');
expect(ALL_SCOPES).toContain('trips:share');
expect(ALL_SCOPES).toContain('places:read');
expect(ALL_SCOPES).toContain('places:write');
expect(ALL_SCOPES).toContain('atlas:read');
expect(ALL_SCOPES).toContain('atlas:write');
expect(ALL_SCOPES).toContain('budget:read');
expect(ALL_SCOPES).toContain('budget:write');
expect(ALL_SCOPES).toContain('packing:read');
expect(ALL_SCOPES).toContain('packing:write');
expect(ALL_SCOPES).toContain('todos:read');
expect(ALL_SCOPES).toContain('todos:write');
expect(ALL_SCOPES).toContain('collab:read');
expect(ALL_SCOPES).toContain('collab:write');
expect(ALL_SCOPES).toContain('places:read');
expect(ALL_SCOPES).toContain('places:write');
expect(ALL_SCOPES).toContain('geo:read');
expect(ALL_SCOPES).toContain('weather:read');
expect(ALL_SCOPES).not.toContain('media:read');
});
it('is a non-empty array', () => {
@@ -131,7 +131,7 @@ describe('Tool: delete_day', () => {
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'day:deleted', { id: day.id });
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'day:deleted', expect.objectContaining({ id: day.id }));
expect(testDb.prepare('SELECT id FROM days WHERE id = ?').get(day.id)).toBeUndefined();
});
});