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

woodsportal-client-sdk

v4.0.11

Published

Official TypeScript/JavaScript SDK for WoodsPortal API - Authentication, user management, pipelines, and more

Readme

woodsportal-client-sdk

TypeScript/JavaScript ESM client for the WoodsPortal HTTP API: authentication, SSO, users, pipelines, HubSpot-aligned objects, notes, emails, uploads, and files.

| | | | ----------- | --------------------------------------------------------------------------------------------------------------------------------- | | npm | woodsportal-client-sdk | | Source | Digital-Woods/digitalwoods.io-woodsportal-client-sdk | | Issues | GitHub Issues | | Runtime | Node ≥ 18; ESM only ("type": "module" in consuming apps is recommended) | | License | ISC — see LICENSE |


Project layout (Spring Boot–style)

Production and test sources are split like WoodsPortal Java services:

| Path | Role | | ----------- | ----------------------------------------------------------------------------------------------------------------- | | src/main/ | Library source — core/, features/, state/, adapters/ (see docs/ARCHITECTURE.md) | | src/test/ | Unit tests mirroring src/main/ package paths |

Example: src/main/client/auth-headers.tssrc/test/client/login-session.test.ts.

See src/test/README.md. Run tests with npm test.

Contributors: docs/DEVELOPER-GUIDE.md · CONTRIBUTING.md

Monorepo watch rebuild: yarn local (or npm run local) in this repo — same script name as client/admin, but library watch only (no Vite dev server). See the developer guide for the two-terminal workflow.


Production checklist

  1. Pin the major version in package.json (e.g. ^1.1.0) and read CHANGELOG.md before upgrades.
  2. Call initializeHttpClient once at startup with a baseURL from environment (never hardcode secrets; never ship a mystery default API host to production without review).
  3. Use HTTPS for baseURL in every deployed environment.
  4. Do not log passwords, refresh tokens, access tokens, or full API error bodies in production telemetry.
  5. Handle errors with try/await catch around mutate(), and map user-visible text with getFormErrors / getFieldErrors when the API returns validation payloads.
  6. Align hub context with how your shell stores HubSpot data (core/utils/hub-context.ts reads hub / dev-portal identifiers from app storage).

Install

npm install woodsportal-client-sdk
yarn add woodsportal-client-sdk

Configure the HTTP client (required for production)

The SDK uses one shared Axios instance. If initializeHttpClient has never been called, the first request throws — call initializeHttpClient at app startup with a baseURL from your environment (see client-frontend configureWoodsPortalSdk).

Call initializeHttpClient once during application bootstrap (before any api.* calls):

import { initializeHttpClient } from 'woodsportal-client-sdk'

initializeHttpClient({
    baseURL: process.env.VITE_API_BASE_URL!, // example: Vite — use your env mechanism
    timeout: 50_000,
    hubId: process.env.VITE_HUB_ID,
    devPortalId: process.env.VITE_DEV_PORTAL_ID,
    skipCurrentPublicPath: () => false,
    routes: {
        unauthorized: '/unauthorized',
        login: '/login'
    },
    onLogout: async () => {
        // Clear cookies / storage and route to login — implementation is app-specific
    }
})

Hub and dev-portal identifiers are also read from browser storage in core/utils/hub-context.ts. Keep that storage in sync with your HubSpot / portal shell so authenticated routes resolve the correct tenant context.


Public API (3.0)

| Export | Purpose | | ----------------------------------------- | -------------------------------------------------------------------------- | | api.auth | Login, MFA, security, SSO, users, session helpers (api.auth.session.*). | | api.crm | Pipelines, objects, notes, emails, files, uploads, cache purge. | | api.navigation | URL factories (makeLink, updateLink), route params, breadcrumbs. | | store | storage, CRM nanostores (table, user, …), tableUi (pagination UI). | | hubContext | Hub / portal identifiers from browser storage. | | initializeHttpClient | Axios base URL, timeouts, hub headers, auth callbacks. | | getFormErrors, getFieldErrors | Map Axios errors to form-level / field-level messages. | | resolveApiErrorDisplay | Map API / transport errors to user-facing title, description, variant, retry/support (see below). |

