Skip to main content

JavaScript SDK

Goal

This guide walks you through installing, configuring, and using the Keymate JavaScript SDK to check permissions, manage Bearer and DPoP tokens, and retrieve organization context from Node.js or browser applications. By the end, you will be able to initialize the SDK, set access tokens, check single and multiple permissions, retrieve organization context, handle errors, and optionally use DPoP sender-constrained tokens.

Audience

  • JavaScript and TypeScript developers building Node.js backends that need fine-grained authorization
  • Frontend developers building React, Next.js, or browser applications with Keymate integration
  • Teams integrating with the Keymate Access Gateway

Prerequisites

  • Node.js 18 or later
  • A running Keymate environment with Access Gateway accessible over HTTP
  • A registered Keycloak client identifier (e.g., my-client-id)
  • npm, pnpm, or yarn package manager

Before You Start

The JavaScript SDK acts as a Policy Enforcement Point (PEP) — the component that enforces authorization decisions within your application. It communicates with the Access Gateway, which serves as the PDP Proxy and Edge Orchestrator handling token validation, access rule evaluation, and decision caching on behalf of all enforcers (PEPs).

The SDK supports two authorization schemes:

  • Bearer tokens — standard OAuth 2.0 bearer tokens (RFC 6750)
  • DPoP tokens — sender-constrained tokens (RFC 9449) that bind the access token to a cryptographic key pair, preventing token replay by third parties

The SDK runs in both Node.js and browser environments. Storage is auto-detected: IndexedDB in the browser, in-memory in Node.js. You can override this behavior through configuration.

Steps

1. Install the SDK

npm install @keymate/access-sdk-js

The SDK requires Node.js 18 or later. It ships with TypeScript type definitions included — no separate @types package is needed.

2. Initialize the client

Create an SDK instance using the KeymateAccessSDK.create() factory method:

Initialize the SDK
import { KeymateAccessSDK } from '@keymate/access-sdk-js';

const sdk = KeymateAccessSDK.create({
baseURL: 'https://gateway.example.com',
clientId: 'my-client-id',
});

Configuration options

The KeymateSDKConfig object accepts the following properties:

PropertyTypeRequiredDefaultDescription
baseURLstringYesBase URL of the Access Gateway
clientIdstringYesKeycloak client identifier, sent as the Keymate-Client-Id header
timeoutnumberNo30000Request timeout in milliseconds
enableCachebooleanNotrueEnable permission result caching
cacheTimeoutnumberNo300000 (5 min)Cache time-to-live in milliseconds
debugbooleanNofalseEnable debug-level logging
retryConfigRetryConfigNoSee belowRetry strategy configuration
storageobjectNo{ type: 'auto' }Storage configuration for caching

Retry configuration

The SDK retries failed requests using exponential backoff by default. Customize retry behavior by passing a retryConfig object:

Custom retry configuration
const sdk = KeymateAccessSDK.create({
baseURL: 'https://gateway.example.com',
clientId: 'my-client-id',
retryConfig: {
maxRetries: 3,
strategy: 'exponential', // 'fixed' or 'exponential'
baseDelay: 1000,
maxDelay: 30000,
jitter: 0.1,
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
},
});
PropertyTypeDefaultDescription
maxRetriesnumber3Maximum number of retry attempts
strategy'fixed' | 'exponential''exponential'Retry delay strategy
baseDelaynumber1000Base delay in milliseconds
maxDelaynumber30000Maximum delay for exponential strategy
jitternumber0Jitter factor (0–1) for delay randomization
retryableStatusCodesnumber[][408, 429, 500, 502, 503, 504]HTTP status codes that trigger retry

Storage configuration

The storage property controls how the SDK persists cached permission results:

PropertyTypeDefaultDescription
type'auto' | 'memory' | 'indexeddb''auto'Storage backend (auto uses IndexedDB in browsers, memory in Node.js)
prefixstring'keymate:'Key prefix for stored items
storageStorageCustom unstorage instance (overrides type)

3. Set the access token

Before making any authorization calls, set the access token obtained from your identity provider (Keycloak):

Set a Bearer token
await sdk.setAccessToken(token);

You can also provide a refresh token for use in token renewal flows:

Set a Bearer token with refresh token
await sdk.setAccessToken(token, { refreshToken: 'your-refresh-token' });

// Backward-compatible string form also works:
await sdk.setAccessToken(token, 'your-refresh-token');

Token events

The SDK emits events when the token state changes. Use these to coordinate token refresh, logging, or UI updates:

