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

@tuwaio/siwx-server

v0.2.2

Published

Layer 2 (L2) of the TUWA Ecosystem. Backend utilities for @tuwaio/siwx. Parses, validates, and verifies CAIP-122 payloads on Node.js or Edge runtimes.

Readme

@tuwaio/siwx-server

NPM Version License

Backend utilities for @tuwaio/siwx (L2). Server-side CAIP-122 payload verification, durable session store abstractions, authenticated stateless demo handlers, and single-use nonce generation. Fully backend-agnostic.


🏛️ Core Capabilities

  • verifySiwxPayload(): Primary server entry point. Parses the CAIP-122 message, enforces verification policy (domain, URI, allowed chains, timing windows), checks nonce replay, and dynamically verifies EVM (siwx-evm) or Solana (siwx-solana) signatures.
  • getSiwxServerSession(): Unified server helper to resolve and verify active sessions across Next.js Server Actions, Route Handlers, and Web APIs from durable stores (sessionStore) or stateless demo tokens (signingSecret).
  • createSiwxApiHandler(): Production durable session handler for Next.js App Router. Requires persistent SiwxSessionStore and SiwxNonceStore (Redis, PostgreSQL, etc.) and uses opaque session IDs in HttpOnly cookies.
  • createStatelessDemoSiwxHandler(): Authenticated HMAC-SHA256 session handler for zero-infrastructure demonstration environments and rapid prototyping.
  • signStatelessDemoSession() / verifyStatelessDemoSession(): Web Crypto API constant-time HMAC signing and verification for demo tokens.
  • MemorySiwxSessionStore / MemorySiwxNonceStore: In-memory stores explicitly designated for local development and testing (fails closed in production).

💾 Installation

pnpm add @tuwaio/siwx-server @tuwaio/siwx-core
# + chain adapters (dynamically imported at runtime):
pnpm add @tuwaio/siwx-evm @tuwaio/siwx-solana

🛡️ Architecture Profiles

1. Durable Profile (Production Standard)

For production applications with user accounts, persistent logins, session revocation, and multi-replica horizontal scaling.

Step 1: Implement or Configure Auth Stores (lib/authStores.ts)

// lib/authStores.ts
import type { SiwxNonceStore, SiwxSession, SiwxSessionRecord, SiwxSessionStore } from '@tuwaio/siwx-server';
import { generateServerNonce } from '@tuwaio/siwx-server';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');

export const sessionStore: SiwxSessionStore = {
  async create({ session, ttlSeconds }: { session: SiwxSession; ttlSeconds: number }): Promise<SiwxSessionRecord> {
    const id = generateServerNonce();
    const createdAt = Date.now();
    const expiresAt = createdAt + ttlSeconds * 1000;
    const record: SiwxSessionRecord = { id, session, createdAt, expiresAt };

    await redis.set(`siwx:session:${id}`, JSON.stringify(record), 'EX', ttlSeconds);
    return record;
  },

  async get(id: string): Promise<SiwxSessionRecord | null> {
    const data = await redis.get(`siwx:session:${id}`);
    return data ? JSON.parse(data) : null;
  },

  async bindSubject(id: string, subjectId: string): Promise<boolean> {
    const record = await this.get(id);
    if (!record) return false;
    record.subjectId = subjectId;
    const ttl = Math.max(1, Math.floor((record.expiresAt - Date.now()) / 1000));
    await redis.set(`siwx:session:${id}`, JSON.stringify(record), 'EX', ttl);
    return true;
  },

  async revoke(id: string): Promise<void> {
    await redis.del(`siwx:session:${id}`);
  },
};

export const nonceStore: SiwxNonceStore = {
  async issue({ nonce, ttlSeconds }: { nonce: string; ttlSeconds: number }): Promise<void> {
    await redis.set(`siwx:nonce:${nonce}`, '1', 'EX', ttlSeconds);
  },

  async consume({ nonce }: { nonce: string }): Promise<boolean> {
    // Atomic single-use consumption via GETDEL
    const value = await redis.getdel(`siwx:nonce:${nonce}`);
    return value !== null;
  },
};

Alternative: In-Memory Stores for Local Testing / Prototyping (No Redis Needed)

For local development or testing without spinning up Redis, you can use the built-in in-memory stores directly from @tuwaio/siwx-server:

// lib/authStores.dev.ts (Zero Dependencies / In-Memory)
import { MemorySiwxNonceStore, MemorySiwxSessionStore } from '@tuwaio/siwx-server';

// Built-in in-memory stores for local testing (fails closed in production by default)
export const sessionStore = new MemorySiwxSessionStore();
export const nonceStore = new MemorySiwxNonceStore();

Or write a custom Map-based storage adapter without external dependencies:

// lib/authStores.memory.ts (Custom Zero-Dependency In-Memory Store)
import type { SiwxNonceStore, SiwxSession, SiwxSessionRecord, SiwxSessionStore } from '@tuwaio/siwx-server';
import { generateServerNonce } from '@tuwaio/siwx-server';

const sessionMap = new Map<string, SiwxSessionRecord>();
const nonceMap = new Map<string, number>();

export const sessionStore: SiwxSessionStore = {
  async create({ session, ttlSeconds }: { session: SiwxSession; ttlSeconds: number }): Promise<SiwxSessionRecord> {
    const id = generateServerNonce();
    const createdAt = Date.now();
    const expiresAt = createdAt + ttlSeconds * 1000;
    const record: SiwxSessionRecord = { id, session, createdAt, expiresAt };
    sessionMap.set(id, record);
    return record;
  },

  async get(id: string): Promise<SiwxSessionRecord | null> {
    const record = sessionMap.get(id);
    if (!record || record.expiresAt < Date.now()) {
      sessionMap.delete(id);
      return null;
    }
    return record;
  },

  async bindSubject(id: string, subjectId: string): Promise<boolean> {
    const record = await this.get(id);
    if (!record) return false;
    record.subjectId = subjectId;
    return true;
  },

  async revoke(id: string): Promise<void> {
    sessionMap.delete(id);
  },
};

