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

@winwinmbs/portal-api

v2.0.0

Published

Business API client for WINWIN Portal (files, email, workflow, user, etc.)

Readme

@winwinmbs/portal-api — Implementation Guide

Typed REST client for the WINWIN Portal API. Wraps every public endpoint (users, roles, permissions, workflows, files, todos, etc.) with type-safe methods. Uses axios under the hood; bearer-token + API-key injection are handled automatically.

For AI agents: this package is just an HTTP client. It does not manage auth state, refresh tokens, or persist anything. Pair it with @winwinmbs/portal-auth (browser) or a service-to-service token (backend) for the auth side.


1. Install

npm install @winwinmbs/portal-api
# or
pnpm add @winwinmbs/portal-api

Peer requirement: axios (declared as dep — installed automatically).


2. The whole shape

import { PortalApiClient } from '@winwinmbs/portal-api';

const portal = new PortalApiClient({
  apiUrl: 'https://api.winwinmbs.com',
  getToken: () => localStorage.getItem('access_token'),     // sync read
  apiKey: 'app_xxx',                                        // optional
  timeout: 30_000,                                          // optional, ms
});

// All API surfaces are properties on the client:
const me = await portal.user.findById(userId);
const orgs = await portal.organization.tree();
const todos = await portal.todo.search({ /* ... */ });

3. Available API namespaces

Each namespace maps to a portal feature module. Consult the IDE's autocomplete for the full method list — types are exported from the package.

| Namespace | Endpoint group | Common methods | |-----------|----------------|----------------| | portal.user | /users/* | search, findById, create, update, delete, setRoles, setPositions | | portal.organization | /organizations/* | tree, findById, create, update, move, delete | | portal.role | /roles/* | search, findById, create, update, setPermissions | | portal.permission | /permissions/* | search, findById, create, update | | portal.permissionCategory | /permission-categories/* | search, tree, create | | portal.workflow | /workflows/* | search, findById, instances, tasks, approve, reject | | portal.todo | /todos/* | search, findById, create, update, complete | | portal.files | /files/* | upload, download, findById, delete | | portal.email | /emails/* | send, templates, history | | portal.otp | /otp/* | request, verify | | portal.line | /line/* | link, unlink, notify | | portal.webhook | /webhooks/* | search, findById, create, delete | | portal.systemConfig | /system-configs/* | getCategory, getByKey, update | | portal.eventLog | /event-logs/* | search, findById | | portal.health | /health | check |

Every method returns the underlying portal response shape (typed via @win-portal/shared DTOs). Most search/list endpoints return ApiPaginatedResponse<T>; single-resource endpoints return ApiResponse<T>.


4. Configuration reference

interface PortalApiConfig {
  /** Portal API base URL (no trailing slash) */
  apiUrl: string;

  /**
   * Sync getter for the current access token. Called on every request.
   * Return null when there's no session — requests will go out without
   * Authorization, and the portal will respond 401 if auth is required.
   */
  getToken: () => string | null;

  /**
   * Optional app-scoped API key. Required for /v1/auth/* endpoints and any
   * other route that gates access via x-api-key. Sent as 'X-API-Key' header.
   */
  apiKey?: string;

  /** Per-request timeout in ms. Default 30_000. */
  timeout?: number;
}

getToken is synchronous on purpose. Most consumers cache the current access token in memory or localStorage and read it cheaply. If you only have an async source, do the refresh out-of-band (e.g. on app boot or via portal-auth.getAccessToken()) and write the result somewhere sync-readable.


5. Quickstart — browser app paired with portal-auth

import { AuthClient } from '@winwinmbs/portal-auth';
import { PortalApiClient } from '@winwinmbs/portal-api';

// 1) Auth side — manages tokens, refresh, storage
const auth = new AuthClient({
  apiUrl: 'https://api.winwinmbs.com',
  mode: 'sso',
  portalUrl: 'https://portal.winwinmbs.com',
  appId: 'your-app-id',
});
await auth.start();

