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

@comitor/account-sdk

v0.1.1

Published

SDK tích hợp Comitor.Account (OIDC) cho các ứng dụng trong hệ sinh thái Comitor.

Readme

@comitor/account-sdk

SDK tích hợp Comitor.Account — Identity Provider (OIDC) dùng chung của hệ sinh thái SaaS Comitor — cho một ứng dụng bất kỳ trong hệ.

Mục tiêu: app mới tích hợp xác thực trong dưới một ngày.


Đây không chỉ là tiện ích — nó là LỚP CÁCH LY

Gói này nuốt mọi hành vi ngầm của tầng OIDC bên dưới, để:

  1. Không app nào phải biết tới chúng.
  2. Đổi tầng OIDC sau này là thay gói này, không phải viết lại từng app.

Vì vậy: đừng để app nào import thẳng better-auth/*. Thiếu thứ gì thì bổ sung vào đây.

Bảy hành vi ngầm được nuốt ở đây

| # | Hành vi | Hậu quả nếu app tự làm | |---|---|---| | 1 | Access token chỉ là JWT khi request có resource= | Token thành chuỗi opaque, verify qua JWKS không được, claim tuỳ biến biến mất | | 2 | Back-channel logout đòi scope offline_access | Client không bao giờ được gọi khi phiên bị xoá — log rỗng, không lỗi | | 3 | PKCE S256 bắt buộc cho mọi client, kể cả confidential | plain bị từ chối với invalid_request | | 4 | /oauth2/authorize đổi kiểu phản hồi theo sec-fetch-mode, không theo Accept | Gọi bằng fetch nhận 200 + JSON, không có Location | | 5 | Chữ ký webhook ký trên "{timestamp}.{body}", body phải là chuỗi thô | JSON.parse rồi stringify lại là chữ ký không bao giờ khớp | | 6 | Dùng lại authorization code hỏng luôn refresh token của grant đó | Thử lại một lần là mất cả chuỗi | | 7 | Scope M2M cần resource riêng thì mới thực sự hẹp | Server lấy giao hai tập; chung resource là ranh giới vô nghĩa |


Cài đặt

pnpm add @comitor/account-sdk

Peer tuỳ chọn: next (chỉ khi dùng entry /next).


Tích hợp một app Next.js — bốn bước

1. Cấu hình một chỗ

// lib/account.ts
import type { AccountClientConfig } from "@comitor/account-sdk";

export const account: AccountClientConfig = {
  accountUrl: process.env.COMITOR_ACCOUNT_URL!,   // https://account.comitor.ai
  clientId: process.env.COMITOR_CLIENT_ID!,
  clientSecret: process.env.COMITOR_CLIENT_SECRET, // bỏ trống với app di động
  redirectUri: `${process.env.APP_URL}/api/auth/callback`,
  postLogoutRedirectUri: process.env.APP_URL,
  appKey: "tasks"                                  // định danh app trong danh mục Comitor
};

2. Bắt đầu đăng nhập

// app/api/auth/dang-nhap/route.ts
import { redirect } from "next/navigation";
import { startSignIn } from "@comitor/account-sdk/next";
import { account } from "@/lib/account";

export async function GET() {
  redirect(await startSignIn(account));
}

startSignIn cất statecode_verifier vào cookie httpOnly sống 10 phút — khớp đúng hạn của authorization code ở phía Account.

3. Nhận callback

// app/api/auth/callback/route.ts
import { completeSignIn } from "@comitor/account-sdk/next";
import { account } from "@/lib/account";

export async function GET(request: Request) {
  const tokens = await completeSignIn(account, new URL(request.url));
  // Phát cookie phiên CỦA RIÊNG APP ở đây. Xem "Nguyên tắc phiên" bên dưới.
}

completeSignIn kiểm stateđó là thứ duy nhất chặn CSRF trên luồng đăng nhập, đừng bỏ qua.

4. Guard trong layout của workspace

// app/w/[slug]/layout.tsx
import { requireWorkspaceAccess } from "@comitor/account-sdk";
import { account } from "@/lib/account";

export default async function Layout({ params, children }) {
  const { slug } = await params;
  const { user, workspace, app } = await requireWorkspaceAccess(account, accessToken, slug);
  // …
}

Nguyên tắc phiên — đọc trước khi tự chế

App phát cookie phiên của RIÊNG NÓ. Cookie của Comitor.Account nằm ở account.comitor.ai và không chia sẻ sang subdomain của app. SSO đi qua luồng OIDC, không đi qua cookie chung — an toàn hơn và đúng chuẩn hơn.

Refresh token là bí mật dài hạn: giữ ở server, không bao giờ để lọt xuống trình duyệt. Khi xoay token, phải lưu bộ mới — Account bật rotation nên refresh token cũ mất hiệu lực ngay:

const fresh = await ensureFreshTokens(account, tokens, async (rotated) => {
  await luuVaoPhien(rotated);   // KHÔNG lưu là lần sau người dùng bị đăng xuất không rõ lý do
});

ensureFreshTokens chống đua từ 0.1.1, và đó là một lỗi ĐÃ ĐO ĐƯỢC. Một lượt tải trang của Next sinh ra vài request phía máy chủ, và cache() của React chỉ gộp trong MỘT request. Khi access token vừa hết hạn, hai request cùng gọi refresh với cùng một token; Account xoay token nên bản cũ chết ngay, và request thua nhận 401 → phiên rơi.

Lỗi này TỰ CHE: SSO đưa người dùng quay lại trong dưới một giây, nên không ai báo. Trên production triệu chứng duy nhất là "thỉnh thoảng bị đăng xuất không rõ lý do".

Từ 0.1.1, các lời gọi đồng thời cùng một refresh token chia nhau MỘT lần xoay, và onRotate chạy cho mọi người gọi — không chỉ người thắng. Nửa thứ hai đó là bắt buộc: mỗi request có phản hồi riêng, nên request nào không chạy onRotate sẽ trả về phản hồi không mang cookie mới.

⚠ Phạm vi: MỘT tiến trình. Nhiều instance sau load balancer vẫn đua — ở đó phải lưu phiên vào database và xoay trong một giao dịch.

Đăng xuất khỏi riêng app thì chỉ cần xoá cookie của app — phiên tại Account còn nguyên và người dùng vẫn đang đăng nhập ở các sản phẩm khác. createLogoutUrl()đăng xuất toàn hệ, chỉ dùng khi người dùng thật sự muốn thoát khỏi mọi sản phẩm Comitor.


Ba lớp guard, ba thông điệp khác nhau

requireWorkspaceAccess tách rõ ba trường hợp vì chúng dẫn tới ba hành động khác nhau của người dùng. Gộp lại thành "bạn không có quyền" là để họ tự đoán:

| Mã lỗi | Nghĩa | Người dùng phải làm gì | |---|---|---| | NOT_A_MEMBER | không thuộc không gian làm việc | xin quản trị viên mời vào | | APP_NOT_ENABLED | workspace chưa bật ứng dụng này | quản trị viên bật trong phần Ứng dụng | | NO_SEAT | đã bật nhưng bạn chưa có chỗ ngồi | xin quản trị viên cấp chỗ | | APP_SUSPENDED | quá hạn thanh toán | cập nhật thanh toán |

Chỗ ngồi tính theo TỪNG ỨNG DỤNG, không theo workspace: một người dùng Tasks mà không dùng CRM là chuyện bình thường. Đừng dựng logic kiểu "là thành viên thì có mọi ứng dụng".


Webhook

Account gửi webhook để bù cho độ trễ cache: app cache tư cách thành viên 30–60 giây, nhưng gỡ thành viên và thu hồi chỗ ngồi thì phải tới ngay.

// app/api/comitor/webhook/route.ts
import { readWebhook, invalidateContextCache } from "@comitor/account-sdk";

export async function POST(request: Request) {
  const result = await readWebhook(request, process.env.COMITOR_WEBHOOK_SECRET!);
  if (!result.ok) return new Response(result.reason, { status: 401 });

  // PHẢI tự khử trùng lặp bằng payload.id — Account thử lại tới 5 lần.
  if (await daXuLy(result.payload.id)) return new Response(null, { status: 200 });

  switch (result.payload.event) {
    case "member.removed":
    case "seat.revoked":
      invalidateContextCache();
      await huyPhienLienQuan(result.payload.data);
      break;
  }
  return new Response(null, { status: 200 });
}

Route nhận webhook không được redirect. Account gửi với redirect: "error", nên URL đăng ký không được có dấu / cuối nếu framework của bạn chuyển hướng để thêm/bớt nó.


Máy-tới-máy

Cho dịch vụ nền gọi API danh bạ và quyền dùng ứng dụng khi không có người dùng nào đứng sau:

import { AccountM2mClient } from "@comitor/account-sdk";

const directory = new AccountM2mClient({
  accountUrl: process.env.COMITOR_ACCOUNT_URL!,
  clientId: process.env.COMITOR_M2M_CLIENT_ID!,
  clientSecret: process.env.COMITOR_M2M_CLIENT_SECRET!
});

const { members } = await directory.listMembers("acme");
const entitlements = await directory.getEntitlements("acme", "tasks");

Token được cache và tự xoay. Nhiều lời gọi đồng thời lúc token vừa hết hạn dùng chung một lần xin token — không có chuyện mười request cùng đấm vào /oauth2/token rồi chạm rate limit của chính nó.


Vai trò thô — và ranh giới không được vượt

import { hasAtLeastRole } from "@comitor/account-sdk";

if (hasAtLeastRole(workspace.role, "admin")) { /* … */ }

⚠ Đây là tiện ích không có nhà nước, dùng nếu muốn. Nó không phải hệ phân quyền của app. "Ai được xoá task", "ai duyệt đơn nghỉ" là câu hỏi của app và dữ liệu nằm ở database của app.

Dựng RBAC của app trên bốn vai trò thô này là con đường biến Account thành kho quyền của mọi sản phẩm — đúng thứ thiết kế của Comitor cố tình tránh, và một khi đã đi thì rất khó quay lại.