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

@moveindustries/wallet-adapter-keyless

v0.1.0

Published

Movement keyless (Google sign-in) wallet adapter. Implements the [`@moveindustries/wallet-standard`](https://www.npmjs.com/package/@moveindustries/wallet-standard) so keyless accounts plug into [`@moveindustries/wallet-adapter-react`](https://www.npmjs.co

Readme

@moveindustries/wallet-adapter-keyless

Movement keyless (Google sign-in) wallet adapter. Implements the @moveindustries/wallet-standard so keyless accounts plug into @moveindustries/wallet-adapter-react the same way Petra / Motion / Nightly do — consumers call useWallet() and don't need to know whether the user signed in via Google or via a browser extension.

Wraps @moveindustries/keyless internally; users of this package never import that SDK directly.

Install

pnpm add @moveindustries/wallet-adapter-keyless \
         @moveindustries/wallet-adapter-react \
         @moveindustries/wallet-standard \
         @moveindustries/ts-sdk \
         @moveindustries/keyless

The adapter exposes features under the movement:* namespace (see Discoverability below). @moveindustries/ts-sdk is a peer of @moveindustries/keyless (the keyless SDK uses Movement SDK types like KeylessAccount internally).

Usage

Register once at app start, then use useWallet() from @moveindustries/wallet-adapter-react everywhere. The adapter shows up in the wallet picker as "Sign in with Google" alongside any other Movement-compatible wallets the user has installed.

// app/providers.tsx (Next.js)
'use client'

import { useEffect } from 'react'
import { registerKeylessWallet } from '@moveindustries/wallet-adapter-keyless'
import { MovementWalletAdapterProvider } from '@moveindustries/wallet-adapter-react'

export function Providers({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    registerKeylessWallet({
      proverUrl: '/api/prove',
      clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
      redirectUri: `${window.location.origin}/callback`,
      network: 'testnet',
    })
  }, [])

  return (
    <MovementWalletAdapterProvider autoConnect={false}>
      {children}
    </MovementWalletAdapterProvider>
  )
}

Then in any component:

'use client'

import { useWallet } from '@moveindustries/wallet-adapter-react'

export function ConnectButton() {
  const { connected, account, wallets, connect, disconnect } = useWallet()

  if (connected && account) {
    return (
      <div>
        <span>{account.address.toString()}</span>
        <button onClick={disconnect}>Disconnect</button>
      </div>
    )
  }

  return wallets.map((w) => (
    <button key={w.name} onClick={() => connect(w.name)}>
      {w.name}
    </button>
  ))
}

OAuth callback

The keyless flow uses a full-page redirect to Google, which is unusual for a wallet adapter (most use popups). The connect() call before the redirect never resolves — the page navigates away. After Google redirects back to your configured redirectUri, your /callback page must call connect() again so the adapter detects the id_token in the URL hash and completes the login.

// app/callback/page.tsx
'use client'

import { useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { useWallet } from '@moveindustries/wallet-adapter-react'
import { takeReturnTo } from '@moveindustries/wallet-adapter-keyless'

const KEYLESS_WALLET_NAME = 'Sign in with Google'

export default function CallbackPage() {
  const router = useRouter()
  const { connect } = useWallet()
  const ran = useRef(false)

  useEffect(() => {
    if (ran.current || !window.location.hash.includes('id_token')) return
    ran.current = true

    Promise.resolve(connect(KEYLESS_WALLET_NAME))
      .then(() => {
        router.replace(takeReturnTo() ?? '/')
      })
      .catch(() => router.replace('/?error=keyless_failed'))
  }, [connect, router])

  return <p>Signing you in…</p>
}

The Google OAuth redirect_uri you register in the Google Cloud Console must match redirectUri exactly — Google does string equality, not prefix matching. Add an entry per environment (e.g. http://localhost:3000/callback, https://yourdomain.com/callback).

The saved return path (keyless_return_to) is pathname + search only — the URL fragment is deliberately dropped. sessionStorage is script-readable and the fragment can carry secrets (e.g. a claim link's #sk=…), so it is never round-tripped through storage. If a flow needs fragment state to survive the redirect, carry it explicitly rather than relying on the return path.

Wallet standard features implemented

The standard:events row is the wallet-standard primitive — not namespaced.

| Namespace | Implementation | | --- | --- | | standard:events | Change-event emitter from @wallet-standard/core. Fires change with the new accounts array on connect / disconnect. Required — wallet-adapter libraries cache wallet.accounts and only re-read it when this fires; without it the consumer's useWallet() never sees connect succeed. | | movement:connect | Detects id_token in URL hash → completes login. Otherwise initiates OAuth redirect. | | movement:disconnect | Clears in-memory account, fires account-change event. | | movement:account | Returns address + public key. Throws if not connected. | | movement:network | Returns Movement testnet info (chain ID 250, Movement RPC). | | movement:signMessage | Builds the AIP-62 fullMessage (with optional address/application/chainId) and signs with the keyless account. Returns { signature, fullMessage, prefix: 'MOVEMENT', ... }, plus a non-standard type naming the signing scheme — MovementSignMessageOutput has no such field, and without it a keyless signature is indistinguishable from an ed25519 one. | | movement:signTransaction (v1.1) | Dispatches both v1.0 (positional (transaction, asFeePayer?)) and v1.1 ({ payload, sender?, feePayer?, gasUnitPrice?, maxGasAmount?, expirationSecondsFromNow? }) calling conventions on the same method. v1.0 returns an AccountAuthenticator; v1.1 returns { authenticator, rawTransaction }. Honors asFeePayer/feePayer to call signWithFeePayerAuthenticator instead of signTransactionWithAuthenticator. | | movement:signIn | Sign-In With Movement (AIP-116). Builds a structured SIWM message from { domain, nonce, statement?, uri?, version?, chainId?, issuedAt?, expirationTime?, notBefore?, requestId?, resources? }, signs it with the keyless account, returns { account, input, signature, type } where type is read from the account ('keyless' here, not 'ed25519' — a keyless signature does not verify under Ed25519 rules). domain must be the origin actually serving the page or the request is rejected unsigned; the same goes for an unparseable timestamp, an already-past expirationTime, or an expirationTime at or before notBefore (a future notBefore is allowed — pre-signing is legitimate). Lets dApps offer one-click "sign in with Google → authenticated session" with no on-chain transaction. | | movement:signAndSubmitTransaction | Builds the transaction via @moveindustries/ts-sdk, signs + submits, returns { hash }. | | movement:onAccountChange | Listener registry — fires on connect/disconnect. | | movement:onNetworkChange | Registered but never fires (testnet only). | | movement:changeNetwork | Always rejects — keyless is testnet-only. |

Discoverability

The adapter is discovered via getMovementWallets() from @moveindustries/wallet-standard.

The chains array is ['movement:testnet'] only — the keyless verification key is deployed on Movement testnet (chain ID 250).

Network

Testnet only. The keyless verification key is currently deployed on Movement testnet (Bardock, chain ID 250); mainnet support requires the chain's governance to publish a matching VK. The adapter advertises only movement:testnet in its chains array and rejects changeNetwork calls.

When mainnet support arrives, this becomes a constructor option:

registerKeylessWallet({ ..., network: 'mainnet' })

config.network selects the endpoint set in networks.ts. Widening the union without adding an entry there is a compile error, so a 'mainnet' request can never silently resolve to testnet.

Session lifetime

Keyless sessions are bounded by the ephemeral key's lifetime — about 1 hour from sign-in. After expiry, signing operations fail and the user must re-authenticate.

The connected account lives in memory only. A page reload (or tab close) drops it and the user signs in again — the OAuth round-trip re-derives the same on-chain address, so no state is lost. There is no sessionStorage rehydration today: restoring across a reload needs the ephemeral key that beginLogin stores internally, and the keyless SDK exposes no getter to retrieve it for completeLoginWithJwt.

There's no automatic silent re-authentication in the current version — it's deliberate, to keep the threat model simple. Hosts that want to surface "session expiring soon" warnings can read account.ephemeralKeyPair.expiryDateSecs and watch the clock client-side.

Accessing the raw account

The adapter holds the KeylessAccount internally and never hands it out. It carries the ephemeral secret key, the pepper, and the raw Google id_token, so there's no accessor for it — signing goes through the wallet-standard features instead. If a future flow needs a specific derived value, it should get that value, not the account.

Runtime reconfiguration

const adapter = registerKeylessWallet({ ... })
adapter.setConfig({ proverUrl: 'http://localhost:3001' })

If proverUrl or clientId changes while connected, the adapter auto-disconnects before recreating its internal SDK. A different prover means a different pepper, which means a different on-chain address — we don't want users unknowingly signing with a different wallet.

registerKeylessWallet is idempotent: calling it again with new config returns the same adapter instance and applies the config via setConfig.

Sender vs. fee-payer

signTransaction honors the fee-payer role on both calling conventions:

// v1.0 positional
signTransaction(rawTx, /* asFeePayer */ true)
// v1.1 with feePayer field
signTransaction({
  payload: { ... },
  feePayer: { address: AccountAddress.from('0xabc...'), publicKey },
})

When asFeePayer === true (v1.0) or when v1.1's feePayer.address matches the connected account, the adapter calls KeylessAccount.signWithFeePayerAuthenticator() instead of signTransactionWithAuthenticator().

Testing

Unit tests use Vitest with jsdom. The package's own test suite uses a hoisted module mock for @moveindustries/keyless and @moveindustries/ts-sdk — see src/adapter.test.ts for the pattern. Consumers writing their own tests against this adapter can use the same approach:

const mockKeyless = {
  beginLogin: vi.fn(),
  completeLogin: vi.fn(),
  logout: vi.fn(),
  hasSession: vi.fn(() => false),
  completeLoginWithJwt: vi.fn(),
}
vi.mock('@moveindustries/keyless', () => ({
  MovementKeyless: vi.fn(() => mockKeyless),
}))

The adapter is module-singleton via registerKeylessWallet; if your tests need a clean slate between cases, instantiate KeylessWalletAdapter directly rather than going through the registration helper.

Roadmap

Known gaps tracked for future versions:

  • secondarySigners in v1.1 signTransaction — currently ignored. Multi-agent flows that need multiple signers per transaction aren't supported yet.
  • Silent re-auth via prompt=none — extend session past EPK expiry without user interaction.
  • Mainnet support — once the keyless verification key is deployed on Movement mainnet.
  • Non-Google identity providers — Apple, Facebook, etc. Blocked on the keyless SDK (@moveindustries/keyless) supporting them upstream.

License

Apache-2.0.