@xemahq/identity-client
v0.32.0
Published
NestJS client library and standalone HTTP client for Identity API - service registration, credential management, and JWT token lifecycle
Readme
@xemahq/identity-client
TypeScript client and NestJS module for calling Identity API
Overview
A client for services that authenticate to Identity API and call it. A service holds no secret of its own: it
presents the platform attestation it already has (a projected Kubernetes ServiceAccount token, SPIFFE JWT-SVID or
federated OIDC credential) and receives a short-lived principal context. It has no dependencies beyond native fetch.
When to use it
Use it from any service that needs its own token, a token for one audience or organization, a context for a user it acts for, or Identity API's membership endpoints.
Installation
npm install @xemahq/identity-clientUsage
import { IdentityBootstrapModule, fileCredentialReader } from '@xemahq/identity-client';
IdentityBootstrapModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
serviceName: 'report-worker',
workloadContext: {
identityApiUrl: config.getOrThrow('IDENTITY_API_URL'),
realm: config.getOrThrow('IDENTITY_REALM'),
audience: 'platform', // a trust domain the realm declares
readCredential: fileCredentialReader('/var/run/secrets/identity/token'),
},
}),
});The workload must be bound to a service principal or the boundary refuses it. Every failure throws
WorkloadContextError; nothing falls back to another credential. State exactly one credential source:
workloadContext, or clientId + clientSecret + tokenEndpoint together for an enrolled client (a workload outside
any attesting platform).
IdentityBootstrapService
| Method | Returns |
| --- | --- |
| getAccessToken(options?) | This service's own token, cached and refreshed before expiry |
| getAccessTokenForAudience(audience, options?) | A token for one audience granted to this workload |
| getWorkerContext({ organizationId, attribution? }, options?) | A terminal token for an agent worker pod in one organization (needs mayMintWorkerContexts) |
| getPrincipalContext({ subjectToken, organizationId?, audience?, realm? }, options?) | A context for a user's upstream token (not cached; valid up to 300s) |
| createDelegation, mintDelegatedContext, renewDelegation, revokeDelegation | Delegation (RFC 8693) as this workload |
| getUserAccessToken({ userId, actorSubject, orgId, ... }) | A user token minted by Identity API naming this service as actor |
| invalidateAccessToken, invalidateAccessTokenForAudience, invalidateUserAccessToken | Evict a credential a peer rejected |
| waitForReady(timeoutMs?), isMember(orgId, userId), listOrgIds() | Readiness and membership checks |
Acquisitions are single-flight. options.signal ends only that caller's wait (IdentityTokenWaitAbortedError);
tokenRequestTimeoutMs bounds an enrolled client's token request (IdentityTokenRequestTimeoutError).
IdentityApiClient
Standalone HTTP client for Identity API's internal endpoints. Give it exactly one credential:
const client = new IdentityApiClient({
baseUrl: 'http://identity-api:3053',
serviceTokenProvider: () => identity.getAccessToken(), // or: internalToken: provisionedCredential
});
await client.verifyMembership(userId, orgId);
await client.listOrganizationMembers(orgId);Organization create, update and delete take { actingUserToken }, sent as the X-Xema-Acting-User-Token header; only
a declared organization-lifecycle relay may call them. A realm's declarer states the realm's complete set of workload bindings with
declareWorkloadBindings({ generation, entries }), built with that realm's dedicated internalToken (one realm, the
one audience identity.workload-bindings.declare). The digest is computed for you (workloadBindingDeclarationDigest),
per-entry outcomes are in the answer, and refusals carry a DeclarationRefusalCode (declaration-generation-stale
means a newer generation is in force and nothing changed; declaration-source-conflict carries a
DeclarationSourceConflictReason in details.reason).
Methods throw IdentityApiError; branch on error.isPermanent
and error.code (for example PrincipalContextRefusalCode), never on the message. Transient failures are timeouts,
5xx, 408 and 429.
Other entry points
WorkloadContextSource: the workload exchange without NestJS (get(),getContextWithExpiry(),invalidate(rejected?),derive({ audience, organizationId? })).@xemahq/identity-client/principal-context-claims: claim names and closed values (SUBJECT_KIND_CLAIM,ORG_ROLES_CLAIM,ContextSubjectKind) with no runtime dependency, for receivers that only verify contexts.
License
Business Source License 1.1, converting to MPL-2.0 four years after each release. See LICENSE.