Listen for token events
// Fires when a token is set
sdk.on('tokenSet', (event) => {
console.log('Token type:', event.token.type);
console.log('First token?', event.isInitial);
});

// Fires when the gateway returns 401 (token expired/invalid)
sdk.on('tokenExpired', async (event) => {
console.log('Token expired:', event.error.message);

// Refresh the token and retry the failed request
const newToken = await myAuthService.refreshToken();
await event.retry({
token: newToken,
type: 'Bearer',
expiresAt: Date.now() + 3600000,
});
});

// Fires when the token is cleared
sdk.on('tokenCleared', (event) => {
console.log('Token cleared, reason:', event.reason);
// reason: 'logout' | 'expired' | 'revoked' | 'manual'
});

Utility methods

MethodReturn TypeDescription
getAccessToken()AccessToken | undefinedReturns the current token object
isAuthenticated()booleanReturns true if a non-expired token is set
clearAccessToken(reason?)thisClears the token; reason defaults to 'manual'

4. Check permissions

The SDK provides three methods for permission checking, each returning a PermissionCheckResponse.

Check a single resource

Single permission check
import { KeymateAccessSDK } from '@keymate/access-sdk-js';

const resource = {
name: 'employee-data',
scopes: ['can-view'],
};

const context = {
method: 'get',
path: '/api/employees',
headers: JSON.stringify({
'x-user-id': 'user-123',
}),
};

const result = await sdk.checkPermission(resource, context);

if (result.status === 'GRANT') {
// User has permission — proceed
}

Check multiple resources

Check permissions for several resources in a single call:

Multiple permission check
const resources = [
{ name: 'employee-data', scopes: ['can-view', 'can-edit'] },
{ name: 'financial-data', scopes: ['can-view'] },
];

const context = {
method: 'post',
path: '/api/reports',
};

const result = await sdk.checkMultiplePermissions(resources, context);

// Inspect individual permission entries
result.permissions.forEach((entry) => {
console.log(`${entry.resource}:${entry.scope}${entry.status}`);
});

Check all available resources

Discover all resources the authenticated user has access to:

All available resources
const result = await sdk.checkAllAvailableResources({
method: 'get',
path: '/api/dashboard',
});

const grantedResources = result.permissions.filter(
(entry) => entry.status === 'GRANT'
);

Type reference

ResourceScope — defines a resource and the scopes to check:

PropertyTypeDescription
namestringResource name (e.g., "employee-data", "invoice")
scopesstring[]Scopes or actions to check (e.g., ["can-view", "can-edit"])

RequestContext — provides context for policy evaluation. All object-type fields must be JSON-stringified:

PropertyTypeDescription
methodstringHTTP method ("get", "post", "put", "delete", etc.)
pathstringRequest path (e.g., "/api/employees/123")
bodystringJSON-stringified request body
headersstringJSON-stringified request headers
contextstringJSON-stringified additional context (IP, amount, region, etc.)
resourcestringJSON-stringified resource metadata (owner, type, organization)

PermissionCheckResponse — the response from all permission check methods:

PropertyTypeDescription
status'GRANT' | 'DENY'Overall permission status
permissionsPermissionEntry[]Individual permission results

Each PermissionEntry contains:

PropertyTypeDescription
resourcestringResource name
scopestringScope or action
status'GRANT' | 'DENY'Status of this specific permission

Fail-closed behavior

The SDK uses fail-closed security for permission checks. If the Access Gateway is unreachable, times out, or returns a server error, the SDK returns DENY for all requested resources rather than throwing an error. This ensures that authorization failures default to denying access.

The one exception is HTTP 401 (Unauthorized): authentication errors are propagated as thrown errors so your application can trigger token refresh or logout flows.

warning

The fail-closed design means your application receives a DENY response — not an exception — when the gateway is down. Always monitor your gateway connectivity to avoid unintended access denials.

5. Retrieve organization context

Fetch the authenticated user's organization hierarchy, including tenants and departments:

Organization context
const orgContext = await sdk.getOrganizationContext();

console.log('User ID:', orgContext.organization.userId);

orgContext.organization.tenants.forEach((tenant) => {
console.log(`Tenant: ${tenant.tenantName} (${tenant.tenantId})`);

tenant.departments.forEach((dept) => {
console.log(` Department: ${dept.departmentName} (${dept.departmentId})`);
});
});

OrganizationContextResponse shape:

PropertyTypeDescription
statusbooleanWhether the request succeeded
errorMessagestring | nullError message if the request failed
organizationOrganizationContextDataOrganization data

OrganizationContextData contains:

