@motion-key/server
v0.1.0
Published
Server-side library for Motion Key 2FA — route handlers, SDK, push/email providers, Prisma schema fragment.
Downloads
88
Maintainers
Readme
@motion-key/server
Server-side library for Motion Key 2FA — drop into any Node app (Next.js, Hono, Express, Bun…) to add iPhone-as-2nd-factor login.
What you get
- Route handlers for QR enrollment, account management, mobile push-token updates, and the iPhone-facing challenge approve/reject endpoints.
createMotionKey(config).sdk— call this from your existing login flow:isUserEnrolled(userId)— does this user have a phone enrolled?createChallenge({ userId, ipAddress?, userAgent? })sendChallengePush(challengeId, { browser, os })getChallengeStatus(challengeId)claimApprovedChallenge(challengeId)→ set your own session cookie on success
- Push providers:
APNsPushProvider(token auth, from env),ConsolePushProvider(dev fallback). ImplementPushProviderfor FCM / OneSignal / your own. - Email providers:
ResendEmailProvider,ConsoleEmailProvider. ImplementEmailProviderfor others. - Prisma schema fragment at
@motion-key/server/prisma/motion-key.prisma— copy it into yourprisma/schema/folder (multi-file schemas are Prisma 6+ GA).
Install
pnpm add @motion-key/server @motion-key/core @prisma/client
# Optional, only if you actually use APNs push:
pnpm add apnMinimum integration (Next.js example)
Add the schema fragment — copy
node_modules/@motion-key/server/prisma/motion-key.prismainto yourprisma/schema/, thenprisma migrate dev.Wire the library —
lib/motion-key.ts:import { apnsProviderFromEnv, ConsolePushProvider, createMotionKey, emailProviderFromEnv, } from "@motion-key/server"; import { prisma } from "./db"; import { getSession } from "./session"; // your existing cookie session import { publish } from "./websocket"; // your existing WS bridge (optional) export const motionKey = createMotionKey({ prisma, baseURL: process.env.APP_BASE_URL!, push: apnsProviderFromEnv() ?? new ConsolePushProvider(), email: emailProviderFromEnv(), getCurrentUser: async () => { const s = await getSession(); return s ? { id: s.userId, email: s.email } : null; }, publish, });Mount the catch-all —
app/api/motion-key/[...path]/route.ts:import { motionKey } from "@/lib/motion-key"; export const GET = motionKey.handlers.GET; export const POST = motionKey.handlers.POST; export const DELETE = motionKey.handlers.DELETE;Use the SDK from your login flow:
if (await motionKey.sdk.isUserEnrolled(user.id)) { const { challengeId } = await motionKey.sdk.createChallenge({ userId: user.id }); await motionKey.sdk.sendChallengePush(challengeId, { browser, os }); return Response.json({ mode: "2fa_required", challengeId }); } // else: set your session cookie and return logged_inPair with
@motion-key/reactfor the QR + waiting screens, or roll your own — the wire types are in@motion-key/core.
Endpoints exposed by the catch-all
POST /enroll/start (authed via getCurrentUser)
GET /enroll/info?token=... (unauthed, token-scoped)
POST /enroll/complete (unauthed, token-scoped)
GET /accounts (authed)
DELETE /accounts/:id (authed)
POST /mobile/push-token (x-account-id)
GET /mobile/challenges/pending (x-account-id)
GET /mobile/challenges/:id (x-account-id)
POST /mobile/challenges/approve (x-account-id + Ed25519 signature)
POST /mobile/challenges/reject (x-account-id)
GET /challenges/status?challengeId=...License
MIT
