@moveindustries/keyless
v0.1.1
Published
Keyless account SDK for Movement blockchain — social login via ZK proofs
Readme
@moveindustries/keyless
TypeScript SDK for keyless accounts on the Movement blockchain. Users sign transactions with their Google account; a zero-knowledge proof vouches for the JWT without revealing it on-chain.
Wraps the @moveindustries/ts-sdk KeylessAccount, hides the prover HTTP call and the Poseidon nonce commitment, and exposes two login flows: browser-redirect (for SPAs) and stored-JWT (for Chrome MV3 extensions).
Install
npm install @moveindustries/keyless @moveindustries/ts-sdk@moveindustries/ts-sdk is a peer dependency — the SDK doesn't bundle it so callers pick the version. This SDK is validated against 5.1.7, which is what package.json#devDependencies pins; any version in the >=5.1.0 range should work.
Prerequisite: a running prover
The SDK talks to the prover's POST /prove endpoint. For local development the prover + pepper-service run as docker compose up --build from the repo root — see the repo README.
For production you need a publicly reachable prover URL.
Browser redirect flow (single-page app)
import { MovementKeyless } from "@moveindustries/keyless";
const keyless = new MovementKeyless({
proverUrl: "http://localhost:3001",
clientId: "123456.apps.googleusercontent.com", // your Google OAuth client_id
redirectUri: "http://localhost:5173/callback", // must be registered with Google
});
// On "Sign in with Google" click: generates an ephemeral key, stores it in
// sessionStorage, redirects to Google's consent screen with a Poseidon nonce
// that commits to the key.
loginButton.onclick = () => keyless.beginLogin();
// On the redirect callback page — reads id_token from the URL hash,
// requests a ZK proof from the prover, returns a signing-ready account.
if (keyless.hasSession()) {
const account = await keyless.completeLogin();
const movement = keyless.getMovement();
const txn = await movement.transaction.build.simple({
sender: account.accountAddress,
data: {
function: "0x1::aptos_account::transfer",
functionArguments: [recipient, amount],
},
});
const pending = await movement.signAndSubmitTransaction({ signer: account, transaction: txn });
await movement.waitForTransaction({ transactionHash: pending.hash });
}getMovement() returns a Movement client already configured against Movement testnet (override with the fullnodeUrl config field).
Chrome extension flow (MV3)
Extensions can't do page redirects. Use the lower-level primitives instead and drive OAuth via chrome.identity.launchWebAuthFlow:
import { MovementKeyless } from "@moveindustries/keyless";
const keyless = new MovementKeyless({
proverUrl: "http://localhost:3001",
clientId: GOOGLE_CLIENT_ID,
redirectUri: chrome.identity.getRedirectURL(), // https://<ext-id>.chromiumapp.org/
});
// 1. Build the OAuth URL and keep the ephemeral key yourself.
const { url, ephemeralKey } = keyless.buildOAuthUrl();
await chrome.storage.local.set({ epk: ephemeralKey });
// 2. Drive OAuth — Chrome returns the redirect URL with #id_token=…
const responseUrl = await chrome.identity.launchWebAuthFlow({ url, interactive: true });
const jwt = new URL(responseUrl).hash
.substring(1)
.split("&")
.map((p) => p.split("="))
.find(([k]) => k === "id_token")?.[1];
// 3. Ask the prover for a proof.
const { account, pepper, address } = await keyless.completeLoginWithJwt(jwt!, ephemeralKey);
// `pepper` + `address` are suitable for the wallet's encrypted vault so the
// user can be re-authenticated on unlock without losing their on-chain address.Ephemeral keys expire after 1 hour; the wallet should watch the expiry and silently re-authenticate (keyless.buildOAuthUrl({ prompt: "none" })) before then.
Configuration
interface MovementKeylessConfig {
proverUrl: string; // POST {proverUrl}/prove
clientId: string; // Google OAuth client_id; becomes `aud` in the JWT
redirectUri: string; // registered Google redirect URI
fullnodeUrl?: string; // default: https://testnet.movementnetwork.xyz/v1
uidKey?: string; // default: "sub"
}clientId is baked into every derived keyless address as the aud claim — changing it changes all addresses.
API surface
| Method | Use case |
|---|---|
| beginLogin() | SPA: redirect to Google OAuth |
| completeLogin() | SPA: on the callback page, returns a KeylessAccount |
| buildOAuthUrl({ prompt? }) | Build the OAuth URL without navigating (extension/native) |
| completeLoginWithJwt(jwt, epk) | Complete login given a JWT obtained externally |
| hasSession() | Check whether sessionStorage holds an ephemeral key |
| logout() | Clear the in-memory session |
| getMovement() | Preconfigured Movement client for submitting transactions |
Validation done locally
completeLogin / completeLoginWithJwt reject the JWT and refuse to attribute the session if any of these client-side checks fail (independent of the prover and chain, defence-in-depth):
- Nonce match.
payload.noncemust equal the Poseidon commitment that was placed in the OAuth URL — prevents a swapped JWT from binding to a different ephemeral key. - Aud match.
payload.audmust equal the configuredclientId— catches a JWT issued to a different OAuth client (wrong env, malicious page that triggered its own OAuth flow). - Exp in the future. Refuses an expired JWT before paying the prover round-trip.
- Address derivation match. After the prover responds, the SDK re-derives the on-chain address locally from the JWT claims + the prover's pepper via
KeylessPublicKey.fromJwtAndPepper(...).authKey().derivedAddress(). If the prover's returned address disagrees, the call rejects. The returnedKeylessLoginResult.addressis always the locally-derived value, so a buggy or compromised prover cannot mis-attribute a proof to a different account.
Development
npm install
npm test # vitest
npm run build # emits dist/Tests live under src/__tests__/. keyless.test.ts exercises the full login flow against a mocked prover and is a good companion to this README.
Open issues
- On-chain VK alignment. Movement testnet's deployed Groth16 verification key matches the
circuit-v4.0.0ceremony (deployed 2026-05-08); the prover must use that zkey. Proofs generated against any other zkey (e.g.circuit-v1.0.1,circuit-v1.2.0,circuit-v5.0.0) will be accepted by the SDK but rejected by the chain. - Prover hosting. No public prover instance yet — consumers need to run their own (docker compose, as above).