PropertyTypeDescription
userIdstringAuthenticated user's ID
tenantsOrganizationContextTenant[]Tenants the user belongs to

Each OrganizationContextTenant contains:

PropertyTypeDescription
tenantIdstringTenant identifier
tenantNamestringTenant display name
departmentsOrganizationContextDepartment[]Departments within the tenant

Each OrganizationContextDepartment contains:

PropertyTypeDescription
departmentIdstringDepartment identifier
departmentNamestringDepartment display name
valuestring | nullOptional value
valuecodestring | nullOptional value code

6. Handle errors

Error codes

The SDK defines the following error codes in the ErrorCode enum:

Error CodeCategoryDescription
NETWORK_ERRORNetworkGateway is unreachable or DNS resolution failed
TIMEOUT_ERRORNetworkRequest exceeded the configured timeout
ABORT_ERRORNetworkRequest was aborted via AbortSignal
API_ERRORHTTPGateway returned an unexpected HTTP error
UNAUTHORIZEDAuthToken is invalid or expired (HTTP 401)
INVALID_TOKENAuthJWT token format is invalid
TOKEN_EXPIREDAuthToken has passed its expiration time
CLAIM_VALIDATION_FAILEDAuthJWT claim validation failed
VALIDATION_FAILEDAuthGeneral validation failure
REFRESH_TOKEN_FAILEDAuthToken refresh attempt failed
PERMISSION_CHECK_FAILEDPermissionPermission check request failed
PERMISSION_DENIEDPermissionAccess was explicitly denied (HTTP 403)
ORG_CONTEXT_FAILEDOrganizationOrganization context request failed
CONFIG_ERRORConfigSDK configuration is invalid (missing baseURL or clientId)
STORAGE_ERRORStorageStorage operation failed (e.g., IndexedDB blocked)

APIError shape

All SDK errors follow the APIError interface:

APIError interface
interface APIError {
code: string; // ErrorCode value (e.g., 'NETWORK_ERROR')
message: string; // Human-readable error description
details?: Record<string, unknown>; // Additional error context
statusCode?: number; // HTTP status code, when applicable
cause?: Error; // Original error that caused this error
timestamp?: number; // Error timestamp for debugging
}

Error handling pattern

Use a helper function to distinguish authentication errors from other failures:

Recommended error handling
import { KeymateAccessSDK, type APIError } from '@keymate/access-sdk-js';

function isAuthError(error: unknown): boolean {
const e = error as APIError;
return e?.statusCode === 401 || e?.code === 'UNAUTHORIZED';
}

try {
const result = await sdk.checkPermission(
{ name: 'employee-data', scopes: ['can-view'] },
{ method: 'get', path: '/api/employees' }
);

if (result.status === 'GRANT') {
// Proceed with the operation
} else {
// Permission denied — show access denied message
}
} catch (error) {
if (isAuthError(error)) {
// Token expired or invalid — refresh or redirect to login
await authService.refreshToken();
} else {
// Network error, timeout, or server error
console.error('Authorization check failed:', (error as APIError).message);
}
}

Token expiry and retry

Use the tokenExpired event to handle 401 responses and retry the failed request with a refreshed token:

Token expiry retry pattern
sdk.on('tokenExpired', async (event) => {
console.log('Token expired:', event.error.code);

// Refresh the token through your auth service
const newToken = await myAuthService.refreshToken();

// Retry the failed request with the new token
await event.retry({
token: newToken,
type: 'Bearer',
expiresAt: Date.now() + 3600000,
});
});

7. Use DPoP tokens (optional)

DPoP (Demonstrating Proof-of-Possession, RFC 9449) binds access tokens to a cryptographic key pair. A stolen token is useless without the matching private key. The SDK generates a fresh DPoP proof for every outgoing request automatically.

Generate a key pair

Generate DPoP key pair
import { KeymateAccessSDK, DPoPManager } from '@keymate/access-sdk-js';

// Generate an ES256 (ECDSA P-256) key pair
const keyPair = await DPoPManager.generateKeyPair();

The generateKeyPair() method accepts an optional extractable parameter:

  • true (default) — the private key can be exported; required for server-side use where you may need to persist the key pair
  • false — the private key is non-extractable; recommended for browser and SPA use for stronger protection against key exfiltration

Set a DPoP token

After obtaining a DPoP-bound access token from your identity provider, set it on the SDK with the key pair:

Set DPoP token
const sdk = KeymateAccessSDK.create({
baseURL: 'https://gateway.example.com',
clientId: 'my-client-id',
});

const keyPair = await DPoPManager.generateKeyPair();