4.0: nested api.* only; useTableUi() + required tableParams on list mutations. See CHANGELOG.md.

Naming rules and migration: docs/PUBLIC-API-NAMING.md.


API error resolver (resolveApiErrorDisplay)

WoodsPortal HTTP errors return errorCode, category, and errorMessage (see woodsportal-api docs/API-ERROR-CODES.md). resolveApiErrorDisplay(error, options?) turns any thrown value into stable UI copy:

import { resolveApiErrorDisplay } from 'woodsportal-client-sdk'

const display = resolveApiErrorDisplay(err)
// display.title, display.description, display.variant, display.showRetry, display.showSupport
// display.errorCode, display.correlationId (when present)

Behavior:

  1. Non-HTTP failures (timeout, offline, 503) → transport variants via classifyHttpError.
  2. API responses → parseApiErrorPayload + lookup in API_ERROR_DISPLAY_CONFIG (all active codes + reserved fallbacks).
  3. Unknown errorCode → fallback by category (AUTH, ACCESS, VALIDATION, FILE, RATE_LIMIT, …).
  4. Description: prefers API errorMessage (server i18n); uses config fallbackDescription only when the API message is empty. Does not surface detailedMessage to end users.

Auth-specific full-page copy (HUBSPOT_REAUTH_REQUIRED, PORTAL_INACTIVE, …) remains in getUnauthorizedPageCopy; the resolver delegates where appropriate.

When API adds a code: update ErrorCode.java, API-ERROR-CODES.md, and src/main/core/errors/api-error-display-config.ts together. The sync guard test in src/test/core/errors/ fails if an active code lacks config.


Mutation-style methods (api.auth.login, …)

Most nested factories wrap createMutation: invoke once with optional MutationOptions, then call the returned mutate or the leaf alias (e.g. login, list).

Loading: isLoading() returns whether any in-flight call exists for that factory (overlapping calls are supported).

Errors: onError runs when the request fails; the returned promise still rejects — use try/catch or .catch() in addition to onError when you need local control flow.

import type { LoginPayload } from 'woodsportal-client-sdk'
import { api } from 'woodsportal-client-sdk'

const { login, mutate, isLoading } = api.auth.login({
    onSuccess: async (data, payload) => {
        // Persist session in app state if needed; tokens are handled inside the SDK login path
    },
    onError: (error, payload) => {
        // Log a redacted message; map to UI state — avoid logging credentials
    },
    onLoadingChange: (loading) => {
        // Drive a global or local spinner
    }
})

await login({ username: '[email protected]', password: '…' })
// `mutate` is identical to `login` here

React example (login form)

import type { LoginPayload } from 'woodsportal-client-sdk'
import { api } from 'woodsportal-client-sdk'

const { login } = api.login({
    onSuccess: () => undefined,
    onError: () => undefined,
    onLoadingChange: () => undefined
})

export async function submitLogin(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)
    const payload: LoginPayload = {
        username: String(formData.get('username') ?? ''),
        password: String(formData.get('password') ?? '')
    }
    await login(payload)
}

Build from source (contributors)

npm ci
npm run build
npm run type-check

Build: npm run build runs tsup and emits a single ESM bundle dist/index.js plus dist/index.d.ts (and source maps). That layout matches how Node’s native ESM loader and Vite/webpack resolve the package; the previous plain tsc output used extensionless relative imports and did not load under Node without a bundler.

Published tarballs include dist, README.md, LICENSE, and CHANGELOG.md.


Verify packaging (local consumer)

The repo includes examples/smoke-consumer: a tiny app that depends on the SDK with "woodsportal-client-sdk": "file:../..", the same resolution shape real projects use after npm pack / registry install. It runs runtime checks (node smoke.mjs) and TypeScript checks (tsc --noEmit on smoke.ts) without calling authenticated WoodsPortal APIs.

npm run test:consumer

