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

@suphark/auth-client

v0.8.2

Published

Pi Auth SDK: OIDC code+PKCE flow, encrypted session cookie, JWT verify via JWKS, Next.js handlers/middleware, React hook, service client, webhook verify

Readme

@suphark/auth-client

SDK สำหรับแอป Next.js ที่ต่อ Pi Auth (OIDC provider + directory + permission แบบ multi-tenant)

bun add @suphark/auth-client   # หรือ npm i / pnpm add

เริ่มเร็ว (0.7.1) — แอปเขียนแค่ 4 จุด

env: PI_AUTH_ISSUER, PI_AUTH_CLIENT_ID, PI_AUTH_CLIENT_SECRET, PI_AUTH_SESSION_SECRET, APP_URL (ไม่บังคับ: PI_AUTH_TENANT ผูกองค์กรเดียว, UPSTASH_REDIS_REST_URL/TOKEN → ถอน session ครบทุก instance อัตโนมัติ)

// lib/pi-auth.ts — อ่านค่าจาก env เอง (ขาดตัวไหนแจ้งชื่อที่ขาดครบในครั้งเดียว)
import { createPiAuth } from "@suphark/auth-client/next";
export const piAuth = createPiAuth({
  // ไม่บังคับ: sync แถวผู้ใช้ของแอป — เรียกตอนเห็นผู้ใช้ (throttle 15 นาที, error ไม่ทำให้คำขอล้ม)
  onUserSync: (u) => db.user.upsert({ where: { piAuthId: u.id }, update: {...}, create: {...} }),
});

// app/auth/[...pi]/route.ts
export const { GET, POST } = piAuth.handlers;

// proxy.ts — server action ไม่มี session ได้ 401 JSON { error: "session_expired" } (วาง <SessionExpiredRedirect /> ใน layout ให้พาไปล็อกอิน)
export default piAuth.middleware({ publicPaths: ["/"] });

// หน้า / server action / route handler
const user = await piAuth.requireUser({ permission: P.use });                  // ไม่ล็อกอิน → redirect, ไม่มีสิทธิ์ → PermissionDeniedError
await piAuth.requireUser({ permission: P.admin, fresh: 300 });                 // งานสำคัญ: ต้องยืนยันตัวตนภายใน 5 นาที (ไม่สด → redirect ไป reauth)
await piAuth.requireUser({ api: true });                                       // JSON API: โยน PiAuthError 401/403 แทน redirect

ผูกองค์กรเดียว: PI_AUTH_TENANT (หรือ defaultTenant) — ไม่ตั้ง = ไม่ล็อกองค์กร (ไม่มี tenant_hint/TENANT_MISMATCH) ถ้าต้องล็อกให้แน่นอนให้ใส่ default ใน env schema ของแอปแล้วส่ง defaultTenant เอง · dev bypass / build โดยไม่มี secret: ขาด env ตอน next build ได้ (ใช้ค่าชั่วคราว) แต่ตอน start ยังโยน MISCONFIGURED — แอปที่รันโดยไม่มี PI_AUTH_* (dev bypass) ให้ส่งค่า dev ใน createPiAuth({...}) เฉพาะตอนเปิด bypass

หน้า sign-in ของแอปไม่จำเป็นต้องมี (/auth/login พาไปหน้าของ Pi Auth) — ถ้ามี ใช้ describeAuthError(code, { locale, tenant }) (@suphark/auth-client/react) แทนตารางข้อความของตัวเอง: คืน { title, body, next, terminal } — terminal = ล็อกอินซ้ำไม่ช่วย (ซ่อนปุ่มหลักแล้วบอกว่าใครแก้ได้) · safeNextPath export จาก /next ไม่ต้องเขียนตัวกรอง next เอง

ตัวอย่างเต็ม (ตั้งค่าเองทุกตัว):

// src/lib/pi-auth.ts
import { createPiAuth, createUpstashRevokedSessionStore } from "@suphark/auth-client/next";

