npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@keelcodes/migrate

v0.1.0

Published

Lazy, dual-module migration tooling for ERC-7579 session validators: version routing, a migration state machine and the silent-AA24 probe.

Readme

@keelcodes/migrate

Lazy, dual-module migration tooling for ERC-7579 session validators.

The account is already an ERC-7579 account — only the module changes. That means no account migration, no key rotation and no asset movement: the account address, its owner and its balances are untouched. The operator needs exactly three things, and this package is those three:

  • Version routing — session records carry a module_version; new sessions go to the new module while in-flight ones keep their old encoding.
  • A migration state machine — not-migrated → migrating → migrated, with exception for a failed probe and explicit retry / rollback.
  • The probe — the two on-chain assertions that catch a silent, AA24-class no-op install before traffic is cut over.

It has no runtime dependencies (only vitest for its own tests), so it can be dropped into a relay, a migration script or a CI job.

Why lazy

Rewriting every session record up front means a flag day, a maintenance window and a rollback plan you will probably never test. Instead:

  • A record written before the version column existed has no moduleVersion. It routes to the registry's default generation and keeps working until it expires or is revoked — no backfill required.
  • A record that does carry a version routes to that exact generation, so old encodings keep validating while the fleet moves over.
  • The cut-over, when it happens, is on the session-rotation path: revoke or rotate is the migration.

Routing

import { createModuleRegistry, routeSession } from '@keelcodes/migrate';

const registry = createModuleRegistry([
  // oldest first — the last descriptor becomes the default
  { version: 'v1', address: '0x…', encoding: 'payload', multiSession: false, enforcedLimits: false },
  { version: 'v2', address: '0x…', encoding: 'onchain', multiSession: true,  enforcedLimits: true  },
]);

routeSession(registry, { id: 's-1' });                       // → v2 (default, legacy record)
routeSession(registry, { id: 's-2', moduleVersion: 'v1' });  // → v1 (in-flight)
routeSession(registry, { id: 's-3', moduleVersion: 'v9' });  // throws unknown-module-version

ModuleDescriptor records what the router must know about one generation: its encoding (onchain on OxaChain, payload elsewhere), whether it allows more than one session per account (multiSession) and whether spend limits are enforced on-chain rather than off-chain only (enforcedLimits).

The probe (silent AA24)

Installing a module and having it enforce something are two different facts. An install can register a module on the account (isModuleInstalled == true) while the module's own onInstall silently no-ops — a mismatched initData layout, or a value it ignored. Nothing reverts, the account looks configured, and the module enforces nothing.

The probe therefore asserts both halves:

| Half | Reads | |---|---| | isModuleInstalled | the account: isModuleInstalled(moduleTypeId, module, 0x) | | isModuleInitialized | the module: IModule.isInitialized(account) |

import { probeModule } from '@keelcodes/migrate';

const result = await probeModule(probe, { account, module, moduleTypeId: 4n });
// { installed: true, initialized: false, ok: false,
//   reason: 'module is installed but isInitialized() is false — silent no-op install (AA24-class)' }

ok is only true when both hold. ModuleProbe is a port — back it with viem in production and a fake in tests.

Rehearsal on a fork

Rehearse the probe against a real chain before pointing it at production, with no credentials and no real transactions. @keelcodes/adapters ships a viem-backed ModuleProbe whose shape matches this package's port, so the two compose without migrate taking on a viem dependency:

import { createPublicClient, http } from 'viem';
import { bsc } from 'viem/chains';
import { createViemModuleProbe } from '@keelcodes/adapters';
import { probeModule } from '@keelcodes/migrate';

const client = createPublicClient({ chain: bsc, transport: http() });
const probe = createViemModuleProbe(client); // structurally a ModuleProbe

await probeModule(probe, { account, module, moduleTypeId: 4n });

createViemModuleProbe reads isModuleInstalled from the account and isInitialized from the module; a module that does not implement isInitialized resolves to false rather than throwing, so the probe returns the AA24 reason above instead of crashing.

The adapters package also ships a drill that forks BSC, validates the real address book and runs both the positive and negative probe paths against a freshly deployed hook — writing only to the fork:

pnpm --filter @keelcodes/adapters build      # the drill loads the built package
pnpm --filter @keelcodes/adapters fork-drill

BSC_RPC_URL overrides the default fork source (https://bsc-dataseed.bnbchain.org); anvil listens on port 8546. The drill exits non-zero if any real address has lost its code or an assertion fails, and always tears the fork down.

The state machine

not-migrated --begin--> migrating --confirm--> migrated
                             |  ^
                           fail |  | retry
                             v  |
                         exception --rollback--> not-migrated
  • Only begin is reachable from not-migrated.
  • migrated is terminal: once both probes pass, the cut-over is one-way.
  • A failed probe lands in exception, never in migrated — the account keeps running the old module until an operator retrys or rollbacks.
  • confirm requires a passing probe result; confirming without one throws probe-failed.

applyMigrationAction is a pure reducer (never mutates); migrateSession runs one whole step — begin, probe, then confirm on success or fail into exception:

import { migrateSession } from '@keelcodes/migrate';

const next = await migrateSession({
  record: { account, sessionId, fromVersion: 'v1', toVersion: 'v2', status: 'not-migrated', updatedAt },
  probe,
  probeArgs: { account, module: v2Module, moduleTypeId: 4n },
  at: new Date().toISOString(),
});
// status: 'migrated' on a clean probe, 'exception' otherwise

Errors

Every failure is a MigrationError with a stable code:

| code | when | |---|---| | unknown-module-version | a session references a version the registry does not know | | duplicate-module-version | two descriptors share a version | | no-default-version | the registry is empty, or its default is unregistered | | invalid-transition | the action is not reachable from the current status | | probe-failed | confirm was called with a missing or failing probe |

Develop

pnpm --filter @keelcodes/migrate test
pnpm --filter @keelcodes/migrate typecheck

The migration plan itself (dual-module, grey release, rollback) is in docs/KEEL_PLAN.md §7.