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

@last1id/sdk-nextjs

v0.1.0

Published

Next.js App Router companion for @last1id/sdk: drop-in /api/auth/last1 route handler, encrypted session cookie, and auto-refresh helpers for Server Components and Route Handlers

Readme

@last1id/sdk-nextjs

Next.js App Router companion to @last1id/sdk.

Drops in a /api/auth/last1 catch-all route, manages an encrypted session cookie, and provides helpers for reading the session inside Server Components and Route Handlers — including automatic refresh-token rotation that persists back to the cookie on the next response.

npm install @last1id/sdk-nextjs @last1id/sdk

This package adds no runtime dependencies beyond the base SDK. The optional peer on next is only consulted when you call getLast1Session() from a Server Component.

Quick start

1. Generate a session secret

openssl rand -base64 48

Add it to your environment:

LAST1_ISSUER_URL=https://last1.id
LAST1_CLIENT_ID=acme
LAST1_CLIENT_SECRET=...
LAST1_REDIRECT_URI=https://app.acme.example/api/auth/last1/callback
LAST1_SESSION_SECRET=<openssl output>

2. Mount the catch-all route

Create app/api/auth/last1/[...last1]/route.ts:

import { createLast1Handler } from "@last1id/sdk-nextjs";

// Edge runtime works too — the handler uses only Web Crypto + fetch.
export const runtime = "nodejs";

const handler = createLast1Handler({
  issuerUrl: process.env.LAST1_ISSUER_URL!,
  clientId: process.env.LAST1_CLIENT_ID!,
  clientSecret: process.env.LAST1_CLIENT_SECRET!,
  redirectUri: process.env.LAST1_REDIRECT_URI!,
  sessionSecret: process.env.LAST1_SESSION_SECRET!,
  scopes: ["openid", "profile", "credentials:read"],
  loginSuccessRedirect: "next",
  logoutRedirect: "/",
  onLoginSuccess: async ({ identityId, claims, tokens }) => {
    // Partner-side DB upsert keyed on identityId. Throwing here
    // fails the login closed — no session cookie is written.
    await db.users.upsert({ last1IdentityId: identityId, email: claims?.email });
  },
});

export const { GET, POST } = handler;

This auto-wires four endpoints:

| Method | Path | Behavior | | ------- | ------------------------------- | -------- | | GET | /api/auth/last1/login | Builds PKCE, redirects to ${issuerUrl}/oauth/authorize. Accepts ?next=/path (validated same-origin). | | GET | /api/auth/last1/callback | Exchanges the code, fires onLoginSuccess, sets the session cookie, redirects to next or loginSuccessRedirect. | | POST | /api/auth/last1/logout | Fires onLogout, clears the cookie, redirects to logoutRedirect. | | GET | /api/auth/last1/me | Returns { session: { identityId, scope, accessTokenExpiresAt, issuedAt } } or 401. Tokens are NEVER included in the response body. |

Any other path under the catch-all returns 404.

3. Add a sign-in button

// app/page.tsx
export default async function Home() {
  return (
    <a href="/api/auth/last1/login?next=/dashboard">
      Sign in with Last1 ID
    </a>
  );
}

4. Read the session in a Server Component

import { getLast1Session } from "@last1id/sdk-nextjs";
import { redirect } from "next/navigation";

export default async function Dashboard() {
  const session = await getLast1Session({
    sessionSecret: process.env.LAST1_SESSION_SECRET!,
  });
  if (!session) redirect("/api/auth/last1/login?next=/dashboard");
  return <h1>Hi, {session.identityId}</h1>;
}

getLast1Session returns null on:

  • No session cookie present.
  • A torn / tampered / rotated-secret cookie. (We intentionally do not distinguish these — the caller always treats null as "show sign-in".)

The returned Last1Session includes accessToken and refreshToken so your Server Component can pass them onward, but you almost always want the helper below instead.

5. Call partner-read APIs from a Route Handler

// app/api/me/credentials/route.ts
import { withLast1Session } from "@last1id/sdk-nextjs";

export const GET = withLast1Session(
  {
    issuerUrl: process.env.LAST1_ISSUER_URL!,
    clientId: process.env.LAST1_CLIENT_ID!,
    clientSecret: process.env.LAST1_CLIENT_SECRET!,
    sessionSecret: process.env.LAST1_SESSION_SECRET!,
  },
  async (req, { client, session }) => {
    const credentials = await client.listCredentials();
    return Response.json({ identityId: session.identityId, credentials });
  },
);

withLast1Session arranges for the rotated tokens (if any) to be written back into the response Set-Cookie header automatically. The next request from the same browser uses the new pair without partner code involvement.

If the refresh chain is dead (Last1AuthError on rotation — user revoked consent, refresh token was reused, etc), the wrapper:

  • Returns the configured unauthorizedResponse (defaults to a 401 JSON).
  • Sends a Set-Cookie that clears the session cookie.

The next page load will therefore see getLast1Session() === null and prompt the user to re-link cleanly.

Cross-app revoke webhooks

When the user revokes consent on last1.id, the platform calls your configured webhook URL. Verify it with verifyWebhookSignature from @last1id/sdk and then call clearLast1SessionCookie if you want to preemptively log the user out on next request:

// app/api/webhooks/last1/route.ts
import { verifyWebhookSignature } from "@last1id/sdk";

export async function POST(req: Request) {
  const raw = await req.text();
  try {
    await verifyWebhookSignature(
      process.env.LAST1_WEBHOOK_SECRET!,
      req.headers.get("x-last1-signature") ?? undefined,
      raw,
    );
  } catch {
    return new Response(null, { status: 204 }); // fail closed, no info leak
  }
  const event = JSON.parse(raw);
  // event.type === "consent.revoked" or "app_link.revoked"
  await db.users.update(event.data.identity_id, { last1Linked: false });
  return new Response(null, { status: 204 });
}

The session cookie itself is encrypted with sessionSecret and lives in the browser; it expires naturally when the refresh token is invalidated. Your DB unlink is what actually disconnects subsequent requests.

Cookie security model

| Property | Default | | ------------- | ---------------------------------------- | | Name | last1_session (PKCE: last1_session_pkce) | | HttpOnly | Always | | Secure | true when NODE_ENV === "production" | | SameSite | Lax (enough for the OAuth redirect) | | Path | / | | Max-Age | 30 days (session), 600s (PKCE) | | Encryption | AES-256-GCM via Web Crypto | | Key derivation| HKDF-SHA256 with per-purpose info label|

The session cookie payload is JSON containing access_token, refresh_token, scope, and timestamps — encrypted in place. Total cookie size for typical scopes is well under the 4KB browser limit.

Rotating LAST1_SESSION_SECRET immediately invalidates every existing session. Plan accordingly: change the secret only when you intend a global sign-out (e.g. after a security incident).

Running on Edge runtime

Everything in this package is Web-Crypto + standard Request/Response, so you can flip the route to Edge:

export const runtime = "edge";

getLast1Session dynamically imports next/headers; that import resolves correctly on both runtimes in Next.js 14+.

Pages Router

This package targets the App Router. For Pages Router, build a thin adapter that converts NextApiRequest to a Request and pipes the Response back out — or migrate the auth endpoints to the App Router even if the rest of the app stays on Pages.

License

MIT