Skip to content

feat: introduce CaveatType enum and normalize caveat type handling in builders#170

Open
mj-kiwi wants to merge 11 commits intomainfrom
feat/caveat-type-enum
Open

feat: introduce CaveatType enum and normalize caveat type handling in builders#170
mj-kiwi wants to merge 11 commits intomainfrom
feat/caveat-type-enum

Conversation

@mj-kiwi
Copy link
Contributor

@mj-kiwi mj-kiwi commented Feb 24, 2026

📝 Description

Apply the ScopeType enum pattern from PR #133 to caveat type parameters, allowing both enum references and string literals when defining caveats.

🔄 What Changed?

  • Added CaveatType enum to constants.ts with all 26 caveat enforcer function names
  • Created ConvertCaveatConfigsToInputs generic type to support flexible caveat type specification (enum or string)
  • Updated CaveatConfiguration type to use the flexible type conversion pattern
  • Enhanced CaveatBuilder.addCaveat() to normalize and accept both enum and string caveat types
  • Updated resolveCaveats() function to handle flexible caveat type normalization
  • Exported new types (CaveatType, CoreCaveatConfiguration, ConvertCaveatConfigsToInputs) in public API

🚀 Why?

  • Provides consistency with the ScopeType pattern established in PR chore: scope types #133
  • Improves API flexibility by allowing users to pass caveat types as either enum references or string literals
  • Maintains backward compatibility with existing string-based caveat type usage
  • Better type safety with enum references while preserving flexibility of string literals

🧪 How to Test?

  • Manual testing steps:
    1. Use CaveatType.AllowedMethods enum reference in caveatBuilder.addCaveat()
    2. Verify it works the same as string literal 'allowedMethods'
    3. Test both patterns in createDelegation caveats array
    4. Verify type completions show both patterns in IDE
  • Automated tests added/updated - Existing test suite passes
  • All existing tests pass ✓

⚠️ Breaking Changes

  • No breaking changes - This is fully backward compatible

📋 Checklist

  • Code follows the project's coding standards
  • Self-review completed
  • Documentation updated (if needed)
  • Tests added/updated - All existing tests pass
  • All CI checks pass ✓

🔗 Related Issues

Related to #145
Related to PR #133 (ScopeType enum pattern)
Related to PR #137 (enum flexibility pattern)

📚 Additional Notes

This implementation follows the same flexible type pattern established in PR #133 for ScopeType. Both enum references and string literals are supported for maximum flexibility:

// Using enum reference
caveatBuilder.addCaveat(CaveatType.AllowedMethods, { selectors: [...] })

// Using string literal (existing pattern)
caveatBuilder.addCaveat('allowedMethods', { selectors: [...] })

// In createDelegation caveats array
createDelegation({
  from: address,
  to: delegate,
  scope: scopeConfig,
  caveats: [
    { type: CaveatType.ValueLte, maxValue: 1000n },
    { type: 'erc20TransferAmount', tokenAddress: token, maxAmount: 100n }
  ]
})

Note

Medium Risk
Moderate risk: adds runtime validation of caveat type strings in resolveCaveats, which can turn previously generic errors into earlier failures and may reject non-core type configs passed in caveat arrays.

Overview
Introduces a new CaveatType enum for all core caveat/enforcer names and updates the core caveat map/types so caveat configs can be specified with either enum members or their string values.

resolveCaveats now validates config-style caveats ({ type, ... }) against CaveatType and improves error messages by reporting the failing array index; the enum is re-exported from caveatBuilder and the package root, and a new test suite exercises enum/string interchangeability across addCaveat, createDelegation, and resolveCaveats.

Written by Cursor Bugbot for commit 755b3e0. This will update automatically on new commits. Configure here.

@mj-kiwi mj-kiwi marked this pull request as ready for review February 24, 2026 06:34
@mj-kiwi mj-kiwi requested a review from a team as a code owner February 24, 2026 06:34
Copy link
Collaborator

@jeffsmale90 jeffsmale90 left a comment

Choose a reason for hiding this comment

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

This looks super nice!

