SessionKit docs

Quickstart

From zero to Login with BLAC on your site — three steps, about ten minutes.

1. Prove your domain

Run this once, anywhere in your project. It generates an Ed25519 key pair and writes the document that proves BLAC Codes really come from your domain.

npx @blaclabs/sessionkit init --name "Your Site"

You get two files:

  • public/.well-known/blac-session.json — publish this at https://your-domain/.well-known/blac-session.json. It holds only your public key.
  • blac-session-private-key.pem — keep it on your server as an environment variable. Never commit it, never ship it to the browser. Add it to .gitignore now.

Serving the well-known document over HTTPS is what makes impersonation impossible. A scam site can run SessionKit — but it can never make BLAC Wallet display your domain on the approval screen.

2. Add the kit

Load it from our CDN, pinned to an exact version with its integrity hash. The script runs in your page with your users' sessions, so the browser should refuse anything that is not the file you reviewed. Every release lists its hash in the changelog.

<script
  src="https://sessionkit.blaclabs.io/v1/0.2.1/sessionkit.js"
  integrity="sha384-k0P8kGYblTAtiBWRRKBJwDYcA6gPW+J3VMyDKLIZdB+Pa5Z+OvzwnR7F1X1S2OCG"
  crossorigin="anonymous"></script>

Or install it if you bundle and want TypeScript types:

npm install @blaclabs/sessionkit

3. Sign requests from your backend

The browser sends only its fresh session key and the scopes it would like. Your server owns the policy: it decides the origin and the most a code may ask for, and signs. The private key never reaches client code, and a script injected into your page can never widen the grant.

// POST /api/blac-sign — body: { kemPubKey, scopes } from the browser
import { createSessionSigner } from "@blaclabs/sessionkit/server";

const signer = createSessionSigner({
  privateKeyPem: process.env.BLAC_SESSION_KEY,
  kid: "2026-09",                           // the kid from your .well-known
  origin: "https://your-domain.com",        // signed into every code, never read from the request
  allowedScopes: ["identity", "address:solana"]  // requests are clipped to this
});

export async function POST(request) {
  // Only your own browser sessions may mint codes. Without this, anyone can
  // call the endpoint and relay a genuine code to a victim.
  const browserSession = await getOrCreateSession(request); // your cookie/CSRF layer
  if (!browserSession) {
    return new Response("Forbidden", { status: 403 });
  }

  const signed = signer.sign(await request.json()); // { fields, sig, sigKid }

  // Remember which session asked for this code, and later complete the login
  // only into that session (see Security → "What you must do").
  await browserSession.set("blacPairingTopic", signed.fields.pairingTopic);

  return Response.json(signed);
}

The two session lines matter. A signing endpoint that anyone can call produces genuine codes for anyone to relay. Binding it to the browser session that asked means a relayed code can only ever log in the attacker's own session, and the code expires within two minutes.

4. Connect from the browser

import { connect } from "@blaclabs/sessionkit";

const session = await connect({
  scopes: ["address:solana"],              // identity is always included
  mount: document.getElementById("blac-login"),
  sign: (input) =>
    fetch("/api/blac-sign", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(input)
    }).then((response) => response.json())
});

session.user.id;            // stable id, unique to your domain
session.user.solanaAddress; // only because you asked, they agreed, and the key proved it

SessionKit draws the BLAC Code into your mount element, waits for the scan, and resolves when the user approves. If the code expires first, the promise rejects with a typed error. A decline sends nothing, so the page simply keeps waiting until then. Offer a cancel button and pass an AbortSignal to stop sooner.

5. Verify the login on your server

Before you issue a session cookie, verify the proof server-side. It is self-contained — no call to any BLAC server is involved.

// POST /api/blac-login — body: session.raw from the browser
import { verifyApproval } from "@blaclabs/sessionkit/server";

const browserSession = await getSession(request);
const pairingTopic = await browserSession.take("blacPairingTopic"); // read and delete: use once

const login = await verifyApproval({
  approval: await request.json(),          // session.raw
  origin: "https://your-domain.com",
  pairingTopic                             // the topic YOU issued, never the browser's copy
});

// login.userId is now cryptographically proven. Create your own session.

Use the pairing topic your signing endpoint stored, and only once. You issued it, so only you know it is fresh. That freshness is what stops a captured approval from being replayed.

Never trust the browser's copy of the user id alone. The identity proof is what makes a login real, and verifyApproval is what checks it. Skipping this step means anyone can POST any user id to your backend.

6. Ask for signatures (optional)

Add a sign:* scope, in the browser and in your server's allowedScopes, and the channel stays open after approval. Your dapp proposes messages and transactions; the wallet decodes each one, shows its risk verdict and your verified origin, and the user approves it there.

const session = await connect({
  scopes: ["sign:solana", "sign:evm"],     // each implies its address scope
  mount,
  sign,
  // Show these four digits next to the code; the wallet shows the same ones.
  onApproved: ({ sas }) => (sasLabel.textContent = sas)
});

// Plug into the stack you already run:
const { provider } = session.solanaWallet({ register: true }); // Wallet Standard + Phantom-shaped API
const ethereum = session.ethereumProvider({                     // EIP-1193, announced via EIP-6963
  rpcUrls: { 1: "https://your-eth-rpc", 137: "https://your-polygon-rpc" },
  announce: true
});

// Or talk to the wallet directly:
const { signature } = await session.request({
  chain: "solana",
  method: "solana_signMessage",
  params: { message: base64Message }
});

session.onEnd((reason) => showReconnect(reason));
await session.disconnect();

A declined or refused request rejects with a SignRequestError carrying the wallet's code. The API reference lists them.

Next