Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c0f6e6d
feature: add capabilities to keyring endowment
hmalik88 Mar 19, 2026
ea8c62f
refactor: remove keyring-api dep and add local definition for capabil…
hmalik88 Mar 20, 2026
e698fdc
Merge remote-tracking branch 'origin/main' into hm/add-keyring-capabi…
hmalik88 Mar 20, 2026
1e2eff9
chore: update changelog
hmalik88 Mar 20, 2026
cfded47
chore: update snap-rpc-methods changelog
hmalik88 Mar 20, 2026
1b7c802
fix: fix capabilities getter
hmalik88 Mar 20, 2026
d122b58
chore: update coverage
hmalik88 Mar 20, 2026
7cfe31b
fix: update type
hmalik88 Mar 20, 2026
9210d91
fix: add capabilities export
hmalik88 Mar 20, 2026
7be44a0
fix: update snapshot
hmalik88 Mar 20, 2026
1662a28
test: add tests for assertIsKeyringCapabilities
hmalik88 Mar 20, 2026
cdc079f
fix: update initial permissions type
hmalik88 Mar 20, 2026
5f1946d
fix: fix types
hmalik88 Mar 20, 2026
61dc185
fix: add missing word to changelog
hmalik88 Apr 2, 2026
1853c08
refactor: apply code review
hmalik88 Apr 2, 2026
5303524
Merge remote-tracking branch 'origin/main' into hm/add-keyring-capabi…
hmalik88 Apr 2, 2026
9dcd36c
fix: lint fix
hmalik88 Apr 2, 2026
04a58cd
test: add test for uncovered line
hmalik88 Apr 2, 2026
9fb1f9f
Merge remote-tracking branch 'origin/main' into hm/add-keyring-capabi…
hmalik88 Apr 2, 2026
a06ad51
chore: update test coverage
hmalik88 Apr 2, 2026
7402845
refactor: apply code review
hmalik88 Apr 2, 2026
00733ae
refactor: update if statement
hmalik88 Apr 2, 2026
2fae117
fix: revert deriving values
hmalik88 Apr 6, 2026
e629c04
Merge remote-tracking branch 'origin/main' into hm/add-keyring-capabi…
hmalik88 Apr 6, 2026
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
6 changes: 6 additions & 0 deletions packages/snaps-rpc-methods/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `capabilities` caveat support to `endowment:keyring` ([#3903](https://github.com/MetaMask/snaps/pull/3903))
- Snap manifests can now declare a `capabilities` object under `endowment:keyring` the capabilities supported by the keyring.
- New `getKeyringCaveatCapabilities` function to retrieve the capabilities caveat value from a permission.

## [15.0.0]

### Changed
Expand Down
8 changes: 4 additions & 4 deletions packages/snaps-rpc-methods/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, {
],
coverageThreshold: {
global: {
branches: 96.57,
functions: 99.19,
lines: 99.05,
statements: 98.77,
branches: 96.62,
functions: 99.2,
lines: 99.06,
statements: 98.78,
},
},
});
149 changes: 146 additions & 3 deletions packages/snaps-rpc-methods/src/endowments/keyring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SnapCaveatType } from '@metamask/snaps-utils';