export const nonceStore: SiwxNonceStore = {
  async issue({ nonce, ttlSeconds }: { nonce: string; ttlSeconds: number }): Promise<void> {
    nonceMap.set(nonce, Date.now() + ttlSeconds * 1000);
  },

  async consume({ nonce }: { nonce: string }): Promise<boolean> {
    const expiresAt = nonceMap.get(nonce);
    if (!expiresAt || expiresAt < Date.now()) {
      nonceMap.delete(nonce);
      return false;
    }
    nonceMap.delete(nonce); // Atomic single-use consumption
    return true;
  },
};

Step 2: Configure the Route Handler (app/api/siwx/[...siwx]/route.ts)

// app/api/siwx/[...siwx]/route.ts
import { createSiwxApiHandler } from '@tuwaio/siwx-server/next';
import { nonceStore, sessionStore } from '@/lib/authStores';

const handler = createSiwxApiHandler({
  sessionStore,
  nonceStore,
  policy: {
    expectedDomain: 'app.tuwa.io',
    expectedUri: 'https://app.tuwa.io',
    allowedChainIds: ['eip155:1', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpK'],
    maxIssuedAtAgeSeconds: 300,
  },
  cookieOptions: {
    name: 'siwx-session-v2',
    secure: process.env.NODE_ENV === 'production',
  },
});

export const { GET, POST, DELETE } = handler;

2. Stateless Demo Profile (Zero-Infrastructure Demonstration & Prototyping)

For sandbox environments, developer playgrounds, integration tests, or demonstration websites running without a backing database or Redis instance.

// app/api/siwx/[...siwx]/route.ts
import { createStatelessDemoSiwxHandler } from '@tuwaio/siwx-server/next';

const handler = createStatelessDemoSiwxHandler({
  signingSecret: process.env.SIWX_DEMO_SIGNING_SECRET!, // Minimum 32 characters
  policy: {
    expectedDomain: 'demo.tuwa.io',
    requireExpirationTime: true,
    maxIssuedAtAgeSeconds: 300,
    maxSessionLifetimeSeconds: 1800, // 30 minutes max session
  },
  cookieOptions: {
    name: 'siwx-demo-session',
    secure: process.env.NODE_ENV === 'production',
  },
});

export const { GET, POST, DELETE } = handler;

Warning: Demo mode works without a database or Redis by using a short-lived stateless session. Real projects with user accounts, persistent login, logout/revoke, multi-replica deployment, and strong protection against session replay MUST connect Redis, PostgreSQL, SQLite, or another durable storage adapter.


3. Server Actions & Route Session Resolution (getSiwxServerSession)

Use getSiwxServerSession in Next.js Server Actions or server-side routes to securely resolve authenticated sessions without accepting client-provided session parameters:

// app/actions/myAction.ts
'use server';

import { cookies } from 'next/headers';
import { getSiwxServerSession } from '@tuwaio/siwx-server';
import { isSessionMatchingTarget } from '@tuwaio/siwx-core';
import { sessionStore } from '@/lib/authStores';

export async function myServerAction(targetAddress: string, data: Record<string, unknown>) {
  // 1. Resolve and verify session from HTTP-Only cookie server-side
  const session = await getSiwxServerSession({
    cookieSource: await cookies(),
    sessionStore, // Or signingSecret for demo profile
  });

  if (!session) {
    throw new Error('Unauthorized: No active session.');
  }

  // 2. Enforce cryptographic subject/address binding
  if (!isSessionMatchingTarget(session, targetAddress)) {
    throw new Error('Forbidden: Wallet address mismatch.');
  }

  // 3. Execute privileged server business logic
  return { success: true };
}

🚀 Low-Level Manual Verification API

For custom controllers (NestJS, Fastify, Express, Cloudflare Workers):

import { toSession, verifySiwxPayload } from '@tuwaio/siwx-server';

export async function handleVerify(request: Request) {
  const { message, signature } = await request.json();

  const result = await verifySiwxPayload(
    { message, signature },
    {
      policy: {
        expectedDomain: 'app.tuwa.io',
        allowedChainIds: ['eip155:1'],
      },
    },
  );

  if (!result.success || !result.data) {
    return new Response(JSON.stringify({ error: result.error }), { status: 401 });
  }

  const session = toSession(result.data);
  return new Response(JSON.stringify(session), { status: 200 });
}

📦 Store Interfaces

SiwxSessionStore

export interface SiwxSessionStore {
  create(input: { session: SiwxSession; ttlSeconds: number }): Promise<SiwxSessionRecord>;
  get(id: string): Promise<SiwxSessionRecord | null>;
  bindSubject(id: string, subjectId: string): Promise<boolean>;
  revoke(id: string): Promise<void>;
}

SiwxNonceStore

export interface SiwxNonceStore {
  issue(input: { nonce: string; ttlSeconds: number }): Promise<void>;
  consume(input: { nonce: string }): Promise<boolean>;
}

SiwxSessionRecord

export interface SiwxSessionRecord {
  id: string;
  session: SiwxSession;
  subjectId?: string;
  createdAt: number;
  expiresAt: number;
}

📄 License

Licensed under the Apache-2.0 License. See the LICENSE file for details.