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
- pnpm
- yarn
npm install @keymate/access-sdk-js
pnpm add @keymate/access-sdk-js
yarn add @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:
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:
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
baseURL | string | Yes | — | Base URL of the Access Gateway |
clientId | string | Yes | — | Keycloak client identifier, sent as the Keymate-Client-Id header |
timeout | number | No | 30000 | Request timeout in milliseconds |
enableCache | boolean | No | true | Enable permission result caching |
cacheTimeout | number | No | 300000 (5 min) | Cache time-to-live in milliseconds |
debug | boolean | No | false | Enable debug-level logging |
retryConfig | RetryConfig | No | See below | Retry strategy configuration |
storage | object | No | { 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:
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],
},
});
| Property | Type | Default | Description |
|---|---|---|---|
maxRetries | number | 3 | Maximum number of retry attempts |
strategy | 'fixed' | 'exponential' | 'exponential' | Retry delay strategy |
baseDelay | number | 1000 | Base delay in milliseconds |
maxDelay | number | 30000 | Maximum delay for exponential strategy |
jitter | number | 0 | Jitter factor (0–1) for delay randomization |
retryableStatusCodes | number[] | [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:
| Property | Type | Default | Description |
|---|---|---|---|
type | 'auto' | 'memory' | 'indexeddb' | 'auto' | Storage backend (auto uses IndexedDB in browsers, memory in Node.js) |
prefix | string | 'keymate:' | Key prefix for stored items |
storage | Storage | — | Custom unstorage instance (overrides type) |
3. Set the access token
Before making any authorization calls, set the access token obtained from your identity provider (Keycloak):
await sdk.setAccessToken(token);
You can also provide a refresh token for use in token renewal flows:
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:
// 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
| Method | Return Type | Description |
|---|---|---|
getAccessToken() | AccessToken | undefined | Returns the current token object |
isAuthenticated() | boolean | Returns true if a non-expired token is set |
clearAccessToken(reason?) | this | Clears 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
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:
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:
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:
| Property | Type | Description |
|---|---|---|
name | string | Resource name (e.g., "employee-data", "invoice") |
scopes | string[] | 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:
| Property | Type | Description |
|---|---|---|
method | string | HTTP method ("get", "post", "put", "delete", etc.) |
path | string | Request path (e.g., "/api/employees/123") |
body | string | JSON-stringified request body |
headers | string | JSON-stringified request headers |
context | string | JSON-stringified additional context (IP, amount, region, etc.) |
resource | string | JSON-stringified resource metadata (owner, type, organization) |
PermissionCheckResponse — the response from all permission check methods:
| Property | Type | Description |
|---|---|---|
status | 'GRANT' | 'DENY' | Overall permission status |
permissions | PermissionEntry[] | Individual permission results |
Each PermissionEntry contains:
| Property | Type | Description |
|---|---|---|
resource | string | Resource name |
scope | string | Scope 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.
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:
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:
| Property | Type | Description |
|---|---|---|
status | boolean | Whether the request succeeded |
errorMessage | string | null | Error message if the request failed |
organization | OrganizationContextData | Organization data |
OrganizationContextData contains:
| Property | Type | Description |
|---|---|---|
userId | string | Authenticated user's ID |
tenants | OrganizationContextTenant[] | Tenants the user belongs to |
Each OrganizationContextTenant contains:
| Property | Type | Description |
|---|---|---|
tenantId | string | Tenant identifier |
tenantName | string | Tenant display name |
departments | OrganizationContextDepartment[] | Departments within the tenant |
Each OrganizationContextDepartment contains:
| Property | Type | Description |
|---|---|---|
departmentId | string | Department identifier |
departmentName | string | Department display name |
value | string | null | Optional value |
valuecode | string | null | Optional value code |
6. Handle errors
Error codes
The SDK defines the following error codes in the ErrorCode enum:
| Error Code | Category | Description |
|---|---|---|
NETWORK_ERROR | Network | Gateway is unreachable or DNS resolution failed |
TIMEOUT_ERROR | Network | Request exceeded the configured timeout |
ABORT_ERROR | Network | Request was aborted via AbortSignal |
API_ERROR | HTTP | Gateway returned an unexpected HTTP error |
UNAUTHORIZED | Auth | Token is invalid or expired (HTTP 401) |
INVALID_TOKEN | Auth | JWT token format is invalid |
TOKEN_EXPIRED | Auth | Token has passed its expiration time |
CLAIM_VALIDATION_FAILED | Auth | JWT claim validation failed |
VALIDATION_FAILED | Auth | General validation failure |
REFRESH_TOKEN_FAILED | Auth | Token refresh attempt failed |
PERMISSION_CHECK_FAILED | Permission | Permission check request failed |
PERMISSION_DENIED | Permission | Access was explicitly denied (HTTP 403) |
ORG_CONTEXT_FAILED | Organization | Organization context request failed |
CONFIG_ERROR | Config | SDK configuration is invalid (missing baseURL or clientId) |
STORAGE_ERROR | Storage | Storage operation failed (e.g., IndexedDB blocked) |
APIError shape
All SDK errors follow the 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:
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:
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
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 pairfalse— 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:
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' }
);
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,
checkPermissionreturns a response withstatus: '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 thetokenExpiredevent fires.
How to Verify
- Install the SDK:
npm install @keymate/access-sdk-js - Initialize the SDK with your Access Gateway URL and client ID.
- Obtain a valid access token from Keycloak and call
setAccessToken(). - Call
checkPermission()with theemployee-dataresource andcan-viewscope. - Verify the response
statusis'GRANT'.
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
| Symptom | Likely Cause | Resolution |
|---|---|---|
NETWORK_ERROR — gateway unreachable | Access Gateway URL is incorrect or the gateway is down | Verify the baseURL is reachable from your application. Check network policies, DNS, and firewall rules. |
TIMEOUT_ERROR — request timed out | Gateway is slow to respond or under heavy load | Increase the timeout configuration value. Check gateway health and resource utilization. |
UNAUTHORIZED / TOKEN_EXPIRED — 401 on every request | Access token is expired or invalid | Refresh the token using the tokenExpired event or your auth service. Verify the token was issued by the correct Keycloak realm. |
PERMISSION_DENIED — unexpected DENY | Resource name or scope does not match policy definitions | Verify the name and scopes values match the resources configured in your Keymate policy. Use checkAllAvailableResources() to list granted permissions. |
STORAGE_ERROR — storage operation failed | Browser 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 clientId | SDK initialized with empty or missing required fields | Provide both baseURL and clientId in the configuration object passed to KeymateAccessSDK.create(). |
| DPoP proof validation fails | Key pair mismatch or clock skew | Generate a fresh key pair with DPoPManager.generateKeyPair(). Verify the system clock is synchronized. |
Next Steps
- Review the Access Gateway Overview to understand how permission checks are processed
- Learn about the Enforcement Pipeline to understand how permission check requests are processed
- Explore the Java SDK for JVM-based applications using the same authorization model
- Read about Sender-Constrained Tokens & DPoP for details on the DPoP security model
Related Docs
SDK Overview
Overview of all Keymate SDK offerings and their capabilities.
Java SDK
Install and use the Keymate Java SDK for JVM-based applications.
Access Gateway Overview
Gateway architecture, endpoints, and technology stack.
Sender-Constrained Tokens & DPoP
RFC 9449 DPoP specification and security model.
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.