http adapter
Delegates storage operations to a remote duck-iam API. Useful for client-server splits, microservices, and shared policy stores.
Install
import { IamHttpAdapter } from '@gentleduck/iam/adapters/http'
Delegates all storage operations to a remote API via fetch. Zero dependencies.
When to use
- Your access engine runs on a dedicated service
- Multiple applications share a single policy store
- Client-side code needs to evaluate permissions against a server (without exposing the database)
Usage
import { IamHttpAdapter } from '@gentleduck/iam/adapters/http'
import { IamEngine } from '@gentleduck/iam'
const adapter = new IamHttpAdapter({
baseUrl: 'https://api.example.com/access',
headers: {
Authorization: 'Bearer ' + serviceToken,
},
})
const engine = new IamEngine({ adapter })
Dynamic headers
Pass a function to compute headers per-request. Useful for rotating tokens or per-request context.
const adapter = new IamHttpAdapter({
baseUrl: 'https://api.example.com/access',
headers: async () => ({
Authorization: 'Bearer ' + (await getServiceToken()),
'X-Request-Id': crypto.randomUUID(),
}),
})
Custom fetch
Provide your own fetch for environments without a global fetch, or to add middleware (logging, retries, tracing).
import { IamHttpAdapter } from '@gentleduck/iam/adapters/http'
const adapter = new IamHttpAdapter({
baseUrl: 'https://api.example.com/access',
fetch: async (url, init) => {
console.log('Access API request:', url)
const res = await globalThis.fetch(url, init)
console.log('Access API response:', res.status)
return res
},
})
API endpoints
The HTTP adapter expects these REST endpoints on your server:
| Method | Path | Description |
|---|---|---|
| GET | /policies | List all policies |
| GET | /policies/:id | Get a single policy |
| PUT | /policies | Create or update a policy |
| DELETE | /policies/:id | Delete a policy |
| GET | /roles | List all roles |
| GET | /roles/:id | Get a single role |
| PUT | /roles | Create or update a role |
| DELETE | /roles/:id | Delete a role |
| GET | /subjects/:id/roles | Get a subject's roles |
| GET | /subjects/:id/scoped-roles | Get a subject's scoped roles |
| POST | /subjects/:id/roles | Assign a role (body: { roleId, scope? }) |
| DELETE | /subjects/:id/roles/:roleId | Revoke a role (query: ?scope=...) |
| GET | /subjects/:id/attributes | Get subject attributes |
| PATCH | /subjects/:id/attributes | Update subject attributes (body: attribute object) |
The built-in Express adminRouter covers part of this admin surface, but it does not implement the full HTTP adapter contract. To use IamHttpAdapter, either implement the complete endpoint set manually or extend the admin router with the missing item lookups, scoped-role reads, and subject-attribute endpoints.
Error handling
The HTTP adapter throws an Error with the message "@gentleduck/iam HTTP {status}: {responseText}" for any 4xx response. 5xx responses, AbortError, and Node connection errors (ECONNRESET, ECONNREFUSED, ETIMEDOUT, ENOTFOUND) are treated as transient and retried with exponential backoff + jitter. 4xx is never retried - it's treated as a definitive answer.
getPolicy(id) and getRole(id) return null on a 404, matching the IamAdapter.IAdapter contract.
A Content-Type: application/json header is sent on every request. If headers is a function, it is awaited before each request.
Retry, timeout, and circuit breaker
IamHttpAdapter ships full SRE plumbing built-in. All four knobs are optional:
new IamHttpAdapter({
baseUrl: process.env.IAM_API!,
timeoutMs: 2_000, // per-request abort (default 5_000; 0 disables)
retries: 2, // 3 total attempts on transient failure (default 2)
backoffMs: 100, // expo base: 100 -> 200 -> 400 + jitter (default 100)
circuitBreakerThreshold: 5, // open after N consecutive transient failures (default 5; 0 disables)
circuitBreakerCooldownMs: 30_000, // cooldown before half-open probe (default 30s)
})
Circuit breaker:
- Closed -> traffic flows.
- Open -> reject immediately for
circuitBreakerCooldownMs(no fetch attempt). - Half-open -> one probe allowed; success closes, failure re-opens. Concurrent callers reject fast so a recovering upstream doesn't get flooded.
Layered with the engine's own IConfig.adapterTimeoutMs - whichever fires first wins.
Constructor config
| Option | Type | Default | Description |
|---|---|---|---|
baseUrl | string | -- | Base URL for the access API (trailing slash stripped) |
headers | Record or () -> Record or Promise | {} | Static or dynamic headers |
fetch | typeof fetch | globalThis.fetch | Custom fetch implementation |
timeoutMs | number | 5000 | Per-request abort timeout. 0 disables. |
retries | number | 2 | Retries on transient failure (3 total attempts). |
backoffMs | number | 100 | Base exponential backoff in ms. |
circuitBreakerThreshold | number | 5 | Consecutive transient failures before opening. 0 disables. |
circuitBreakerCooldownMs | number | 30000 | Cooldown before half-open probe. |
Notes
IamHttpAdapterfetches authorization data over HTTP, but the consuming engine still evaluates permissions locally. It is not an outsourcing-evaluation adapter - it just moves storage behind a service boundary.- For server-side evaluation across services, expose
engine.permissions()as an endpoint and consume the resultingPermissionMapon clients via the client integrations.
Types
All types live under the Http namespace at @gentleduck/iam/adapters/http. Type-only - zero bundle cost.
Http.IConfig- constructor config forIamHttpAdapter(baseUrl, headers, fetch, retry / timeout / circuit breaker knobs).
import type { Http } from '@gentleduck/iam/adapters/http'
const config: Http.IConfig = {
baseUrl: process.env.IAM_API!,
timeoutMs: 2_000,
retries: 2,
}
The deprecated bare alias IHttpAdapterConfig remains for back-compat and will be removed in 3.0.