import { SnapEndowments } from './enum';
import {
getKeyringCaveatCapabilities,
getKeyringCaveatMapper,
getKeyringCaveatOrigins,
keyringCaveatSpecifications,
Expand All @@ -18,6 +19,7 @@ describe('endowment:keyring', () => {
endowmentGetter: expect.any(Function),
allowedCaveats: [
SnapCaveatType.KeyringOrigin,
SnapCaveatType.KeyringCapabilities,
SnapCaveatType.MaxRequestTime,
],
subjectTypes: [SubjectType.Snap],
Expand All @@ -44,7 +46,7 @@ describe('endowment:keyring', () => {
caveats: [{ type: 'foo', value: 'bar' }],
}),
).toThrow(
'Expected the following caveats: "keyringOrigin", "maxRequestTime", received "foo".',
'Expected the following caveats: "keyringOrigin", "keyringCapabilities", "maxRequestTime", received "foo".',
);

expect(() =>
Expand All @@ -61,7 +63,7 @@ describe('endowment:keyring', () => {
});

describe('getKeyringCaveatMapper', () => {
it('maps a value to a caveat', () => {
it('maps a value to a caveat without capabilities', () => {
expect(
getKeyringCaveatMapper({ allowedOrigins: ['foo.com'] }),
).toStrictEqual({
Expand All @@ -73,6 +75,36 @@ describe('getKeyringCaveatMapper', () => {
],
});
});

it('maps a value to caveats including capabilities', () => {
expect(
getKeyringCaveatMapper({
allowedOrigins: ['foo.com'],
capabilities: {
scopes: ['bip122:000000000019d6689c085ae165831e93'],
bip44: { derivePath: true },
},
}),
).toStrictEqual({
caveats: [
{
type: SnapCaveatType.KeyringOrigin,
value: {
allowedOrigins: ['foo.com'],
},
},
{
type: SnapCaveatType.KeyringCapabilities,
value: {
capabilities: {
scopes: ['bip122:000000000019d6689c085ae165831e93'],
bip44: { derivePath: true },
},
},
},
],
});
});
});

describe('getKeyringCaveatOrigins', () => {
Expand Down Expand Up @@ -118,8 +150,52 @@ describe('getKeyringCaveatOrigins', () => {
});
});

describe('getKeyringCaveatCapabilities', () => {
it('returns the capabilities from the caveat', () => {
expect(
// @ts-expect-error Missing other required permission types.
getKeyringCaveatCapabilities({
caveats: [
{
type: SnapCaveatType.KeyringCapabilities,
value: {
capabilities: {
scopes: ['bip122:000000000019d6689c085ae165831e93'],
bip44: { derivePath: true },
},
},
},
],
}),
).toStrictEqual({
capabilities: {
scopes: ['bip122:000000000019d6689c085ae165831e93'],
bip44: { derivePath: true },
},
});
});

it('returns null when the capabilities caveat is absent', () => {
expect(
// @ts-expect-error Missing other required permission types.
getKeyringCaveatCapabilities({
caveats: [
{
type: SnapCaveatType.KeyringOrigin,
value: { allowedOrigins: ['foo.com'] },
},
],
}),
).toBeNull();
});

it('returns null when permission is undefined', () => {
expect(getKeyringCaveatCapabilities(undefined)).toBeNull();
});
});

describe('keyringCaveatSpecifications', () => {
describe('validator', () => {
describe('keyringOrigin validator', () => {
it('throws if the caveat values are invalid', () => {
expect(() =>
keyringCaveatSpecifications[SnapCaveatType.KeyringOrigin].validator?.(
Expand All @@ -142,4 +218,71 @@ describe('keyringCaveatSpecifications', () => {
);
});
});

describe('keyringCapabilities validator', () => {
it('throws if the caveat value is not a plain object', () => {
expect(() =>
keyringCaveatSpecifications[
SnapCaveatType.KeyringCapabilities
].validator?.(
// @ts-expect-error Missing value type.
{
type: SnapCaveatType.KeyringCapabilities,
},
),
).toThrow('Invalid keyring capabilities: Expected a plain object.');
});

it('throws if the caveat value has invalid fields', () => {
expect(() =>
keyringCaveatSpecifications[
SnapCaveatType.KeyringCapabilities
].validator?.({
type: SnapCaveatType.KeyringCapabilities,
value: { foo: 'bar' },
}),
).toThrow('Invalid keyring capabilities');
});

it('throws if scopes is missing', () => {
expect(() =>
keyringCaveatSpecifications[
SnapCaveatType.KeyringCapabilities
].validator?.({
type: SnapCaveatType.KeyringCapabilities,
value: { capabilities: { bip44: { derivePath: true } } },
}),
).toThrow('Invalid keyring capabilities');
});

it('does not throw for a valid capabilities value', () => {
expect(() =>
keyringCaveatSpecifications[
SnapCaveatType.KeyringCapabilities
].validator?.({
type: SnapCaveatType.KeyringCapabilities,
value: {
capabilities: {
scopes: ['bip122:000000000019d6689c085ae165831e93'],
bip44: {
derivePath: true,
deriveIndex: true,
deriveIndexRange: true,
discover: true,
},
privateKey: {
importFormats: [
{
encoding: 'base58',
type: 'bip122:p2pkh',
},
],
exportFormats: [{ encoding: 'base58' }],
},
},
},
}),
).not.toThrow();
});
});
});
99 changes: 84 additions & 15 deletions packages/snaps-rpc-methods/src/endowments/keyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ import type {
} from '@metamask/permission-controller';
import { PermissionType, SubjectType } from '@metamask/permission-controller';
import { rpcErrors } from '@metamask/rpc-errors';
import type { KeyringOrigins } from '@metamask/snaps-utils';
import { assertIsKeyringOrigins, SnapCaveatType } from '@metamask/snaps-utils';
import type {
KeyringCapabilities,
KeyringOrigins,
} from '@metamask/snaps-utils';
import {
assertIsKeyringCapabilities,
assertIsKeyringOrigins,
SnapCaveatType,
} from '@metamask/snaps-utils';
import type { Json, NonEmptyArray } from '@metamask/utils';
import { assert, hasProperty, isPlainObject } from '@metamask/utils';

Expand Down Expand Up @@ -45,11 +52,13 @@ const specificationBuilder: PermissionSpecificationBuilder<
targetName: permissionName,
allowedCaveats: [
SnapCaveatType.KeyringOrigin,
SnapCaveatType.KeyringCapabilities,
SnapCaveatType.MaxRequestTime,
],
endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null,
validator: createGenericPermissionValidator([
{ type: SnapCaveatType.KeyringOrigin },
{ type: SnapCaveatType.KeyringCapabilities, optional: true },
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Did we decide on behaviour if this is not defined? Since we are making it optional

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, omission of the capabilities is an implicit indication of using keyring v1

{ type: SnapCaveatType.MaxRequestTime, optional: true },
]),
subjectTypes: [SubjectType.Snap],
Expand All @@ -62,8 +71,8 @@ export const keyringEndowmentBuilder = Object.freeze({
} as const);

/**
* Validate the value of a caveat. This does not validate the type of the
* caveat itself, only the value of the caveat.
* Validate the value of a keyring origins caveat. This does not validate the
* type of the caveat itself, only the value of the caveat.
*
* @param caveat - The caveat to validate.
* @throws If the caveat value is invalid.
Expand All @@ -79,25 +88,62 @@ function validateCaveatOrigins(caveat: Caveat<string, any>) {
assertIsKeyringOrigins(value, rpcErrors.invalidParams);
}

/**
* Validate the value of a keyring capabilities caveat. This does not validate
* the type of the caveat itself, only the value of the caveat.
*
* @param caveat - The caveat to validate.
* @throws If the caveat value is invalid.
*/
function validateCaveatCapabilities(caveat: Caveat<string, any>) {
if (!hasProperty(caveat, 'value') || !isPlainObject(caveat.value)) {
throw rpcErrors.invalidParams({
message: 'Invalid keyring capabilities: Expected a plain object.',
});
}

const { value } = caveat;
assertIsKeyringCapabilities(value, rpcErrors.invalidParams);
}

/**
* Expected shape of the keyring endowment value from `initialPermissions`.
* The mapper assumes this shape for typing only; it does not validate.
* Invalid data is rejected when the permission is requested (see validator).
*/
type KeyringCaveatMapperInput = KeyringOrigins & {
capabilities?: KeyringCapabilities;
};

/**
* Map a raw value from the `initialPermissions` to a caveat specification.
* Note that this function does not do any validation, that's handled by the
* PermissionsController when the permission is requested.
* This function only maps: it does not validate. The permission validator
* runs when the permission is requested and will reject invalid caveats.
* We assume the manifest supplies a KeyringCaveatMapperInput-shaped value;
* the public signature accepts Json to satisfy CaveatMapperFunction.
*
* @param value - The raw value from the `initialPermissions`.
* @returns The caveat specification.
*/
export function getKeyringCaveatMapper(
value: Json,
): Pick<PermissionConstraint, 'caveats'> {
return {
caveats: [
{
type: SnapCaveatType.KeyringOrigin,
value,
},
],
};
const input = value as KeyringCaveatMapperInput;
const caveats: PermissionConstraint['caveats'] = [
{
type: SnapCaveatType.KeyringOrigin,
value: { allowedOrigins: input.allowedOrigins } as Json,
},
];

if (hasProperty(input, 'capabilities')) {
caveats.push({
type: SnapCaveatType.KeyringCapabilities,
value: { capabilities: input.capabilities } as Json,
});
}

return { caveats };
}

/**
Expand All @@ -120,12 +166,35 @@ export function getKeyringCaveatOrigins(
return caveat.value;
}

/**
* Getter function to get the {@link KeyringCapabilities} caveat value from a
* permission.
*
* @param permission - The permission to get the caveat value from.
* @returns The caveat value, or `null` if the permission does not have a
* {@link KeyringCapabilities} caveat.
*/
export function getKeyringCaveatCapabilities(
permission?: PermissionConstraint,
): KeyringCapabilities | null {
const caveat = permission?.caveats?.find(
(permCaveat) => permCaveat.type === SnapCaveatType.KeyringCapabilities,
) as Caveat<string, KeyringCapabilities> | undefined;

return caveat?.value ?? null;
}

export const keyringCaveatSpecifications: Record<
SnapCaveatType.KeyringOrigin,
SnapCaveatType.KeyringOrigin | SnapCaveatType.KeyringCapabilities,
CaveatSpecificationConstraint
> = {
[SnapCaveatType.KeyringOrigin]: Object.freeze({
type: SnapCaveatType.KeyringOrigin,
validator: (caveat: Caveat<string, any>) => validateCaveatOrigins(caveat),
}),
[SnapCaveatType.KeyringCapabilities]: Object.freeze({
type: SnapCaveatType.KeyringCapabilities,
validator: (caveat: Caveat<string, any>) =>
validateCaveatCapabilities(caveat),
}),
};
1 change: 1 addition & 0 deletions packages/snaps-rpc-methods/src/permissions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe('buildSnapEndowmentSpecifications', () => {
"endowment:keyring": {
"allowedCaveats": [
"keyringOrigin",
"keyringCapabilities",
"maxRequestTime",
],
"endowmentGetter": [Function],
Expand Down
4 changes: 4 additions & 0 deletions packages/snaps-utils/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `KeyringCapabilities` type and `KeyringCapabilitiesStruct` / `assertIsKeyringCapabilities` exports for the `endowment:keyring` capabilities caveat ([#3903](https://github.com/MetaMask/snaps/pull/3903))

## [12.1.1]

### Changed
Expand Down
Loading
Loading