feat: add keyring state migration framework#505
Conversation
ad96b17 to
0679d1f
Compare
03a6c5b to
13c9dd8
Compare
|
This PR is marked as stale because it has been open for 60 days with no activity. Please remove the stale label or leave a comment, or it will be closed in 14 days. |
|
This PR was closed due to no follow-up activity in the last 14 days. Thank you for your contributions. |
Add a versioned migration framework that allows keyrings to evolve their
serialized state format over time. Migrations run during deserialize()
and wrap state in a { version, data } envelope.
- KeyringMigration type with generic Output extends Json
- defineMigration() helper for compile-time type binding between
migrate and superstruct schema
- applyMigrations() runner with sequential version validation
- isVersionedState() type guard and getLatestVersion() helper
- Developer documentation with integration examples
- Add jest types to tsconfig.packages.json for IDE support
Replace manual type checks in isVersionedState with a VersionedStateStruct schema using superstruct. Derive VersionedState type from the struct. Use JsonStruct from @metamask/utils for the data field.
…raint - applyMigrations now returns MigrationResult with a migrated boolean so callers can detect when migrations ran and schedule a persist - defineMigration gains an optional inputSchema (Struct<Input>) that validates state before migrate is called; the Input type parameter removes the need for manual casts inside migrate - migrations.md updated: inputSchema example, migrated usage in deserialize, output-as-input validation chain note, and a new Idempotent migrations constraint
defineMigrations uses rest parameters to infer a tuple type, which lets
applyMigrations resolve data as the last migration's output without any
cast or as const:
const migrations = defineMigrations(
defineMigration<V1, V0>({ version: 1, ... }),
defineMigration<V2, V1>({ version: 2, ... }),
);
const { data } = await applyMigrations(state, migrations); // data: V2
Falls back to Json when the array is typed as KeyringMigration[].
Reorders MigrationStep and add()'s type parameters so Input reads first, matching the migrate(data: Input) => Output signature. Also drops the InternalStep alias, which was equivalent to bare MigrationStep once both type params defaulted to Json.
4870bfc to
caa5293
Compare
The @ts-expect-error-based type check for add()'s input-shape rejection lived in a runtime test file, but ts-jest doesn't execute the type-only assertion at runtime — it only validates it at compile time, and the "chain still builds" assertion tested nothing about the rejection. Move it to a dedicated .test-d.ts (this package's existing convention for type-only tests) and add coverage for input flow, inputSchema narrowing, and apply()'s resolved type. Also nest the outputSchema/inputSchema describes under apply(), since they exercise apply() like their sibling scenarios, and clean up a couple of test names for clarity.
caa5293 to
6152ce5
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6152ce5. Configure here.
A negative version passed the upper-bound check and made `steps.slice(version)` use negative-index semantics, silently applying the wrong subset of migration steps instead of throwing. Also fixes a docs formatting issue flagged by CI.
79c9242 to
08ccf0d
Compare
ccharly
left a comment
There was a problem hiding this comment.
LGTM! Left 2 open question on the typing tests, but after 2nd thoughts, I think the current approach might be better.
I'll let you decide 😄
Co-authored-by: Charly Chevalier <charly.chevalier@consensys.net>
…tion The suggestion applied in 1c2113b replaced the @ts-expect-error check that add() rejects a migrate whose input type doesn't match the previous step's output with an inference check, but didn't keep both. The comment above still claimed the rejection was tested, when it no longer was. Restore the rejection test alongside the new inference check.

Summary
@metamask/keyring-sdkfor evolving keyring serialized statedeserialize()and wrap state in a{ version, data }envelopedefineMigration()helper provides compile-time type binding betweenmigrateand superstructschemaTest plan
yarn workspace @metamask/keyring-sdk testpasses (85 tests, 100% coverage)yarn workspace @metamask/keyring-sdk buildsucceedsNote
Medium Risk
New library for transforming serialized keyring/vault state; bugs in future migrations could corrupt sensitive persisted data, though this PR only adds the framework and does not wire it into existing keyrings.
Overview
Adds a versioned migration framework to
@metamask/keyring-sdkso keyrings can evolve persisted serialized state duringdeserialize()without breaking older vaults.Callers build a forward-only chain with
createMigrations().add(...), where each step’s position is its version.apply(state)treats legacy unversioned JSON as version 0, runs only pending steps, and returns a{ version, data, migrated }envelope (VersionedState/MigrationResult). Optional superstructinputSchema/outputSchemavalidate step I/O;isVersionedStatedetects the envelope. The module is exported from the package entry and documented indocs/migrations.md(integration pattern forserialize()/deserialize()).Includes unit tests,
tsdtype tests for chained step typing, and an Unreleased changelog entry.Reviewed by Cursor Bugbot for commit 688b35e. Bugbot is set up for automated code reviews on this repo. Configure here.