// 2) API client — reads the token written by auth
let cachedToken: string | null = null;
auth.on('token_refreshed', ({ token }) => { cachedToken = token; });
auth.on('authenticated', () => { auth.getAccessToken().then(t => cachedToken = t); });

const portal = new PortalApiClient({
  apiUrl: 'https://api.winwinmbs.com',
  getToken: () => cachedToken,
});

// 3) Use it
const users = await portal.user.search({ limit: 20 });

Or (simpler, but does an async read every request):

const portal = new PortalApiClient({
  apiUrl,
  // axios wrapper is sync, so we can't await here. Pre-cache and let auth
  // events keep the local token current.
  getToken: () => auth['token'] ?? null,    // 👈 if you want to read internal cache
});

The recommended pattern is the event-listener one above — it keeps the getToken callback predictably synchronous.


6. Quickstart — backend service-to-service

import { PortalApiClient } from '@winwinmbs/portal-api';

// Backend has its own long-lived API key + can mint its own service token
// (or proxy the user's token through). Simplest: read both from env.
const portal = new PortalApiClient({
  apiUrl: process.env.PORTAL_API_URL!,
  apiKey: process.env.PORTAL_API_KEY!,
  getToken: () => process.env.PORTAL_SERVICE_TOKEN ?? null,
});

// Hit any read endpoint:
const apps = await portal.organization.tree();