Would it make sense to add test coverage? I think for the most part behaviour hasn't changed (as the enum is a string anyways), but maybe it would be nice to have tests showing how to pass an enum in each of the cases.

I hope typescript errors in the tests would cause the tests to fail, so we can at least assert the type resolutions work as expected.

…ad of returning a value, allowing TypeScript to narrow the type and eliminating the need for variable reassignment

- Remove unnecessary 'as string' type assertion in resolveCaveats.ts - the type is already correctly inferred
- Move CaveatType enum from constants.ts to coreCaveatBuilder.ts to better organize the caveat-specific type definition
- Remove ConvertCaveatConfigsToInputs and CoreCaveatConfiguration from public exports - only expose CaveatType and essential builder types
- Update imports across the codebase to reflect the new locations
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

export type ConvertCaveatConfigsToInputs<T extends { type: string }> =
T extends { type: string }
? Omit<T, 'type'> & { type: T['type'] | `${T['type']}` }
: never;
Copy link

Choose a reason for hiding this comment

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

Duplicated generic type utility for enum flexibility

Low Severity

ConvertCaveatConfigsToInputs is a near-duplicate of the existing ConvertScopeConfigsToInputs in scope/index.ts. Both have identical logic (Omit<T, 'type'> & { type: T['type'] | \${T['type']}` }), differing only in the constraint ({ type: string }vs{ type: ScopeType }). The caveat version with { type: string }` is strictly more general and could serve both use cases as a single shared utility type, avoiding duplicated logic that risks diverging over time.

Fix in Cursor Fix in Web

Copy link
Collaborator

Choose a reason for hiding this comment

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

If we can define one generic utility type that would be helpful - it doesn't get exported in the public API, so no need for backwards compatibility, we can just drop the unused util.

@mj-kiwi mj-kiwi marked this pull request as draft February 26, 2026 08:51
…lders

- Updated caveat builders to use the CaveatType enum instead of string literals for better type safety and maintainability.
- Removed the validateCaveatType function from utils as it is no longer necessary.
- Adjusted tests to reflect the changes in caveat type usage.
@mj-kiwi mj-kiwi marked this pull request as ready for review March 1, 2026 19:55
@mj-kiwi mj-kiwi requested a review from jeffsmale90 March 1, 2026 19:55
Copy link
Collaborator

@jeffsmale90 jeffsmale90 left a comment

Choose a reason for hiding this comment

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

Looking really good! It's definitely a struggle when changes touch every caveat type!

A couple minor comments - mostly I think we need to be able to:

caveatBuilder.add(CaveatType.AllowedCaveats, { ... }); // works 
caveatBuilder.add("allowedCaveats", { ...}); // doesn't work

export type ConvertCaveatConfigsToInputs<T extends { type: string }> =
T extends { type: string }
? Omit<T, 'type'> & { type: T['type'] | `${T['type']}` }
: never;
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we can define one generic utility type that would be helpful - it doesn't get exported in the public API, so no need for backwards compatibility, we can just drop the unused util.

const { type, ...config } = caveat;
scopeCaveatBuilder.addCaveat(type, config);
// Type assertion: addCaveat validates at runtime and throws if enforcer doesn't exist
(scopeCaveatBuilder.addCaveat as any)(type, config);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I was going to suggest scopeCaveatBuilder.addCaveat(type as CaveatType, config); but this is going to be a problem for callers as well - the CoreCaveatBuilder is exported publicly, and the caller expects to be able to pass either the enum or the string as the first parameter.

This problem is my fault, because I suggested using the enum as the key to the CoreCaveatMap, which makes the type parameter of the CoreCaveatBuilder constrained to just the enum.

If we revert the key of CoreCaveatMap back to the string, I guess it won't allow the caveat to be passed, so we need CoreCaveatBuilder's generic parameter of CaveatBuilder to be keyed on CaveatType | ${CaveatType}`

Copy link
Collaborator

Choose a reason for hiding this comment

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

We have some good coverage for adding caveats with the CaveatType enum, but none for addCaveat(string, config)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I left a comment further down indicating that we don't cover addCaveat(string, config) - I guess each of these individual caveat type tests should cover both?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants