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

better-near-auth

v1.5.0

Published

Sign in with NEAR (SIWN) plugin for Better Auth

Downloads

2,963

Readme

This Better Auth plugin enables secure authentication via NEAR wallets following NEP-413 and adds a built-in NEP-366 delegate action relayer so authenticated users can call on-chain contracts gaslessly. It uses near-kit for RPC queries and transaction broadcasting, and @hot-labs/near-connect for wallet connection.

Features

  • SIWN authentication — wallet-based sign-in with automatic single-step/two-step flow detection
  • Gasless relay — server relays signed delegate actions on-chain, paying gas from a relayer account
  • Ephemeral relayer keypair — auto-generated ED25519 keypair on first startup, private key encrypted with AES-256-GCM in the database, persists across restarts
  • Profile integration — near-kit profile lookup primary, NEAR Social fallback

Installation

  1. Install the package
npm install better-near-auth
  1. Add the SIWN plugin to your auth configuration:

    import { betterAuth } from "better-auth";
    import { siwn } from "better-near-auth";
    
    export const auth = betterAuth({
        database: drizzleAdapter(db, {
          // db configuration
        }),
        plugins: [
            siwn({
                recipient: "myapp.com",
    
                // Optional: enable gasless relay
                relayer: {
                  whitelistedContracts: ["myapp.near"],
                },
            }),
        ],
    });
  2. Generate the schema to add the necessary fields and tables to the database.

npx @better-auth/cli generate
  1. Add the Client Plugin

    import { createAuthClient } from "better-auth/client";
    import { siwnClient } from "better-near-auth/client";
    
    export const authClient = createAuthClient({
        plugins: [
            siwnClient({
                recipient: "myapp.com",
                networkId: "mainnet",
            })
        ],
    });

Usage

Sign In with NEAR

The signIn.near() method automatically detects wallet capabilities and uses the best available flow:

import { authClient } from "./auth-client";
import { useState } from "react";

export function LoginButton() {
  const { data: session } = authClient.useSession();
  const [isSigningIn, setIsSigningIn] = useState(false);

  if (session) {
    return (
      <div>
        <p>Welcome, {session.user.name}!</p>
        <button onClick={() => authClient.near.disconnect()}>Sign out</button>
      </div>
    );
  }

  const handleSignIn = async () => {
    setIsSigningIn(true);
    await authClient.signIn.near({
      onSuccess: () => {
        setIsSigningIn(false);
      },
      onError: (error) => {
        setIsSigningIn(false);
        console.error("Sign in failed:", error.message);
      },
    });
  };

  return (
    <button onClick={handleSignIn} disabled={isSigningIn}>
      {isSigningIn ? "Signing in..." : "Sign in with NEAR"}
    </button>
  );
}

Supported wallets: HOT Wallet, Meteor Wallet, Intear Wallet, MyNearWallet, and more.

Gasless Relay

Once the relayer is configured on the server, authenticated users can call on-chain contracts without paying gas:

// 1. Build a signed delegate action using the wallet's FAK
import { Gas } from "near-kit";

const signedAction = await authClient.near.buildSignedDelegateAction(
  "myapp.near",
  (builder, receiverId) => builder.functionCall(receiverId, "some_method", { key: "value" }, {
    gas: Gas.Tgas(30),
    attachedDeposit: BigInt(0),
  })
);

// 2. Relay it — the server pays gas
const result = await authClient.near.relayTransaction({
  payload: signedAction,
});

console.log("Tx hash:", result.txHash);

// 3. Check status
const status = await authClient.near.getRelayStatus(result.txHash);

Profile Access

const myProfile = await authClient.near.getProfile();
const aliceProfile = await authClient.near.getProfile("alice.near");

Wallet Management

const accountId = authClient.near.getAccountId();
const { data } = await authClient.near.listAccounts();
const activeAccount = data?.activeAccount;
const availableAccounts = data?.availableAccounts ?? [];
await authClient.near.setPrimaryAccount({ accountId: "alice.near", network: "mainnet" });
await authClient.near.disconnect();

Configuration Options

Server Options

| Option | Type | Default | Description | |---|---|---|---| | recipient | string | — | NEP-413 recipient identifier (required) | | requireFullAccessKey | boolean | false | Require full access keys | | getNonce | () => Promise<Uint8Array> | — | Custom nonce generation | | getProfile | (accountId: string) => Promise<Profile \| null> | — | Custom profile lookup | | validateLimitedAccessKey | (args) => Promise<boolean> | — | Validate FAK when requireFullAccessKey is false | | apiKey | string | process.env.FASTNEAR_API_KEY | API key for RPC | | rpcUrl | string | — | Custom RPC URL (e.g., sandbox, private node) | | relayer | RelayerConfig | — | Relayer configuration (see below) |

Relayer Configuration

| Option | Type | Default | Description | |---|---|---|---| | accountId | string | — | Named relayer account (explicit mode) | | privateKey | string | — | Base64 private key (explicit mode) | | whitelistedContracts | string[] | — | Restrict relay to these contracts | | maxGasPerTransaction | string | — | Max gas per relayed tx | | maxDepositPerTransaction | string | — | Max deposit per relayed tx |

When accountId and privateKey are omitted, the relayer starts in ephemeral mode: an ED25519 keypair is generated on first startup, the implicit account ID is derived from the public key, and the private key is encrypted with AES-256-GCM (using BETTER_AUTH_SECRET as KEK via HKDF-SHA256) and stored in the database. The same keypair is recovered on restart.

Client Options

| Option | Type | Default | Description | |---|---|---|---| | recipient | string | — | NEP-413 recipient (must match server) | | networkId | "mainnet" \| "testnet" | "mainnet" | NEAR network |

Schema

nearAccount

| Field | Type | Description | |---|---|---| | id | string | Primary key | | userId | string | → user.id | | accountId | string | NEAR account ID | | network | string | mainnet/testnet | | publicKey | string | Associated public key | | isPrimary | boolean | User's primary account | | createdAt | date | |

relayedTransaction

| Field | Type | Description | |---|---|---| | userId | string | → user.id | | txHash | string | On-chain tx hash | | senderId | string | Delegate action sender | | receiverId | string | Contract called | | status | string | pending/completed/failed | | gasUsed | string | Gas consumed | | createdAt | date | |

relayerKey

| Field | Type | Description | |---|---|---| | id | string | Singleton per network | | accountId | string | Implicit NEAR account ID | | encryptedPrivateKey | string | AES-256-GCM encrypted, base64 | | iv | string | Initialization vector, base64 | | publicKey | string | ed25519:base64 format | | network | string | mainnet/testnet | | createdAt | date | | | lastUsedAt | date | Updated on each relay |

API Reference

Client Actions — authClient.near

SIWN

  • nonce(params) — Request a nonce from the server
  • verify(params) — Verify an auth token with the server
  • getProfile(accountId?) — Get user profile (near-kit profile lookup → NEAR Social fallback)
  • getAccountId() — Currently connected account ID
  • getState() — Current wallet state
  • disconnect() — Disconnect wallet and clear cached data
  • link(callbacks?) — Link a NEAR account to the current session
  • unlink(params) — Unlink a NEAR account
  • listAccounts() — List linked NEAR accounts with activeAccount and availableAccounts
  • setPrimaryAccount(params) — Select the active NEAR account for the session user

Relay

  • buildSignedDelegateAction(receiverId, buildActions) — Build + sign a delegate action via wallet FAK
  • relayTransaction({ payload }) — Submit a signed delegate action to the relayer
  • getRelayStatus(txHash) — Check relayed transaction status
  • getRelayerInfo() — Get relayer account info, mode, and balance
  • relayHistory() — List relayed transactions for current user

authClient.signIn

  • near(callbacks?) — Connect wallet, sign message, and authenticate (single popup)

Callback Interface