export const piAuth = createPiAuth({
  issuer: process.env.PI_AUTH_ISSUER!, // https://auth.example.com (ไม่มี /api/auth)
  clientId: process.env.PI_AUTH_CLIENT_ID!,
  clientSecret: process.env.PI_AUTH_CLIENT_SECRET,
  redirectUri: `${process.env.APP_URL}/auth/callback`,
  scopes: ["openid", "profile", "email", "offline_access", "tenant", "permissions"],
  sessionSecret: process.env.PI_AUTH_SESSION_SECRET!, // ≥ 32 ตัวอักษร
  session: { cookieName: "myapp.session", maxAgeSeconds: 60 * 60 * 8 },
  // แอปที่ผูกกับองค์กรเดียว: ใส่ tenant_hint ให้ทุกครั้ง (รวม redirect จาก middleware) และปฏิเสธ token ขององค์กรอื่น
  defaultTenant: "suphark",
  // ปุ่ม "ออกจากระบบ" ของแอปนี้ออกเฉพาะแอป (ผู้ใช้ยังล็อกอินที่ Pi Auth → กลับเข้าได้ทันที); ค่าเริ่มต้น "all" = ออกทุกแอป
  logoutScope: "local",
  // back-channel logout บนหลาย instance (Vercel): เก็บ sid ที่ถูกถอนใน Upstash แทน memory (ค่าเริ่มต้น = instance เดียวเท่านั้น)
  revokedSessionStore: createUpstashRevokedSessionStore({
    url: process.env.UPSTASH_REDIS_REST_URL!,
    token: process.env.UPSTASH_REDIS_REST_TOKEN!,
  }),
});

// app/auth/[...pi]/route.ts
export const { GET, POST } = piAuth.handlers; // /auth/login, /auth/callback, /auth/logout, /auth/session, /auth/backchannel-logout

// proxy.ts (middleware)
export default piAuth.middleware({ publicPaths: ["/", "/api/webhooks"] });

// ผู้ใช้อยู่หลายองค์กร: ส่งไป /auth/login?tenant=<slug> เพื่อขอ token ขององค์กรนั้น (ชนะ defaultTenant)
// (Pi Auth สลับ tenant ให้ถ้าเป็นสมาชิก; ถ้าได้ token ของ tenant อื่นกลับมา SDK ปฏิเสธด้วย TENANT_MISMATCH)

// logout: /auth/logout (ตาม logoutScope) · /auth/logout?scope=local&next=/login · /auth/logout?scope=all
// piAuth.logoutUrl({ scope: "local", next: "/login" }) สร้าง URL ให้; scope=all กลับที่ postLogoutRedirectUri
// ลิงก์ logout ที่มาจากเว็บอื่น (CSRF) ได้หน้ายืนยันแทนการ logout ทันที (0.6.3)

// ยืนยันตัวตนซ้ำก่อนงานสำคัญ (0.6.4): user.authTime = เวลาล็อกอินที่ Pi Auth ครั้งล่าสุด (unix seconds)
// if (!user.authTime || Date.now() / 1000 - user.authTime > 300) redirect(piAuth.reauthUrl("/secret"))

// callback ล้มเหลว (ลิงก์หมดอายุ, กด back, tenant ไม่ตรง ฯลฯ) → redirect ไป errorRedirect (ค่าเริ่มต้น "/")
// พร้อม ?auth_error=<code> (และ &tenant=<slug> เมื่อเป็น TENANT_MISMATCH) — ไม่ใช่ JSON ดิบ
// createPiAuth({ ..., errorRedirect: "/welcome" })  หรือคุมเองด้วย onError(error, request) => Response

// app/page.tsx — แสดงข้อความตามรหัส (server: import จาก /next; client component: import จาก /react)
import { readAuthError } from "@suphark/auth-client/next";
const authError = readAuthError(await searchParams); // { code: "TENANT_MISMATCH", tenant: "beta" } | null

// server
const user = await piAuth.getCurrentUser(); // null ถ้าไม่ได้ล็อกอิน
piAuth.requirePermission(user, "memo:manage"); // throw 403
user?.org?.path; // "/acme/eng/it"
user?.tenants; // องค์กรทั้งหมดที่เป็นสมาชิก [{ id, slug, name, role }] (scope tenant; ใช้กับ <OrganizationSwitcher/> ของ @suphark/auth-ui)

// service-to-service (client_credentials ของ service account)
const svc = piAuth.serviceClient(); // ต้องตั้ง service: { clientId, clientSecret, scopes }
await svc.directory.resolve({ positionCode: "DIR", withinOrgPathOf: user!.tenant!.membershipId! });
// รับ webhook: app/api/pi-webhook/route.ts — ตรวจด้วย JWKS ของ PI_AUTH_ISSUER ไม่ต้องมี secret
import { parsePiWebhook } from "@suphark/auth-client/webhooks";
export async function POST(request: Request) {
  const event = await parsePiWebhook(request); // หรือ parsePiWebhook(request, { secret }) แบบ HMAC
  // idempotent ด้วย event.id
  return new Response(null, { status: 204 });
}
// client
import { readAuthError, usePiAuth } from "@suphark/auth-client/react";
const { user, hasPermission } = usePiAuth();
const authError = readAuthError(useSearchParams()); // หน้า login ที่เป็น client component
  • access token เก็บใน cookie ที่เข้ารหัส (JWE) ไม่ถึง browser; refresh อัตโนมัติใน middleware
  • verify JWT ด้วย JWKS ของ issuer (cache + rotation), clock skew 60 วินาที
  • ต้องการ Node.js runtime (ใช้ node:crypto); middleware ของ Next 16 (proxy.ts) รันบน Node อยู่แล้ว

