Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,10 @@ development/generate-lavamoat-policies.js @MetaMask/extension-platfo
# Background Process
app/scripts/lib/createMetaRPCHandler.js @MetaMask/extension-platform
app/scripts/lib/metaRPCClientFactory.ts @MetaMask/extension-platform

# Delegation Team
shared/lib/deep-links/routes/gator-permissions.ts @MetaMask/delegation
shared/lib/gator-permissions/ @MetaMask/delegation
app/scripts/controller-init/gator-permissions/ @MetaMask/delegation
ui/hooks/gator-permissions/ @MetaMask/delegation
ui/components/multichain/pages/gator-permissions/ @MetaMask/delegation
3 changes: 3 additions & 0 deletions app/_locales/en/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions app/_locales/en_GB/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

170 changes: 170 additions & 0 deletions shared/lib/deep-links/routes/gator-permissions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {
gatorPermissions,
GatorPermissionsQueryParams,
} from './gator-permissions';
import { Destination } from './route';

function assertPathDestination(
result: Destination,
): asserts result is Extract<Destination, { path: string }> {
expect('path' in result).toBe(true);
}

describe('gatorPermissionsRoute', () => {
describe('handler with valid parameters', () => {
it('returns encoded path with valid type and site', () => {
const params = new URLSearchParams({
[GatorPermissionsQueryParams.Type]: 'token-transfer',
[GatorPermissionsQueryParams.Site]: 'https://example.com',
});

const result = gatorPermissions.handler(params);

assertPathDestination(result);
expect(result.path).toBe(
'/gator-permissions/token-transfer/https%3A%2F%2Fexample.com',
);
expect(result.query.toString()).toBe('');
});

it('handles http protocol in site parameter', () => {
const params = new URLSearchParams({
[GatorPermissionsQueryParams.Type]: 'token-transfer',
[GatorPermissionsQueryParams.Site]: 'http://example.com',
});

const result = gatorPermissions.handler(params);

assertPathDestination(result);
expect(result.path).toBe(
'/gator-permissions/token-transfer/http%3A%2F%2Fexample.com',
);
});

it('handles site with port number', () => {
const params = new URLSearchParams({
[GatorPermissionsQueryParams.Type]: 'token-transfer',
[GatorPermissionsQueryParams.Site]: 'https://example.com:8080',
});

const result = gatorPermissions.handler(params);

assertPathDestination(result);
expect(result.path).toBe(
'/gator-permissions/token-transfer/https%3A%2F%2Fexample.com%3A8080',
);
});
});

describe('handler with missing parameters', () => {
it('throws when type parameter is missing', () => {
const params = new URLSearchParams({
[GatorPermissionsQueryParams.Site]: 'https://example.com',
});

expect(() => gatorPermissions.handler(params)).toThrow(
'Missing type parameter',
);
});

it('throws when site parameter is missing', () => {
const params = new URLSearchParams({
[GatorPermissionsQueryParams.Type]: 'token-transfer',
});

expect(() => gatorPermissions.handler(params)).toThrow(
'Missing site parameter',
);
});

it('throws when both type and site are missing', () => {
const params = new URLSearchParams();

expect(() => gatorPermissions.handler(params)).toThrow(
'Missing type parameter',
);
});
});

describe('handler with invalid type parameter', () => {
it('throws when type is not "token-transfer"', () => {
const params = new URLSearchParams({
[GatorPermissionsQueryParams.Type]: 'invalid-type',
[GatorPermissionsQueryParams.Site]: 'https://example.com',
});

expect(() => gatorPermissions.handler(params)).toThrow(
'Invalid type parameter',
);
});

it('throws for empty type parameter', () => {
const params = new URLSearchParams({
[GatorPermissionsQueryParams.Type]: '',
[GatorPermissionsQueryParams.Site]: 'https://example.com',
});

expect(() => gatorPermissions.handler(params)).toThrow(
'Missing type parameter',
);
});
});

describe('handler with invalid site parameter', () => {
type InvalidSiteTestCase = {
site: string;
description: string;
};

const invalidSiteCases: InvalidSiteTestCase[] = [
{ site: 'not-a-url', description: 'invalid URL format' },
{ site: 'ftp://example.com', description: 'unsupported protocol (ftp)' },
{ site: 'data:text/html,<h1>test</h1>', description: 'data URI' },
// eslint-disable-next-line no-script-url
{ site: 'javascript:alert(1)', description: 'javascript protocol' },
{ site: 'https://example.com:notaport', description: 'invalid port' },
{
site: 'https://example.com/path',
description: 'URL with path instead of origin',
},
{
site: 'https://example.com?query=1',
description: 'URL with query params instead of origin',
},
{
site: 'https://example.com#hash',
description: 'URL with hash instead of origin',
},
];

// @ts-expect-error This function is missing from the Mocha type definitions
it.each(invalidSiteCases)(
'throws for invalid site: $description',
({ site }: InvalidSiteTestCase) => {
const params = new URLSearchParams({
[GatorPermissionsQueryParams.Type]: 'token-transfer',
[GatorPermissionsQueryParams.Site]: site,
});

expect(() => gatorPermissions.handler(params)).toThrow(
'Invalid site parameter',
);
},
);
});

describe('getTitle', () => {
it('returns the correct title key', () => {
const params = new URLSearchParams();
const title = gatorPermissions.getTitle(params);

expect(title).toBe('deepLink_theGatorPermissionsPage');
});
});

describe('pathname', () => {
it('has the correct pathname', () => {
expect(gatorPermissions.pathname).toBe('/gator-permissions');
});
});
});
51 changes: 51 additions & 0 deletions shared/lib/deep-links/routes/gator-permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Route } from './route';

