woodsportal-client-sdk
v4.0.11
Published
Official TypeScript/JavaScript SDK for WoodsPortal API - Authentication, user management, pipelines, and more
Maintainers
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.ts ↔ src/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
- Pin the major version in
package.json(e.g.^1.1.0) and readCHANGELOG.mdbefore upgrades. - Call
initializeHttpClientonce at startup with abaseURLfrom environment (never hardcode secrets; never ship a mystery default API host to production without review). - Use HTTPS for
baseURLin every deployed environment. - Do not log passwords, refresh tokens, access tokens, or full API error bodies in production telemetry.
- Handle errors with
try/await catcharoundmutate(), and map user-visible text withgetFormErrors/getFieldErrorswhen the API returns validation payloads. - Align hub context with how your shell stores HubSpot data (
core/utils/hub-context.tsreads hub / dev-portal identifiers from app storage).
Install
npm install woodsportal-client-sdkyarn add woodsportal-client-sdkConfigure 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:
- Non-HTTP failures (timeout, offline, 503) → transport variants via
classifyHttpError. - API responses →
parseApiErrorPayload+ lookup inAPI_ERROR_DISPLAY_CONFIG(all active codes + reserved fallbacks). - Unknown
errorCode→ fallback bycategory(AUTH,ACCESS,VALIDATION,FILE,RATE_LIMIT, …). - Description: prefers API
errorMessage(server i18n); uses configfallbackDescriptiononly when the API message is empty. Does not surfacedetailedMessageto 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` hereReact 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-checkBuild: 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:consumerRun 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-sdkConsume 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.