e2e แบบล็อกอินแล้ว (0.8.0) — @suphark/auth-client/testing

ให้ CI ได้ session ของบัญชีทดสอบเอง ไม่ต้องคัดลอก cookie ของคนจริงไปเก็บเป็น secret:

// e2e/global-setup.ts (Playwright)
import { createTestSession } from "@suphark/auth-client/testing";

export default async function globalSetup() {
  const s = await createTestSession({
    issuer: process.env.PI_AUTH_ISSUER!,
    clientId: process.env.PI_AUTH_CLIENT_ID!, // client **dev** ที่เปิด "ล็อกอินทดสอบ (CI)" ไว้
    clientSecret: process.env.PI_AUTH_CLIENT_SECRET!,
    sessionSecret: process.env.PI_AUTH_SESSION_SECRET!, // ค่าเดียวกับที่แอปที่รันอยู่ใช้
    email: process.env.PI_AUTH_TEST_EMAIL!,
    cookieName: "pi-done.session", // = session.cookieName ที่แอปตั้งใน createPiAuth
    appUrl: "http://localhost:3000",
  });
  fs.writeFileSync("e2e/.auth/state.json", JSON.stringify(s.storageState)); // + use: { storageState } ใน playwright.config
}
  • Pi Auth ต้องตั้งก่อน: แพลตฟอร์มเปิดสวิตช์ · client dev (redirect เป็น localhost ทั้งหมด) เปิดล็อกอินทดสอบ + ระบุอีเมลบัญชีทดสอบ · บัญชีทดสอบเป็นสมาชิกองค์กร SANDBOX ที่ติดตั้งแอป — ขั้นตอนเต็มที่ docs/ops/ci-test-login.md ของ Pi Auth
  • session อายุ 15 นาที ไม่มี refresh token; ผิดเงื่อนไขใดก็ปฏิเสธพร้อมเหตุผล (TestSessionError.message เช่น user_not_listed)
  • ใช้ใน runtime ของแอปจริงไม่ได้ (ปฏิเสธเมื่อ NODE_ENV=production)

ความปลอดภัยของ SDK (สิ่งที่ทำให้แล้ว)

  • sessionSecret ต้องยาว ≥ 32 ตัวอักษร — สั้นกว่านั้น createPiAuth จะ throw WEAK_SESSION_SECRET (กัน env หายแล้วได้ cookie ที่ถอดได้)
  • cookie session/transaction เข้ารหัส A256GCM (key จาก HKDF ของ sessionSecret) · HttpOnly · SameSite=Lax · Secure เมื่อ redirect URI เป็น https
  • PKCE S256 + state + nonce ทุกครั้ง; next รับเฉพาะ path ภายใน (กัน open redirect)
  • perms_ref ดึงเฉพาะ URL ใต้ origin ของ issuer — access token ไม่ถูกส่งไป host อื่น
  • manifest (code-first): definePermissions({...}) ใน lib/permissions.ts → P.keys สำหรับ hasPermission() + ส่ง manifest: P.manifest ให้ createPiAuth เพื่อเปิด route GET /auth/manifest อัตโนมัติให้ Pi Auth ดึงตอน register/กด Sync; confidential client ยัง Push ผ่าน CI ได้; มี PiAuthManifest/validatePiAuthManifest สำหรับตรวจ JSON
  • ขอ ?tenant=<slug> แล้วได้ token ของ tenant อื่น → TENANT_MISMATCH (ตั้งแต่ 0.1.2) — แอปที่ผูกกับองค์กรเดียวไม่มีทางได้ session ผิดองค์กร
  • callback ล้มเหลวส่งกลับแอปเป็นรหัสเท่านั้น (PI_AUTH_ERROR_CODES) ไม่ส่งข้อความลง URL; errorRedirect รับเฉพาะ path ภายใน (ตั้งแต่ 0.1.3) — request ที่ขอ Accept: application/json ยังได้ JSON แบบเดิม
  • แพ็กเกจไม่มี secret/hostname ภายใน และไม่แนบ source map (ตั้งแต่ 0.1.1)