export enum GatorPermissionsQueryParams {
Type = 'type',
Site = 'site',
}

function isValidOrigin(originString: string): boolean {
try {
const url = new URL(originString);
// The URL constructor ensures proper URL formatting.
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
return false;
}
return originString === url.origin;
} catch (e) {
// If new URL() throws a TypeError, the string is not a valid URL format
return false;
}

Check warning on line 19 in shared/lib/deep-links/routes/gator-permissions.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Handle this exception or don't catch it at all.

See more on https://sonarcloud.io/project/issues?id=metamask-extension&issues=AZz_SLilaGxXXtxMAgmp&open=AZz_SLilaGxXXtxMAgmp&pullRequest=40995
}

export const gatorPermissions = new Route({
pathname: '/gator-permissions',
getTitle: (_: URLSearchParams) => 'deepLink_theGatorPermissionsPage',
handler: function handler(params: URLSearchParams) {
const type = params.get(GatorPermissionsQueryParams.Type);
const site = params.get(GatorPermissionsQueryParams.Site);
const query = new URLSearchParams();
if (!type) {
throw new Error('Missing type parameter');
}

if (type !== 'token-transfer') {
throw new Error('Invalid type parameter');
}

if (!site) {
throw new Error('Missing site parameter');
}

if (!isValidOrigin(site)) {
throw new Error('Invalid site parameter');
}

const encodedSite = encodeURIComponent(site);
return {
path: `/gator-permissions/${type}/${encodedSite}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely only a nit: should we lean into the public naming /advanced-permissions/ ?

query,
};
},
});
2 changes: 2 additions & 0 deletions shared/lib/deep-links/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { sell } from './sell';
import { shield } from './shield';
import { swap } from './swap';
import { trending } from './trending';
import { gatorPermissions } from './gator-permissions';

export type { Route } from './route';

Expand Down Expand Up @@ -60,3 +61,4 @@ addRoute(rewards);
addRoute(shield);
addRoute(asset);
addRoute(trending);
addRoute(gatorPermissions);
Loading