For per-request user-scoped calls (e.g. an Express handler forwarding the caller's token), close over the request:

app.get('/me/orgs', async (req, res) => {
  const portal = new PortalApiClient({
    apiUrl: process.env.PORTAL_API_URL!,
    apiKey: process.env.PORTAL_API_KEY!,
    getToken: () => req.token ?? null,    // populated by portal-auth-server
  });
  const orgs = await portal.organization.tree();
  res.json(orgs);
});

The PortalApiClient is cheap to construct — instantiating per-request is fine. Or hoist + close over a mutable token holder if you prefer.


7. Common scenarios

7.1 Search with pagination

const page = await portal.user.search({
  page: 1,
  limit: 20,
  filter: { status: ['active'] },
  sort: [{ field: 'created_at', order: 'DESC' }],
});

// Returned shape (ApiPaginatedResponse<UserResponseDto>):
//   { success, data: User[], pagination: { page, limit, total, total_pages } }

7.2 File upload

const formData = new FormData();
formData.append('file', file);
const result = await portal.files.upload(formData, { category: 'avatar' });
// result.data.id is the file_id you can store on user.avatar_file_id

(Browser FormData for browser; Node consumers use FormData from undici or form-data.)

7.3 Workflow approval

const tasks = await portal.workflow.tasks({ assignee_user_id: meUserId });
const myPending = tasks.data.filter(t => t.status === 'pending');

await portal.workflow.approve(taskId, {
  comment: 'LGTM',
  metadata: { /* per-workflow custom data */ },
});

7.4 Composing typed responses

The DTOs are exported by @win-portal/shared (the portal's source of truth). This client re-uses those types directly:

import type { UserResponseDto } from '@win-portal/shared/user';

async function findUser(id: string): Promise<UserResponseDto | null> {
  try {
    const res = await portal.user.findById(id);
    return res.data;
  } catch (err) {
    if (axios.isAxiosError(err) && err.response?.status === 404) return null;
    throw err;
  }
}

8. Error handling

The client lets axios errors propagate. Patterns:

import axios from 'axios';

try {
  await portal.user.findById(id);
} catch (err) {
  if (axios.isAxiosError(err)) {
    const status = err.response?.status;
    const body = err.response?.data;        // ApiResponse-style error envelope
    if (status === 401) /* token expired or invalid */;
    if (status === 403) /* forbidden — wrong app or missing permission */;
    if (status === 404) /* not found */;
    if (status === 429) /* rate limited; body.retry_after_ms tells you when */;
    throw err;
  }
  throw err;
}

The portal's error envelope (post-2026-05-05) is:

{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "VALIDATION_ERROR" | ...,
    "message": "<localized human message>",
    "details": { ... },
    "timestamp": "2026-05-05T...",
    "path": "/users/abc",
    "method": "GET",
    "statusCode": 401,
    "requestId": "req_..."
  }
}

requestId is searchable in the portal's audit log — pass it to portal support when reporting issues.


9. Don'ts

  • ❌ Don't manage tokens inside this client. It's a transport, not an auth library. Refresh, storage, and lifecycle live in portal-auth.
  • ❌ Don't decode getToken's return value to check expiry. Just return it. If the portal says 401, deal with it then.
  • ❌ Don't omit apiKey for /v1/auth/* calls. That whole surface requires it; you'll get 401 with MISSING_API_KEY.
  • ❌ Don't share one PortalApiClient across users on a backend. It closes over getToken, so per-request construction (or a mutable holder) is the correct pattern.
  • ❌ Don't write your own retry logic on top of this. The portal's rate limiter returns 429 with retry_after_ms; respect that. Generic exponential backoff against any 5xx is reasonable, but skip 4xx (it won't recover by retrying).

10. Cross-package integration

| Combine with | Why | |--------------|-----| | @winwinmbs/portal-auth | Browser auth — feeds getToken from the SDK's storage | | @winwinmbs/portal-auth-react | React app structure — useSession().getAccessToken for token reads (note: that's async, see §5) | | @winwinmbs/portal-auth-server | Backend that validates AND forwards the user's token via this client |

Two-package combo (browser):

React component
  └─ useApi() {
       const { getAccessToken } = useSession();
       return useMemo(() => new PortalApiClient({
         apiUrl,
         getToken: () => /* sync mirror of getAccessToken result */,
       }), [...]);
     }

The trick: React renders synchronously, but getAccessToken is async (because it may refresh). Either:

  1. Maintain a sync mirror updated by the 'token_refreshed' event (recommended — illustrated in §5).
  2. Or wrap your fetch helpers in async (path, init) => { const t = await getAccessToken(); return fetch(...) } and skip PortalApiClient for that one call site.

11. Where to look in source

| File | Contents | |------|----------| | src/portal-api-client.ts | The PortalApiClient class, axios setup, interceptors | | src/api/user.api.ts | User CRUD + role/position management | | src/api/organization.api.ts | Org tree CRUD + move | | src/api/role.api.ts | Role CRUD + permission attachment | | src/api/permission.api.ts | Permission CRUD | | src/api/workflow.api.ts | Workflow definition + instance + task | | src/api/files.api.ts | File upload/download | | src/api/email.api.ts | Email send + templates | | src/api/otp.api.ts | OTP request/verify | | src/api/todo.api.ts | Todo CRUD | | src/api/system-config.api.ts | System config read/write (admin-scoped) | | src/api/health.api.ts | Health check | | src/api/event-log.api.ts | Audit log search | | src/api/line.api.ts | LINE link/unlink/notify | | src/api/webhook.api.ts | Webhook subscriptions | | src/api/permission-category.api.ts | Permission grouping tree | | src/types/ | Re-exported DTO types from @win-portal/shared | | src/portal-api-client.spec.ts | Reference for instantiation + interceptor behaviour |


12. Reference: API surface contract

This client tracks the portal's HTTP API. Every method here corresponds to a NestJS controller route in apps/api/src/modules/features/<domain>/. If the portal adds an endpoint, it must be added here too — but the portal team keeps the two in sync via a shared DTO package, so renames or signature changes break compilation immediately rather than at runtime.

For a list of capabilities (high-level inventory of what the portal can do), see docs/PORTAL_CAPABILITIES.md.

For low-level type definitions, browse packages/shared/src/dto/ — the client re-exports those types unchanged.