Run this after meaningful changes to package.json exports, build output, or public entrypoints. See examples/smoke-consumer/README.md.

Live check (optional): npm run test:consumer:live — builds, installs the consumer, then runs smoke:live if examples/smoke-consumer/.env defines USER_NAME and USER_PASSWORD (and optionally BASE_URL; see examples/smoke-consumer/.env.example). Never commit .env.


Local linking

cd path/to/digitalwoods.io-woodsportal-client-sdk
npm run build
npm link

cd path/to/your-app
npm link woodsportal-client-sdk

Consume with the same import paths as npm:

import { api } from 'woodsportal-client-sdk'
import { useTable, useSync } from 'woodsportal-client-sdk/react'
// import { useTable, useSync } from "woodsportal-client-sdk/vue";
// import { useTable, useSync } from "woodsportal-client-sdk/angular";

After npm run build, the main entry and framework adapters (/react, /vue, /angular) share one store2 state via ESM code-splitting — API calls like api.objects.list() update the same stores your composables subscribe to.

React

import { useTable } from 'woodsportal-client-sdk/react'

function ObjectsTable() {
    const table = useTable()
    // table.tableData, table.setTableData(...)
}

Vue 3

Call composables inside setup() (or <script setup>):

<script setup lang="ts">
import { useTable } from 'woodsportal-client-sdk/vue'

const table = useTable()
</script>

Angular (16+)

Call composables in an injection context (constructor, field initializer, or runInInjectionContext):

import { Component } from '@angular/core'
import { useTable } from 'woodsportal-client-sdk/angular'

@Component({
    /* ... */
})
export class ObjectsTableComponent {
    readonly table = useTable()
}

Cache purge (CRM Sync)

Prefer POST /api/{hubId}/{portalId}/cache-purge-jobs over habitual cache=false on list reads. Requires FEATURE_CACHE_PURGE_API_ENABLED on the API.

| Export | Use | | ------------------------------------------------------- | --------------------------------------------------- | | createCachePurgeJob | POST + optional warm job poll | | buildCrmListPurgeTarget / buildCrmSinglePurgeTarget | List or detail scope | | buildEngagementPurgeTarget | notes / emails / files (requires recordIds) | | purgeCrmListCache / purgeEngagementCaches | Convenience wrappers returning PurgeResult | | purgeCrmObjectDataCache | Legacy list-only boolean shorthand |

API guide: woodsportal-api/docs/CACHE-PURGE-API.md in the monorepo. Types: src/types/cache-purge.ts; helpers: src/utils/cache/.


MFA & login (client lane)

When POST /api/auth/login returns twoFactorRequired: true, the SDK stores only the temporary access JWT — not the refresh token. Complete MFA with api.verifyOtp() (or pending passkey verify); on success the SDK persists the full session.

Full guide: docs/MFA-SECURITY-SDK.md (every method, payload, and example).

Backend reference: woodsportal-api/docs/MFA-FRONTEND-DEVELOPER-GUIDE.md.

Login & MFA step (unauthenticated)

| SDK method | HTTP | Purpose | | --------------------------------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------- | | login({ username, password }) | POST /api/auth/login?hubId= | Password login; may return twoFactorRequired | | verifyOtp({ token, otp, method }) | POST /api/auth/verify-otp?hubId= | Complete OTP/TOTP/backup MFA step; full session on success | | sendMfaOtp({ token, method }) | POST /api/auth/mfa/pending/otp/send | Resend OTP or switch to email/SMS on MFA gate | | pendingPasskeyOptions({ token, portalId? }) | POST /api/auth/mfa/pending/passkey/authenticate/options | Start passkey MFA-step ceremony | | pendingPasskeyVerify({ token, challengeId, credential, portalId? }) | POST /api/auth/mfa/pending/passkey/authenticate/verify?hubId= | Finish passkey MFA step; full session on success | | passkeyLoginOptions({ email, hubId?, portalId? }) | POST /api/auth/passkey/login/options?hubId= | Passwordless passkey login start | | passkeyLoginVerify({ challengeId, credential, portalId? }) | POST /api/auth/passkey/login/verify?hubId= | Passwordless login finish; may still require MFA |

MFA enrollment (authenticated)

| SDK method | HTTP | Purpose | | --------------------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------- | | getMfaStatus({ portalId? }) | GET /api/auth/mfa/status?portalId= | Enrollment + policy snapshot | | setMfaPreferences({ defaultMethod, portalId? }) | PUT /api/auth/mfa/preferences?portalId= | Set scoped default MFA method | | startPhoneVerify({ phone }) | POST /api/auth/mfa/phone/verify/start | Send phone verification OTP (E.164) | | confirmPhoneVerify({ phone, code }) | POST /api/auth/mfa/phone/verify/confirm | Confirm phone; enables SMS at login | | totpEnrollStart({ portalId? }) | POST /api/auth/mfa/totp/enroll/start?portalId= | Start TOTP; returns QR/otpauthUri | | totpEnrollVerify({ code, portalId? }) | POST /api/auth/mfa/totp/enroll/verify?portalId= | Confirm TOTP; backup codes returned once | | totpDisable({ password }) | POST /api/auth/mfa/totp/disable | Disable TOTP for current scope | | webauthnRegisterOptions({ portalId? }) | POST /api/auth/mfa/webauthn/register/options?portalId= | Passkey registration ceremony | | webauthnRegisterVerify({ challengeId, credential, nickname?, portalId? }) | POST /api/auth/mfa/webauthn/register/verify?portalId= | Complete passkey registration | | webauthnAuthOptions({ portalId? }) | POST /api/auth/mfa/webauthn/authenticate/options?portalId= | Logged-in passkey re-verify | | webauthnAuthVerify({ challengeId, credential, portalId? }) | POST /api/auth/mfa/webauthn/authenticate/verify?portalId= | Complete logged-in passkey verify | | listWebauthnCredentials({ portalId? }) | GET /api/auth/mfa/webauthn/credentials?portalId= | List passkeys | | deleteWebauthnCredential({ credentialRecordId, portalId? }) | DELETE /api/auth/mfa/webauthn/credentials/{id}?portalId= | Remove a passkey |

WebAuthn ceremonies use @simplewebauthn/browser in the host app; the SDK transports credential JSON only.


Security settings (client lane)

Use dedicated security endpoints for the account Security page — not GET /me + GET /mfa/status. Full contract: woodsportal-api/docs/SECURITY-FRONTEND-DEVELOPER-GUIDE.md. Examples: docs/MFA-SECURITY-SDK.md.

| SDK method | HTTP | Purpose | | ---------------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------- | | getSecurityOverview({ portalId? }) | GET /api/auth/security/overview?portalId= | Password age, MFA methods, policy flags | | getSecurityLoginActivity({ page?, limit?, sort? }) | GET /api/auth/security/login-activity | Paginated login history | | getSecuritySessions({ currentFamilyId?, refreshToken? }) | GET /api/auth/security/sessions | Active sessions; pass refresh to mark current | | revokeSecuritySession({ familyId, refreshToken? }) | POST /api/auth/security/sessions/{familyId}/revoke | Sign out one device | | revokeOtherSecuritySessions({ refreshToken? }) | POST /api/auth/security/sessions/revoke-others | Sign out all other devices |

Pass refreshToken (or use getRefreshToken() from SDK cookies) so the API can mark the current session when listing or revoking others.


Security & privacy

  • Send credentials and tokens only over HTTPS in production.
  • Never commit API keys, client secrets, or personal tokens into application source.
  • Prefer short-lived access tokens and secure httpOnly / Secure cookie policies where your architecture allows.
  • Redact Authorization headers and passwords in logs and crash reports.

Semver & compatibility

Follow semantic versioning for this package. Breaking HTTP contract or breaking TypeScript export shape → major. Backward-compatible endpoints or types → minor. Fixes → patch. Details per release are recorded in CHANGELOG.md.


License

ISC — see LICENSE.