// Set the DPoP-bound token — the SDK attaches proofs automatically
await sdk.setAccessToken(dpopAccessToken, {
type: 'DPoP',
keyPair,
refreshToken: 'your-refresh-token', // optional
});

// All subsequent requests include Authorization: DPoP <token> and a DPoP proof header
const result = await sdk.checkPermission(
{ name: 'employee-data', scopes: ['can-view'] },
{ method: 'get', path: '/api/employees' }
);
note

Keycloak must be configured with a client that has DPoP enabled. Refer to your Keycloak administrator if you are unsure whether your client supports DPoP.

Validation Scenario

Scenario

A Node.js application checks whether the authenticated user has can-view permission on the employee-data resource.

Expected Result

  • If the user has the permission, checkPermission returns a response with status: 'GRANT'.
  • If the user lacks the permission, the response has status: 'DENY'.
  • If the token is invalid or expired, the SDK throws an error with code: 'UNAUTHORIZED' and the tokenExpired event fires.

How to Verify

  1. Install the SDK: npm install @keymate/access-sdk-js
  2. Initialize the SDK with your Access Gateway URL and client ID.
  3. Obtain a valid access token from Keycloak and call setAccessToken().
  4. Call checkPermission() with the employee-data resource and can-view scope.
  5. Verify the response status is 'GRANT'.
Verification script
import { KeymateAccessSDK } from '@keymate/access-sdk-js';

const sdk = KeymateAccessSDK.create({
baseURL: 'https://gateway.example.com',
clientId: 'my-client-id',
});

await sdk.setAccessToken(yourAccessToken);

const result = await sdk.checkPermission(
{ name: 'employee-data', scopes: ['can-view'] },
{ method: 'get', path: '/api/employees' }
);

console.log('Status:', result.status); // Expected: 'GRANT'
console.log('Permissions:', result.permissions);

await sdk.destroy();

Troubleshooting

SymptomLikely CauseResolution
NETWORK_ERROR — gateway unreachableAccess Gateway URL is incorrect or the gateway is downVerify the baseURL is reachable from your application. Check network policies, DNS, and firewall rules.
TIMEOUT_ERROR — request timed outGateway is slow to respond or under heavy loadIncrease the timeout configuration value. Check gateway health and resource utilization.
UNAUTHORIZED / TOKEN_EXPIRED — 401 on every requestAccess token is expired or invalidRefresh the token using the tokenExpired event or your auth service. Verify the token was issued by the correct Keycloak realm.
PERMISSION_DENIED — unexpected DENYResource name or scope does not match policy definitionsVerify the name and scopes values match the resources configured in your Keymate policy. Use checkAllAvailableResources() to list granted permissions.
STORAGE_ERROR — storage operation failedBrowser blocks IndexedDB (private browsing, storage quota)Set storage.type to 'memory' in the SDK configuration, or handle the error and fall back to uncached operation.
CONFIG_ERROR — missing baseURL or clientIdSDK initialized with empty or missing required fieldsProvide both baseURL and clientId in the configuration object passed to KeymateAccessSDK.create().
DPoP proof validation failsKey pair mismatch or clock skewGenerate a fresh key pair with DPoPManager.generateKeyPair(). Verify the system clock is synchronized.

Next Steps

Does the JavaScript SDK work in the browser?

Yes. The SDK runs in both Node.js and browser environments. It auto-detects the runtime and selects the appropriate storage backend — IndexedDB in browsers and in-memory storage in Node.js. You can override this by setting storage.type in the configuration.

What happens when the Access Gateway is unreachable?

The SDK uses fail-closed behavior for permission checks. If the gateway is unreachable, times out, or returns a server error, the SDK returns a DENY response for all requested resources instead of throwing an error. This ensures that authorization failures default to denying access. The exception is HTTP 401, which is propagated as a thrown error so your application can handle token refresh.

When should I use DPoP instead of Bearer tokens?

Use DPoP when you need sender-constrained tokens for higher security environments. DPoP binds the access token to a cryptographic key pair, so a stolen token cannot be replayed without the private key. This is recommended for applications handling sensitive data or operating in zero-trust architectures. Your Keycloak client must have DPoP enabled.

Does the SDK cache permission results?

Yes. Permission result caching is enabled by default with a 5-minute TTL. You can configure caching through the enableCache and cacheTimeout properties when initializing the SDK. Set enableCache to false to disable caching entirely.

How do I clean up SDK resources?

Call await sdk.destroy() when you no longer need the SDK instance. This clears the permission cache, removes stored data, and releases the access token. This is important in long-running applications and test environments to prevent resource leaks.