interface AuthCallbacks {
  onSuccess?: () => void;
  onError?: (error: Error & { status?: number; code?: string }) => void;
}

Error Codes

| Code | Description | |---|---| | UNAUTHORIZED_NONCE_REPLAY | Nonce already used (replay attack detected) | | UNAUTHORIZED | Generic auth failure (invalid signature, account mismatch, etc.) |

Server Endpoints

| Method | Path | Description | |---|---|---| | POST | /near/nonce | Generate nonce for signing | | POST | /near/verify | Verify NEP-413 signature, create session | | POST | /near/profile | Get NEAR profile | | POST | /near/link-account | Link NEAR account to session | | POST | /near/unlink-account | Unlink NEAR account | | GET | /near/list-accounts | List linked NEAR accounts | | POST | /near/relay | Relay a signed delegate action on-chain | | GET | /near/relay-status/:txHash | Check relayed transaction status | | GET | /near/relayer-info | Get relayer accountId, mode, balance | | GET | /near/relay-history | List relayed transactions for current user | | POST | /near/view | Server-side read-only contract call (authenticated) |

Advanced Configuration

import { betterAuth } from "better-auth";
import { siwn } from "better-near-auth";
import { generateNonce } from "near-kit";

const usedNonces = new Set<string>();

export const auth = betterAuth({
  plugins: [
    siwn({
      recipient: "myapp.com",
      requireFullAccessKey: false,

      getNonce: async () => generateNonce(),

      getProfile: async (accountId) => {
        try {
          const res = await fetch(`https://api.myapp.com/profiles/${accountId}`);
          if (res.ok) {
            const p = await res.json();
            return { name: p.displayName, description: p.bio, image: { url: p.avatar } };
          }
        } catch {}
        return null;
      },

      validateLimitedAccessKey: async ({ accountId, publicKey, recipient }) => {
        const allowed = ["myapp.near", "social.near"];
        return recipient ? allowed.includes(recipient) : true;
      },

      apiKey: process.env.FASTNEAR_API_KEY,

      relayer: {
        accountId: "relayer.myapp.near",
        privateKey: process.env.RELAYER_PRIVATE_KEY,
        whitelistedContracts: ["myapp.near"],
        maxGasPerTransaction: "300000000000000",
        maxDepositPerTransaction: "0",
      },
    }),
  ],
});

Network Support

The plugin detects the network from the account ID:

  • Accounts ending with .testnet → testnet
  • All other accounts → mainnet

Security

NEP-413 Compliance

  • Proper nonce handling prevents replay attacks
  • Message format and recipient validation
  • 15-minute server-side nonce expiration with DB replay detection

Relayer Key Security

  • Ephemeral private key encrypted at rest with AES-256-GCM
  • KEK derived from BETTER_AUTH_SECRET via HKDF-SHA256
  • Private key held only in process memory — never in env vars or config files
  • Trust model matches Better Auth session tokens: DB access + secret = full access

Access Key Support

  • Full access keys and function-call access keys (FAK)
  • FAK scoped to recipient contract for delegate actions
  • Configurable validation for limited access keys

Troubleshooting

| Issue | Solution | |---|---| | "Invalid or expired nonce" | Server nonces expire after 15 min; check clock sync | | "Account ID mismatch" | Verify signed message account ID matches wallet | | "Network ID mismatch" | Ensure networkId matches the account's network | | Relay fails with "insufficient balance" | Fund the relayer account with NEAR | | Relay fails with "contract not whitelisted" | Add receiverId to whitelistedContracts |

Examples

Browser to Server Example

A full-stack example showing NEAR authentication + gasless relay.

# From repo root
pnpm install
cd examples/browser-2-server
pnpm dev

Development

Interested in contributing? See CONTRIBUTING.md.

Quick start:

pnpm install
pnpm build
pnpm typecheck
pnpm test

Build output:

  • dist/index.js — Server plugin (ESM)
  • dist/client.js — Client plugin (ESM)
  • dist/*.d.ts — TypeScript declarations

Links